diff --git a/.eslintrc.js b/.eslintrc.js index 2736a5d6d3e57..f7f748516d9ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -622,6 +622,7 @@ module.exports = { ScrollTimeline: 'readonly', EventListenerOptionsOrUseCapture: 'readonly', FocusOptions: 'readonly', + OptionalEffectTiming: 'readonly', spyOnDev: 'readonly', spyOnDevAndProd: 'readonly', diff --git a/.github/workflows/compiler_discord_notify.yml b/.github/workflows/compiler_discord_notify.yml index 7a5f5db0fb988..5a57cf6a32c19 100644 --- a/.github/workflows/compiler_discord_notify.yml +++ b/.github/workflows/compiler_discord_notify.yml @@ -11,6 +11,7 @@ permissions: {} jobs: check_access: + if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest outputs: is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index 1d0a896984e26..0762df8ed64dd 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -280,6 +280,37 @@ jobs: if: steps.node_modules.outputs.cache-hit != 'true' - run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }} + # Hardcoded to improve parallelism + test-linter: + name: Test eslint-plugin-react-hooks + needs: [runtime_compiler_node_modules_cache] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: yarn + cache-dependency-path: | + yarn.lock + compiler/yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: | + **/node_modules + key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }} + - name: Install runtime dependencies + run: yarn install --frozen-lockfile + if: steps.node_modules.outputs.cache-hit != 'true' + - name: Install compiler dependencies + run: yarn install --frozen-lockfile + working-directory: compiler + if: steps.node_modules.outputs.cache-hit != 'true' + - run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh + - run: yarn workspace eslint-plugin-react-hooks test + # ----- BUILD ----- build_and_lint: name: yarn build and lint diff --git a/.github/workflows/runtime_discord_notify.yml b/.github/workflows/runtime_discord_notify.yml index 69e4c3453f343..8d047e697640d 100644 --- a/.github/workflows/runtime_discord_notify.yml +++ b/.github/workflows/runtime_discord_notify.yml @@ -11,6 +11,7 @@ permissions: {} jobs: check_access: + if: ${{ github.event.pull_request.draft == false }} runs-on: ubuntu-latest outputs: is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} diff --git a/.github/workflows/runtime_prereleases.yml b/.github/workflows/runtime_prereleases.yml index ee8dd72ce9665..ee340e079f3d3 100644 --- a/.github/workflows/runtime_prereleases.yml +++ b/.github/workflows/runtime_prereleases.yml @@ -17,6 +17,17 @@ on: description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.' required: false type: boolean + only_packages: + description: Packages to publish (space separated) + type: string + skip_packages: + description: Packages to NOT publish (space separated) + type: string + dry: + required: true + description: Dry run instead of publish? + type: boolean + default: true secrets: DISCORD_WEBHOOK_URL: description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.' @@ -61,15 +72,41 @@ jobs: if: steps.node_modules.outputs.cache-hit != 'true' - run: yarn --cwd scripts/release install --frozen-lockfile if: steps.node_modules.outputs.cache-hit != 'true' + - run: cp ./scripts/release/ci-npmrc ~/.npmrc - run: | GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }} - cp ./scripts/release/ci-npmrc ~/.npmrc - scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }} + - name: Check prepared files + run: ls -R build/node_modules + - if: '${{ inputs.only_packages }}' + name: 'Publish ${{ inputs.only_packages }}' + run: | + scripts/release/publish.js \ + --ci \ + --skipTests \ + --tags=${{ inputs.dist_tag }} \ + --onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}} + ${{ inputs.dry && '--dry' || '' }} + - if: '${{ inputs.skip_packages }}' + name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}' + run: | + scripts/release/publish.js \ + --ci \ + --skipTests \ + --tags=${{ inputs.dist_tag }} \ + --skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}} + ${{ inputs.dry && '--dry' || '' }} + - if: '${{ !(inputs.skip_packages && inputs.only_packages) }}' + name: 'Publish all packages' + run: | + scripts/release/publish.js \ + --ci \ + --tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}} + ${{ inputs.dry && '--dry' || '' }} - name: Notify Discord on failure if: failure() && inputs.enableFailureNotification == true uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 with: webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} embed-author-name: "GitHub Actions" - embed-title: 'Publish of $${{ inputs.release_channel }} release failed' + embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed' embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }} diff --git a/.github/workflows/runtime_prereleases_manual.yml b/.github/workflows/runtime_prereleases_manual.yml index 71e25ba073a83..407d931e90738 100644 --- a/.github/workflows/runtime_prereleases_manual.yml +++ b/.github/workflows/runtime_prereleases_manual.yml @@ -5,6 +5,25 @@ on: inputs: prerelease_commit_sha: required: true + only_packages: + description: Packages to publish (space separated) + type: string + skip_packages: + description: Packages to NOT publish (space separated) + type: string + dry: + required: true + description: Dry run instead of publish? + type: boolean + default: true + experimental_only: + type: boolean + description: Only publish to the experimental tag + default: false + force_notify: + description: Force a Discord notification? + type: boolean + default: false permissions: {} @@ -12,8 +31,26 @@ env: TZ: /usr/share/zoneinfo/America/Los_Angeles jobs: + notify: + if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }} + runs-on: ubuntu-latest + steps: + - name: Discord Webhook Action + uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 + with: + webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }} + embed-author-name: ${{ github.event.sender.login }} + embed-author-url: ${{ github.event.sender.html_url }} + embed-author-icon-url: ${{ github.event.sender.avatar_url }} + embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}" + embed-description: | + ```json + ${{ toJson(inputs) }} + ``` + embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }} publish_prerelease_canary: + if: ${{ !inputs.experimental_only }} name: Publish to Canary channel uses: facebook/react/.github/workflows/runtime_prereleases.yml@main permissions: @@ -33,6 +70,9 @@ jobs: # downstream consumers might still expect that tag. We can remove this # after some time has elapsed and the change has been communicated. dist_tag: canary,next + only_packages: ${{ inputs.only_packages }} + skip_packages: ${{ inputs.skip_packages }} + dry: ${{ inputs.dry }} secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -48,10 +88,15 @@ jobs: # different versions of the same package, even if they use different # dist tags. needs: publish_prerelease_canary + # Ensures the job runs even if canary is skipped + if: always() with: commit_sha: ${{ inputs.prerelease_commit_sha }} release_channel: experimental dist_tag: experimental + only_packages: ${{ inputs.only_packages }} + skip_packages: ${{ inputs.skip_packages }} + dry: ${{ inputs.dry }} secrets: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/runtime_prereleases_nightly.yml b/.github/workflows/runtime_prereleases_nightly.yml index a38e241d53996..f13a92e46f401 100644 --- a/.github/workflows/runtime_prereleases_nightly.yml +++ b/.github/workflows/runtime_prereleases_nightly.yml @@ -22,6 +22,7 @@ jobs: release_channel: stable dist_tag: canary,next enableFailureNotification: true + dry: false secrets: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -43,6 +44,7 @@ jobs: release_channel: experimental dist_tag: experimental enableFailureNotification: true + dry: false secrets: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/runtime_releases_from_npm_manual.yml b/.github/workflows/runtime_releases_from_npm_manual.yml index 51e38439553de..f164e9f080660 100644 --- a/.github/workflows/runtime_releases_from_npm_manual.yml +++ b/.github/workflows/runtime_releases_from_npm_manual.yml @@ -110,7 +110,7 @@ jobs: --tags=${{ inputs.tags }} \ --publishVersion=${{ inputs.version_to_publish }} \ --onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}} - ${{ inputs.dry && '--dry'}} + ${{ inputs.dry && '--dry' || '' }} - if: '${{ inputs.skip_packages }}' name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}' run: | @@ -119,7 +119,7 @@ jobs: --tags=${{ inputs.tags }} \ --publishVersion=${{ inputs.version_to_publish }} \ --skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}} - ${{ inputs.dry && '--dry'}} + ${{ inputs.dry && '--dry' || '' }} - name: Archive released package for debugging uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/shared_stale.yml b/.github/workflows/shared_stale.yml index a2c707973c927..c24895edc5da7 100644 --- a/.github/workflows/shared_stale.yml +++ b/.github/workflows/shared_stale.yml @@ -6,7 +6,10 @@ on: - cron: '0 * * * *' workflow_dispatch: -permissions: {} +permissions: + # https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions + issues: write + pull-requests: write env: TZ: /usr/share/zoneinfo/America/Los_Angeles diff --git a/CHANGELOG.md b/CHANGELOG.md index d5551df26c7de..a10cda01536b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,11 @@ An Owner Stack is a string representing the components that are directly respons * Updated `useId` to use valid CSS selectors, changing format from `:r123:` to `«r123»`. [#32001](https://github.com/facebook/react/pull/32001) * Added a dev-only warning for null/undefined created in useEffect, useInsertionEffect, and useLayoutEffect. [#32355](https://github.com/facebook/react/pull/32355) * Fixed a bug where dev-only methods were exported in production builds. React.act is no longer available in production builds. [#32200](https://github.com/facebook/react/pull/32200) -* Improved consistency across prod and dev to improve compatibility with Google Closure Complier and bindings [#31808](https://github.com/facebook/react/pull/31808) +* Improved consistency across prod and dev to improve compatibility with Google Closure Compiler and bindings [#31808](https://github.com/facebook/react/pull/31808) * Improve passive effect scheduling for consistent task yielding. [#31785](https://github.com/facebook/react/pull/31785) * Fixed asserts in React Native when passChildrenWhenCloningPersistedNodes is enabled for OffscreenComponent rendering. [#32528](https://github.com/facebook/react/pull/32528) * Fixed component name resolution for Portal [#32640](https://github.com/facebook/react/pull/32640) -* Added support for beforetoggle and toggle events on the dialog element. #32479 [#32479](https://github.com/facebook/react/pull/32479) +* Added support for beforetoggle and toggle events on the dialog element. [#32479](https://github.com/facebook/react/pull/32479) ### React DOM * Fixed double warning when the `href` attribute is an empty string [#31783](https://github.com/facebook/react/pull/31783) diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 39571fa092593..02813a8d2fd01 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -44,6 +44,7 @@ import { PrintedCompilerPipelineValue, } from './Output'; import {transformFromAstSync} from '@babel/core'; +import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint'; function parseInput( input: string, @@ -143,6 +144,7 @@ const COMMON_HOOKS: Array<[string, Hook]> = [ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { const results = new Map>(); const error = new CompilerError(); + const otherErrors: Array = []; const upsert: (result: PrintedCompilerPipelineValue) => void = result => { const entry = results.get(result.name); if (Array.isArray(entry)) { @@ -210,7 +212,11 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { }, logger: { debugLogIRs: logIR, - logEvent: () => {}, + logEvent: (_filename: string | null, event: LoggerEvent) => { + if (event.kind === 'CompileError') { + otherErrors.push(new CompilerErrorDetail(event.detail)); + } + }, }, }); transformOutput = invokeCompiler(source, language, opts); @@ -237,6 +243,10 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] { ); } } + // Only include logger errors if there weren't other errors + if (!error.hasErrors() && otherErrors.length !== 0) { + otherErrors.forEach(e => error.push(e)); + } if (error.hasErrors()) { return [{kind: 'err', results, error: error}, language]; } diff --git a/compiler/apps/playground/scripts/link-compiler.sh b/compiler/apps/playground/scripts/link-compiler.sh index 1ee5f0b81bf09..96188f7b45137 100755 --- a/compiler/apps/playground/scripts/link-compiler.sh +++ b/compiler/apps/playground/scripts/link-compiler.sh @@ -8,8 +8,8 @@ set -eo pipefail HERE=$(pwd) -cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE -cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE +cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE" +cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE" yarn --silent link babel-plugin-react-compiler yarn --silent link react-compiler-runtime diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index fe97c8d642f60..c5ca3434b1b54 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -104,6 +104,8 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF import {CompilerError} from '..'; import {validateStaticComponents} from '../Validation/ValidateStaticComponents'; import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions'; +import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects'; +import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -227,15 +229,27 @@ function runWithEnvironment( analyseFunctions(hir); log({kind: 'hir', name: 'AnalyseFunctions', value: hir}); - const fnEffectErrors = inferReferenceEffects(hir); - if (env.isInferredMemoEnabled) { - if (fnEffectErrors.length > 0) { - CompilerError.throw(fnEffectErrors[0]); + if (!env.config.enableNewMutationAliasingModel) { + const fnEffectErrors = inferReferenceEffects(hir); + if (env.isInferredMemoEnabled) { + if (fnEffectErrors.length > 0) { + CompilerError.throw(fnEffectErrors[0]); + } + } + log({kind: 'hir', name: 'InferReferenceEffects', value: hir}); + } else { + const mutabilityAliasingErrors = inferMutationAliasingEffects(hir); + log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir}); + if (env.isInferredMemoEnabled) { + if (mutabilityAliasingErrors.isErr()) { + throw mutabilityAliasingErrors.unwrapErr(); + } } } - log({kind: 'hir', name: 'InferReferenceEffects', value: hir}); - validateLocalsNotReassignedAfterRender(hir); + if (!env.config.enableNewMutationAliasingModel) { + validateLocalsNotReassignedAfterRender(hir); + } // Note: Has to come after infer reference effects because "dead" code may still affect inference deadCodeElimination(hir); @@ -249,8 +263,21 @@ function runWithEnvironment( pruneMaybeThrows(hir); log({kind: 'hir', name: 'PruneMaybeThrows', value: hir}); - inferMutableRanges(hir); - log({kind: 'hir', name: 'InferMutableRanges', value: hir}); + if (!env.config.enableNewMutationAliasingModel) { + inferMutableRanges(hir); + log({kind: 'hir', name: 'InferMutableRanges', value: hir}); + } else { + const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, { + isFunctionExpression: false, + }); + log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir}); + if (env.isInferredMemoEnabled) { + if (mutabilityAliasingErrors.isErr()) { + throw mutabilityAliasingErrors.unwrapErr(); + } + validateLocalsNotReassignedAfterRender(hir); + } + } if (env.isInferredMemoEnabled) { if (env.config.assertValidMutableRanges) { @@ -277,7 +304,10 @@ function runWithEnvironment( validateNoImpureFunctionsInRender(hir).unwrap(); } - if (env.config.validateNoFreezingKnownMutableFunctions) { + if ( + env.config.validateNoFreezingKnownMutableFunctions || + env.config.enableNewMutationAliasingModel + ) { validateNoFreezingKnownMutableFunctions(hir).unwrap(); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts index d44f6108eaa57..773986a1b5e77 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts @@ -5,13 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -import invariant from 'invariant'; -import {HIRFunction, Identifier, MutableRange} from './HIR'; +import {HIRFunction, MutableRange, Place} from './HIR'; import { eachInstructionLValue, eachInstructionOperand, eachTerminalOperand, } from './visitors'; +import {CompilerError} from '..'; +import {printPlace} from './PrintHIR'; /* * Checks that all mutable ranges in the function are well-formed, with @@ -20,38 +21,43 @@ import { export function assertValidMutableRanges(fn: HIRFunction): void { for (const [, block] of fn.body.blocks) { for (const phi of block.phis) { - visitIdentifier(phi.place.identifier); - for (const [, operand] of phi.operands) { - visitIdentifier(operand.identifier); + visit(phi.place, `phi for block bb${block.id}`); + for (const [pred, operand] of phi.operands) { + visit(operand, `phi predecessor bb${pred} for block bb${block.id}`); } } for (const instr of block.instructions) { for (const operand of eachInstructionLValue(instr)) { - visitIdentifier(operand.identifier); + visit(operand, `instruction [${instr.id}]`); } for (const operand of eachInstructionOperand(instr)) { - visitIdentifier(operand.identifier); + visit(operand, `instruction [${instr.id}]`); } } for (const operand of eachTerminalOperand(block.terminal)) { - visitIdentifier(operand.identifier); + visit(operand, `terminal [${block.terminal.id}]`); } } } -function visitIdentifier(identifier: Identifier): void { - validateMutableRange(identifier.mutableRange); - if (identifier.scope !== null) { - validateMutableRange(identifier.scope.range); +function visit(place: Place, description: string): void { + validateMutableRange(place, place.identifier.mutableRange, description); + if (place.identifier.scope !== null) { + validateMutableRange(place, place.identifier.scope.range, description); } } -function validateMutableRange(mutableRange: MutableRange): void { - invariant( - (mutableRange.start === 0 && mutableRange.end === 0) || - mutableRange.end > mutableRange.start, - 'Identifier scope mutableRange was invalid: [%s:%s]', - mutableRange.start, - mutableRange.end, +function validateMutableRange( + place: Place, + range: MutableRange, + description: string, +): void { + CompilerError.invariant( + (range.start === 0 && range.end === 0) || range.end > range.start, + { + reason: `Invalid mutable range: [${range.start}:${range.end}]`, + description: `${printPlace(place)} in ${description}`, + loc: place.loc, + }, ); } 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 cfb15fb595ccc..104b08068151c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -47,7 +47,7 @@ import { makeType, promoteTemporary, } from './HIR'; -import HIRBuilder, {Bindings} from './HIRBuilder'; +import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder'; import {BuiltInArrayId} from './ObjectShape'; /* @@ -72,7 +72,7 @@ export function lower( env: Environment, // Bindings captured from the outer function, in case lower() is called recursively (for lambdas) bindings: Bindings | null = null, - capturedRefs: Array = [], + capturedRefs: Map = new Map(), ): Result { const builder = new HIRBuilder(env, { bindings, @@ -80,13 +80,13 @@ export function lower( }); const context: HIRFunction['context'] = []; - for (const ref of capturedRefs ?? []) { + for (const [ref, loc] of capturedRefs ?? []) { context.push({ kind: 'Identifier', identifier: builder.resolveBinding(ref), effect: Effect.Unknown, reactive: false, - loc: ref.loc ?? GeneratedSource, + loc, }); } @@ -181,6 +181,7 @@ export function lower( loc: GeneratedSource, value: lowerExpressionToTemporary(builder, body), id: makeInstructionId(0), + effects: null, }; builder.terminateWithContinuation(terminal, fallthrough); } else if (body.isBlockStatement()) { @@ -210,6 +211,7 @@ export function lower( loc: GeneratedSource, }), id: makeInstructionId(0), + effects: null, }, null, ); @@ -219,7 +221,7 @@ export function lower( params, fnType: bindings == null ? env.fnType : 'Other', returnTypeAnnotation: null, // TODO: extract the actual return type node if present - returnType: makeType(), + returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource), body: builder.build(), context, generator: func.node.generator === true, @@ -227,6 +229,7 @@ export function lower( loc: func.node.loc ?? GeneratedSource, env, effects: null, + aliasingEffects: null, directives, }); } @@ -287,6 +290,7 @@ function lowerStatement( loc: stmt.node.loc ?? GeneratedSource, value, id: makeInstructionId(0), + effects: null, }; builder.terminate(terminal, 'block'); return; @@ -1237,6 +1241,7 @@ function lowerStatement( kind: 'Debugger', loc, }, + effects: null, loc, }); return; @@ -1894,6 +1899,7 @@ function lowerExpression( place: leftValue, loc: exprLoc, }, + effects: null, loc: exprLoc, }); builder.terminateWithContinuation( @@ -2829,6 +2835,7 @@ function lowerOptionalCallExpression( args, loc, }, + effects: null, loc, }); } else { @@ -2842,6 +2849,7 @@ function lowerOptionalCallExpression( args, loc, }, + effects: null, loc, }); } @@ -3430,10 +3438,12 @@ function lowerFunction( * This isn't a problem in practice because use Babel's scope analysis to * identify the correct references. */ - const lowering = lower(expr, builder.environment, builder.bindings, [ - ...builder.context, - ...capturedContext, - ]); + const lowering = lower( + expr, + builder.environment, + builder.bindings, + new Map([...builder.context, ...capturedContext]), + ); let loweredFunc: HIRFunction; if (lowering.isErr()) { lowering @@ -3465,9 +3475,10 @@ export function lowerValueToTemporary( const place: Place = buildTemporaryPlace(builder, value.loc); builder.push({ id: makeInstructionId(0), + lvalue: {...place}, value: value, + effects: null, loc: value.loc, - lvalue: {...place}, }); return place; } @@ -4150,6 +4161,11 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set { return scopes; } +/** + * Returns a mapping of "context" identifiers — references to free variables that + * will become part of the function expression's `context` array — along with the + * source location of their first reference within the function. + */ function gatherCapturedContext( fn: NodePath< | t.FunctionExpression @@ -4158,8 +4174,8 @@ function gatherCapturedContext( | t.ObjectMethod >, componentScope: Scope, -): Array { - const capturedIds = new Set(); +): Map { + const capturedIds = new Map(); /* * Capture all the scopes from the parent of this function up to and including @@ -4202,8 +4218,15 @@ function gatherCapturedContext( // Add the base identifier binding as a dependency. const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name); - if (binding !== undefined && pureScopes.has(binding.scope)) { - capturedIds.add(binding.identifier); + if ( + binding !== undefined && + pureScopes.has(binding.scope) && + !capturedIds.has(binding.identifier) + ) { + capturedIds.set( + binding.identifier, + path.node.loc ?? binding.identifier.loc ?? GeneratedSource, + ); } } @@ -4240,7 +4263,7 @@ function gatherCapturedContext( }, }); - return [...capturedIds.keys()]; + return capturedIds; } function notNull(value: T | null): value is T { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 27b578b3c7834..90a352620ce35 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -243,6 +243,11 @@ export const EnvironmentConfigSchema = z.object({ */ enableUseTypeAnnotations: z.boolean().default(false), + /** + * Enable a new model for mutability and aliasing inference + */ + enableNewMutationAliasingModel: z.boolean().default(true), + /** * Enables inference of optional dependency chains. Without this flag * a property chain such as `props?.items?.foo` will infer as a dep on diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index b8504494662d6..13f87528b2185 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -17,6 +17,7 @@ import { BuiltInSetId, BuiltInUseActionStateId, BuiltInUseContextHookId, + BuiltInUseEffectEventId, BuiltInUseEffectHookId, BuiltInUseInsertionEffectHookId, BuiltInUseLayoutEffectHookId, @@ -27,6 +28,7 @@ import { BuiltInUseTransitionId, BuiltInWeakMapId, BuiltInWeakSetId, + BuiltinEffectEventId, ReanimatedSharedValueId, ShapeRegistry, addFunction, @@ -642,6 +644,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ calleeEffect: Effect.Read, hookKind: 'useEffect', returnValueKind: ValueKind.Frozen, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: ['@effect'], + effects: [ + // Freezes the function and deps + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.Effect, + }, + // Internally creates an effect object that captures the function and deps + { + kind: 'Create', + into: '@effect', + value: ValueKind.Frozen, + reason: ValueReason.KnownReturnSignature, + }, + // The effect stores the function and dependencies + { + kind: 'Capture', + from: '@rest', + into: '@effect', + }, + // Returns undefined + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], + }, }, BuiltInUseEffectHookId, ), @@ -722,6 +759,27 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ BuiltInFireId, ), ], + [ + 'useEffectEvent', + addHook( + DEFAULT_SHAPES, + { + positionalParams: [], + restParam: Effect.Freeze, + returnType: { + kind: 'Function', + return: {kind: 'Poly'}, + shapeId: BuiltinEffectEventId, + isConstructor: false, + }, + calleeEffect: Effect.Read, + hookKind: 'useEffectEvent', + // Frozen because it should not mutate any locally-bound values + returnValueKind: ValueKind.Frozen, + }, + BuiltInUseEffectEventId, + ), + ], ]; TYPED_GLOBALS.push( @@ -847,6 +905,7 @@ export function installTypeConfig( noAlias: typeConfig.noAlias === true, mutableOnlyIfOperandsAreMutable: typeConfig.mutableOnlyIfOperandsAreMutable === true, + aliasing: typeConfig.aliasing, }); } case 'hook': { @@ -864,6 +923,7 @@ export function installTypeConfig( ), returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen, noAlias: typeConfig.noAlias === true, + aliasing: typeConfig.aliasing, }); } case 'object': { 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 1699a0fc3d292..deb725a0482e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -13,6 +13,7 @@ import {Environment, ReactFunctionType} from './Environment'; import type {HookKind} from './ObjectShape'; import {Type, makeType} from './Types'; import {z} from 'zod'; +import type {AliasingEffect} from '../Inference/AliasingEffects'; /* * ******************************************************************************************* @@ -100,6 +101,7 @@ export type ReactiveInstruction = { id: InstructionId; lvalue: Place | null; value: ReactiveValue; + effects?: Array | null; // TODO make non-optional loc: SourceLocation; }; @@ -277,13 +279,14 @@ export type HIRFunction = { env: Environment; params: Array; returnTypeAnnotation: t.FlowType | t.TSType | null; - returnType: Type; + returns: Place; context: Array; effects: Array | null; body: HIR; generator: boolean; async: boolean; directives: Array; + aliasingEffects?: Array | null; }; export type FunctionEffect = @@ -449,6 +452,7 @@ export type ReturnTerminal = { value: Place; id: InstructionId; fallthrough?: never; + effects: Array | null; }; export type GotoTerminal = { @@ -609,6 +613,7 @@ export type MaybeThrowTerminal = { id: InstructionId; loc: SourceLocation; fallthrough?: never; + effects: Array | null; }; export type ReactiveScopeTerminal = { @@ -645,12 +650,14 @@ export type Instruction = { lvalue: Place; value: InstructionValue; loc: SourceLocation; + effects: Array | null; }; export type TInstruction = { id: InstructionId; lvalue: Place; value: T; + effects: Array | null; loc: SourceLocation; }; @@ -1380,6 +1387,21 @@ export enum ValueReason { */ JsxCaptured = 'jsx-captured', + /** + * Argument to a hook + */ + HookCaptured = 'hook-captured', + + /** + * Return value of a hook + */ + HookReturn = 'hook-return', + + /** + * Passed to an effect + */ + Effect = 'effect', + /** * Return value of a function with known frozen return value, e.g. `useState`. */ @@ -1430,6 +1452,20 @@ export const ValueKindSchema = z.enum([ ValueKind.Context, ]); +export const ValueReasonSchema = z.enum([ + ValueReason.Context, + ValueReason.Effect, + ValueReason.Global, + ValueReason.HookCaptured, + ValueReason.HookReturn, + ValueReason.JsxCaptured, + ValueReason.KnownReturnSignature, + ValueReason.Other, + ValueReason.ReactiveFunctionArgument, + ValueReason.ReducerState, + ValueReason.State, +]); + // The effect with which a value is modified. export enum Effect { // Default value: not allowed after lifetime inference @@ -1733,6 +1769,10 @@ export function isUseStateType(id: Identifier): boolean { return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState'; } +export function isJsxType(type: Type): boolean { + return type.kind === 'Object' && type.shapeId === 'BuiltInJsx'; +} + export function isRefOrRefValue(id: Identifier): boolean { return isUseRefType(id) || isRefValueType(id); } @@ -1785,6 +1825,13 @@ export function isFireFunctionType(id: Identifier): boolean { ); } +export function isEffectEventFunctionType(id: Identifier): boolean { + return ( + id.type.kind === 'Function' && + id.type.shapeId === 'BuiltInEffectEventFunction' + ); +} + export function isStableType(id: Identifier): boolean { return ( isSetStateType(id) || diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts index 9ed37bb2fc85f..c3a6c18d3aaff 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts @@ -106,7 +106,7 @@ export default class HIRBuilder { #current: WipBlock; #entry: BlockId; #scopes: Array = []; - #context: Array; + #context: Map; #bindings: Bindings; #env: Environment; #exceptionHandlerStack: Array = []; @@ -121,7 +121,7 @@ export default class HIRBuilder { return this.#env.nextIdentifierId; } - get context(): Array { + get context(): Map { return this.#context; } @@ -137,13 +137,13 @@ export default class HIRBuilder { env: Environment, options?: { bindings?: Bindings | null; - context?: Array; + context?: Map; entryBlockKind?: BlockKind; }, ) { this.#env = env; this.#bindings = options?.bindings ?? new Map(); - this.#context = options?.context ?? []; + this.#context = options?.context ?? new Map(); this.#entry = makeBlockId(env.nextBlockId); this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block'); } @@ -165,6 +165,7 @@ export default class HIRBuilder { handler: exceptionHandler, id: makeInstructionId(0), loc: instruction.loc, + effects: null, }, continuationBlock, ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts index ea132b772aa44..881e4e93ffcdb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts @@ -12,6 +12,7 @@ import { GeneratedSource, HIRFunction, Instruction, + Place, } from './HIR'; import {markPredecessors} from './HIRBuilder'; import {terminalFallthrough, terminalHasFallthrough} from './visitors'; @@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void { suggestions: null, }); const operand = Array.from(phi.operands.values())[0]!; + const lvalue: Place = { + kind: 'Identifier', + identifier: phi.place.identifier, + effect: Effect.ConditionallyMutate, + reactive: false, + loc: GeneratedSource, + }; const instr: Instruction = { id: predecessor.terminal.id, - lvalue: { - kind: 'Identifier', - identifier: phi.place.identifier, - effect: Effect.ConditionallyMutate, - reactive: false, - loc: GeneratedSource, - }, + lvalue: {...lvalue}, value: { kind: 'LoadLocal', place: {...operand}, loc: GeneratedSource, }, + effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}], loc: GeneratedSource, }; predecessor.instructions.push(instr); @@ -104,6 +107,17 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void { merged.merge(block.id, predecessorId); fn.body.blocks.delete(block.id); } + for (const [, block] of fn.body.blocks) { + for (const phi of block.phis) { + for (const [predecessorId, operand] of phi.operands) { + const mapped = merged.get(predecessorId); + if (mapped !== predecessorId) { + phi.operands.delete(predecessorId); + phi.operands.set(mapped, operand); + } + } + } + } markPredecessors(fn.body); for (const [, {terminal}] of fn.body.blocks) { if (terminalHasFallthrough(terminal)) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index 03f4120149b0e..4b67dc647f72c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -6,14 +6,30 @@ */ import {CompilerError} from '../CompilerError'; -import {Effect, ValueKind, ValueReason} from './HIR'; +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; +import {assertExhaustive} from '../Utils/utils'; +import { + Effect, + GeneratedSource, + Hole, + makeDeclarationId, + makeIdentifierId, + makeInstructionId, + Place, + SourceLocation, + SpreadPattern, + ValueKind, + ValueReason, +} from './HIR'; import { BuiltInType, FunctionType, + makeType, ObjectType, PolyType, PrimitiveType, } from './Types'; +import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema'; /* * This file exports types and defaults for JavaScript object shapes. These are @@ -42,13 +58,20 @@ function createAnonId(): string { export function addFunction( registry: ShapeRegistry, properties: Iterable<[string, BuiltInType | PolyType]>, - fn: Omit, + fn: Omit & { + aliasing?: AliasingSignatureConfig | null | undefined; + }, id: string | null = null, isConstructor: boolean = false, ): FunctionType { const shapeId = id ?? createAnonId(); + const aliasing = + fn.aliasing != null + ? parseAliasingSignatureConfig(fn.aliasing, '', GeneratedSource) + : null; addShape(registry, shapeId, properties, { ...fn, + aliasing, hookKind: null, }); return { @@ -66,11 +89,18 @@ export function addFunction( */ export function addHook( registry: ShapeRegistry, - fn: FunctionSignature & {hookKind: HookKind}, + fn: Omit & { + hookKind: HookKind; + aliasing?: AliasingSignatureConfig | null | undefined; + }, id: string | null = null, ): FunctionType { const shapeId = id ?? createAnonId(); - addShape(registry, shapeId, [], fn); + const aliasing = + fn.aliasing != null + ? parseAliasingSignatureConfig(fn.aliasing, '', GeneratedSource) + : null; + addShape(registry, shapeId, [], {...fn, aliasing}); return { kind: 'Function', return: fn.returnType, @@ -79,6 +109,129 @@ export function addHook( }; } +function parseAliasingSignatureConfig( + typeConfig: AliasingSignatureConfig, + moduleName: string, + loc: SourceLocation, +): AliasingSignature { + const lifetimes = new Map(); + function define(temp: string): Place { + CompilerError.invariant(!lifetimes.has(temp), { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`, + loc, + }); + const place = signatureArgument(lifetimes.size); + lifetimes.set(temp, place); + return place; + } + function lookup(temp: string): Place { + const place = lifetimes.get(temp); + CompilerError.invariant(place != null, { + reason: `Invalid type configuration for module`, + description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`, + loc, + }); + return place; + } + const receiver = define(typeConfig.receiver); + const params = typeConfig.params.map(define); + const rest = typeConfig.rest != null ? define(typeConfig.rest) : null; + const returns = define(typeConfig.returns); + const temporaries = typeConfig.temporaries.map(define); + const effects = typeConfig.effects.map( + (effect: AliasingEffectConfig): AliasingEffect => { + switch (effect.kind) { + case 'CreateFrom': + case 'Capture': + case 'Alias': + case 'Assign': { + const from = lookup(effect.from); + const into = lookup(effect.into); + return { + kind: effect.kind, + from, + into, + }; + } + case 'Mutate': + case 'MutateTransitiveConditionally': { + const value = lookup(effect.value); + return {kind: effect.kind, value}; + } + case 'Create': { + const into = lookup(effect.into); + return { + kind: 'Create', + into, + reason: effect.reason, + value: effect.value, + }; + } + case 'Freeze': { + const value = lookup(effect.value); + return { + kind: 'Freeze', + value, + reason: effect.reason, + }; + } + case 'Impure': { + const place = lookup(effect.place); + return { + kind: 'Impure', + place, + error: CompilerError.throwTodo({ + reason: 'Support impure effect declarations', + loc: GeneratedSource, + }), + }; + } + case 'Apply': { + const receiver = lookup(effect.receiver); + const fn = lookup(effect.function); + const args: Array = effect.args.map( + arg => { + if (typeof arg === 'string') { + return lookup(arg); + } else if (arg.kind === 'Spread') { + return {kind: 'Spread', place: lookup(arg.place)}; + } else { + return arg; + } + }, + ); + const into = lookup(effect.into); + return { + kind: 'Apply', + receiver, + function: fn, + mutatesFunction: effect.mutatesFunction, + args, + into, + loc, + signature: null, + }; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); + } + } + }, + ); + return { + receiver: receiver.identifier.id, + params: params.map(p => p.identifier.id), + rest: rest != null ? rest.identifier.id : null, + returns: returns.identifier.id, + temporaries, + effects, + }; +} + /* * Add an object to an existing ShapeRegistry. * @@ -131,6 +284,7 @@ export type HookKind = | 'useCallback' | 'useTransition' | 'useImperativeHandle' + | 'useEffectEvent' | 'Custom'; /* @@ -179,6 +333,8 @@ export type FunctionSignature = { impure?: boolean; canonicalName?: string; + + aliasing?: AliasingSignature | null | undefined; }; /* @@ -226,6 +382,8 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition'; export const BuiltInStartTransitionId = 'BuiltInStartTransition'; export const BuiltInFireId = 'BuiltInFire'; export const BuiltInFireFunctionId = 'BuiltInFireFunction'; +export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent'; +export const BuiltinEffectEventId = 'BuiltInEffectEventFunction'; // See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types export const ReanimatedSharedValueId = 'ReanimatedSharedValueId'; @@ -302,6 +460,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [ returnType: PRIMITIVE_TYPE, calleeEffect: Effect.Store, returnValueKind: ValueKind.Primitive, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Push directly mutates the array itself + {kind: 'Mutate', value: '@receiver'}, + // The arguments are captured into the array + { + kind: 'Capture', + from: '@rest', + into: '@receiver', + }, + // Returns the new length, a primitive + { + kind: 'Create', + into: '@returns', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + ], + }, }), ], [ @@ -332,6 +514,60 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [ returnValueKind: ValueKind.Mutable, noAlias: true, mutableOnlyIfOperandsAreMutable: true, + aliasing: { + receiver: '@receiver', + params: ['@callback'], + rest: null, + returns: '@returns', + temporaries: [ + // Temporary representing captured items of the receiver + '@item', + // Temporary representing the result of the callback + '@callbackReturn', + /* + * Undefined `this` arg to the callback. Note the signature does not + * support passing an explicit thisArg second param + */ + '@thisArg', + ], + effects: [ + // Map creates a new mutable array + { + kind: 'Create', + into: '@returns', + value: ValueKind.Mutable, + reason: ValueReason.KnownReturnSignature, + }, + // The first arg to the callback is an item extracted from the receiver array + { + kind: 'CreateFrom', + from: '@receiver', + into: '@item', + }, + // The undefined this for the callback + { + kind: 'Create', + into: '@thisArg', + value: ValueKind.Primitive, + reason: ValueReason.KnownReturnSignature, + }, + // calls the callback, returning the result into a temporary + { + kind: 'Apply', + receiver: '@thisArg', + args: ['@item', {kind: 'Hole'}, '@receiver'], + function: '@callback', + into: '@callbackReturn', + mutatesFunction: false, + }, + // captures the result of the callback into the return array + { + kind: 'Capture', + from: '@callbackReturn', + into: '@returns', + }, + ], + }, }), ], [ @@ -479,6 +715,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [ calleeEffect: Effect.Store, // returnValueKind is technically dependent on the ValueKind of the set itself returnValueKind: ValueKind.Mutable, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Set.add returns the receiver Set + { + kind: 'Assign', + from: '@receiver', + into: '@returns', + }, + // Set.add mutates the set itself + { + kind: 'Mutate', + value: '@receiver', + }, + // Captures the rest params into the set + { + kind: 'Capture', + from: '@rest', + into: '@receiver', + }, + ], + }, }), ], [ @@ -948,6 +1210,19 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [ ['*', {kind: 'Object', shapeId: BuiltInRefValueId}], ]); +addFunction( + BUILTIN_SHAPES, + [], + { + positionalParams: [], + restParam: Effect.ConditionallyMutate, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.ConditionallyMutate, + returnValueKind: ValueKind.Mutable, + }, + BuiltinEffectEventId, +); + /** * MixedReadOnly = * | primitive @@ -1166,6 +1441,53 @@ export const DefaultNonmutatingHook = addHook( calleeEffect: Effect.Read, hookKind: 'Custom', returnValueKind: ValueKind.Frozen, + aliasing: { + receiver: '@receiver', + params: [], + rest: '@rest', + returns: '@returns', + temporaries: [], + effects: [ + // Freeze the arguments + { + kind: 'Freeze', + value: '@rest', + reason: ValueReason.HookCaptured, + }, + // Returns a frozen value + { + kind: 'Create', + into: '@returns', + value: ValueKind.Frozen, + reason: ValueReason.HookReturn, + }, + // May alias any arguments into the return + { + kind: 'Alias', + from: '@rest', + into: '@returns', + }, + ], + }, }, 'DefaultNonmutatingHook', ); + +export function signatureArgument(id: number): Place { + const place: Place = { + kind: 'Identifier', + effect: Effect.Unknown, + loc: GeneratedSource, + reactive: false, + identifier: { + declarationId: makeDeclarationId(id), + id: makeIdentifierId(id), + loc: GeneratedSource, + mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)}, + name: null, + scope: null, + type: makeType(), + }, + }; + return place; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts index c8182c9e72a7c..caaced124e15d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -35,6 +35,7 @@ import type { Type, } from './HIR'; import {GotoVariant, InstructionKind} from './HIR'; +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects'; export type Options = { indent: number; @@ -53,6 +54,8 @@ export function printFunction(fn: HIRFunction): string { let definition = ''; if (fn.id !== null) { definition += fn.id; + } else { + definition += '<>'; } if (fn.params.length !== 0) { definition += @@ -67,13 +70,13 @@ export function printFunction(fn: HIRFunction): string { }) .join(', ') + ')'; + } else { + definition += '()'; } - if (definition.length !== 0) { - output.push(definition); - } - output.push(printType(fn.returnType)); - output.push(printHIR(fn.body)); + definition += `: ${printPlace(fn.returns)}`; + output.push(definition); output.push(...fn.directives); + output.push(printHIR(fn.body)); return output.join('\n'); } @@ -151,7 +154,10 @@ export function printMixedHIR( export function printInstruction(instr: ReactiveInstruction): string { const id = `[${instr.id}]`; - const value = printInstructionValue(instr.value); + let value = printInstructionValue(instr.value); + if (instr.effects != null) { + value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`; + } if (instr.lvalue !== null) { return `${id} ${printPlace(instr.lvalue)} = ${value}`; @@ -213,6 +219,9 @@ export function printTerminal(terminal: Terminal): Array | string { value = `[${terminal.id}] Return${ terminal.value != null ? ' ' + printPlace(terminal.value) : '' }`; + if (terminal.effects != null) { + value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`; + } break; } case 'goto': { @@ -281,6 +290,9 @@ export function printTerminal(terminal: Terminal): Array | string { } case 'maybe-throw': { value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`; + if (terminal.effects != null) { + value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`; + } break; } case 'scope': { @@ -555,8 +567,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string { } }) .join(', ') ?? ''; - const type = printType(instrValue.loweredFunc.func.returnType).trim(); - value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`; + const aliasingEffects = + instrValue.loweredFunc.func.aliasingEffects + ?.map(printAliasingEffect) + ?.join(', ') ?? ''; + value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`; break; } case 'TaggedTemplateExpression': { @@ -700,7 +715,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string { break; } case 'FinishMemoize': { - value = `FinishMemoize decl=${printPlace(instrValue.decl)}`; + value = `FinishMemoize decl=${printPlace(instrValue.decl)}${instrValue.pruned ? ' pruned' : ''}`; break; } default: { @@ -922,3 +937,107 @@ function getFunctionName( return defaultValue; } } + +export function printAliasingEffect(effect: AliasingEffect): string { + switch (effect.kind) { + case 'Assign': { + return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`; + } + case 'Alias': { + return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`; + } + case 'Capture': { + return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`; + } + case 'ImmutableCapture': { + return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`; + } + case 'Create': { + return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`; + } + case 'CreateFrom': { + return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`; + } + case 'CreateFunction': { + return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`; + } + case 'Apply': { + const receiverCallee = + effect.receiver.identifier.id === effect.function.identifier.id + ? printPlaceForAliasEffect(effect.receiver) + : `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`; + const args = effect.args + .map(arg => { + if (arg.kind === 'Identifier') { + return printPlaceForAliasEffect(arg); + } else if (arg.kind === 'Hole') { + return ' '; + } + return `...${printPlaceForAliasEffect(arg.place)}`; + }) + .join(', '); + let signature = ''; + if (effect.signature != null) { + if (effect.signature.aliasing != null) { + signature = printAliasingSignature(effect.signature.aliasing); + } else { + signature = JSON.stringify(effect.signature, null, 2); + } + } + return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`; + } + case 'Freeze': { + return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`; + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`; + } + case 'MutateFrozen': { + return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + } + case 'MutateGlobal': { + return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + } + case 'Impure': { + return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`; + } + case 'Render': { + return `Render ${printPlaceForAliasEffect(effect.place)}`; + } + default: { + assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`); + } + } +} + +function printPlaceForAliasEffect(place: Place): string { + return printIdentifier(place.identifier); +} + +export function printAliasingSignature(signature: AliasingSignature): string { + const tokens: Array = ['function ']; + if (signature.temporaries.length !== 0) { + tokens.push('<'); + tokens.push( + signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '), + ); + tokens.push('>'); + } + tokens.push('('); + tokens.push('this=$' + String(signature.receiver)); + for (const param of signature.params) { + tokens.push(', $' + String(param)); + } + if (signature.rest != null) { + tokens.push(`, ...$${String(signature.rest)}`); + } + tokens.push('): '); + tokens.push('$' + String(signature.returns) + ':'); + for (const effect of signature.effects) { + tokens.push('\n ' + printAliasingEffect(effect)); + } + return tokens.join(''); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts index 5d30aeb6444ee..6e9ff08b86242 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts @@ -88,6 +88,7 @@ function writeNonOptionalDependency( }, id: makeInstructionId(1), loc: loc, + effects: null, }); /** @@ -118,6 +119,7 @@ function writeNonOptionalDependency( }, id: makeInstructionId(1), loc: loc, + effects: null, }); curr = next; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts index 9aac2a264f60a..5945e3a07822f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/TypeSchema.ts @@ -8,7 +8,12 @@ import {isValidIdentifier} from '@babel/types'; import {z} from 'zod'; import {Effect, ValueKind} from '..'; -import {EffectSchema, ValueKindSchema} from './HIR'; +import { + EffectSchema, + ValueKindSchema, + ValueReason, + ValueReasonSchema, +} from './HIR'; export type ObjectPropertiesConfig = {[key: string]: TypeConfig}; export const ObjectPropertiesSchema: z.ZodType = z @@ -31,6 +36,194 @@ export const ObjectTypeSchema: z.ZodType = z.object({ properties: ObjectPropertiesSchema.nullable(), }); +export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), { + message: "Placeholder names must start with '@'", +}); + +export type FreezeEffectConfig = { + kind: 'Freeze'; + value: string; + reason: ValueReason; +}; + +export const FreezeEffectSchema: z.ZodType = z.object({ + kind: z.literal('Freeze'), + value: LifetimeIdSchema, + reason: ValueReasonSchema, +}); + +export type MutateEffectConfig = { + kind: 'Mutate'; + value: string; +}; + +export const MutateEffectSchema: z.ZodType = z.object({ + kind: z.literal('Mutate'), + value: LifetimeIdSchema, +}); + +export type MutateTransitiveConditionallyConfig = { + kind: 'MutateTransitiveConditionally'; + value: string; +}; + +export const MutateTransitiveConditionallySchema: z.ZodType = + z.object({ + kind: z.literal('MutateTransitiveConditionally'), + value: LifetimeIdSchema, + }); + +export type CreateEffectConfig = { + kind: 'Create'; + into: string; + value: ValueKind; + reason: ValueReason; +}; + +export const CreateEffectSchema: z.ZodType = z.object({ + kind: z.literal('Create'), + into: LifetimeIdSchema, + value: ValueKindSchema, + reason: ValueReasonSchema, +}); + +export type AssignEffectConfig = { + kind: 'Assign'; + from: string; + into: string; +}; + +export const AssignEffectSchema: z.ZodType = z.object({ + kind: z.literal('Assign'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type AliasEffectConfig = { + kind: 'Alias'; + from: string; + into: string; +}; + +export const AliasEffectSchema: z.ZodType = z.object({ + kind: z.literal('Alias'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type CaptureEffectConfig = { + kind: 'Capture'; + from: string; + into: string; +}; + +export const CaptureEffectSchema: z.ZodType = z.object({ + kind: z.literal('Capture'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, +}); + +export type CreateFromEffectConfig = { + kind: 'CreateFrom'; + from: string; + into: string; +}; + +export const CreateFromEffectSchema: z.ZodType = + z.object({ + kind: z.literal('CreateFrom'), + from: LifetimeIdSchema, + into: LifetimeIdSchema, + }); + +export type ApplyArgConfig = + | string + | {kind: 'Spread'; place: string} + | {kind: 'Hole'}; + +export const ApplyArgSchema: z.ZodType = z.union([ + LifetimeIdSchema, + z.object({ + kind: z.literal('Spread'), + place: LifetimeIdSchema, + }), + z.object({ + kind: z.literal('Hole'), + }), +]); + +export type ApplyEffectConfig = { + kind: 'Apply'; + receiver: string; + function: string; + mutatesFunction: boolean; + args: Array; + into: string; +}; + +export const ApplyEffectSchema: z.ZodType = z.object({ + kind: z.literal('Apply'), + receiver: LifetimeIdSchema, + function: LifetimeIdSchema, + mutatesFunction: z.boolean(), + args: z.array(ApplyArgSchema), + into: LifetimeIdSchema, +}); + +export type ImpureEffectConfig = { + kind: 'Impure'; + place: string; +}; + +export const ImpureEffectSchema: z.ZodType = z.object({ + kind: z.literal('Impure'), + place: LifetimeIdSchema, +}); + +export type AliasingEffectConfig = + | FreezeEffectConfig + | CreateEffectConfig + | CreateFromEffectConfig + | AssignEffectConfig + | AliasEffectConfig + | CaptureEffectConfig + | ImpureEffectConfig + | MutateEffectConfig + | MutateTransitiveConditionallyConfig + | ApplyEffectConfig; + +export const AliasingEffectSchema: z.ZodType = z.union([ + FreezeEffectSchema, + CreateEffectSchema, + CreateFromEffectSchema, + AssignEffectSchema, + AliasEffectSchema, + CaptureEffectSchema, + ImpureEffectSchema, + MutateEffectSchema, + MutateTransitiveConditionallySchema, + ApplyEffectSchema, +]); + +export type AliasingSignatureConfig = { + receiver: string; + params: Array; + rest: string | null; + returns: string; + effects: Array; + temporaries: Array; +}; + +export const AliasingSignatureSchema: z.ZodType = + z.object({ + receiver: LifetimeIdSchema, + params: z.array(LifetimeIdSchema), + rest: LifetimeIdSchema.nullable(), + returns: LifetimeIdSchema, + effects: z.array(AliasingEffectSchema), + temporaries: z.array(LifetimeIdSchema), + }); + export type FunctionTypeConfig = { kind: 'function'; positionalParams: Array; @@ -42,6 +235,7 @@ export type FunctionTypeConfig = { mutableOnlyIfOperandsAreMutable?: boolean | null | undefined; impure?: boolean | null | undefined; canonicalName?: string | null | undefined; + aliasing?: AliasingSignatureConfig | null | undefined; }; export const FunctionTypeSchema: z.ZodType = z.object({ kind: z.literal('function'), @@ -54,6 +248,7 @@ export const FunctionTypeSchema: z.ZodType = z.object({ mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(), impure: z.boolean().nullable().optional(), canonicalName: z.string().nullable().optional(), + aliasing: AliasingSignatureSchema.nullable().optional(), }); export type HookTypeConfig = { @@ -63,6 +258,7 @@ export type HookTypeConfig = { returnType: TypeConfig; returnValueKind?: ValueKind | null | undefined; noAlias?: boolean | null | undefined; + aliasing?: AliasingSignatureConfig | null | undefined; }; export const HookTypeSchema: z.ZodType = z.object({ kind: z.literal('hook'), @@ -71,6 +267,7 @@ export const HookTypeSchema: z.ZodType = z.object({ returnType: z.lazy(() => TypeSchema), returnValueKind: ValueKindSchema.nullable().optional(), noAlias: z.boolean().nullable().optional(), + aliasing: AliasingSignatureSchema.nullable().optional(), }); export type BuiltInTypeConfig = diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts index 49ff3c256e016..52bbefc732856 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -735,6 +735,7 @@ export function mapTerminalSuccessors( loc: terminal.loc, value: terminal.value, id: makeInstructionId(0), + effects: terminal.effects, }; } case 'throw': { @@ -842,6 +843,7 @@ export function mapTerminalSuccessors( handler, id: makeInstructionId(0), loc: terminal.loc, + effects: terminal.effects, }; } case 'try': { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts new file mode 100644 index 0000000000000..f844129e26bde --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts @@ -0,0 +1,244 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerErrorDetailOptions} from '../CompilerError'; +import { + FunctionExpression, + GeneratedSource, + Hole, + IdentifierId, + ObjectMethod, + Place, + SourceLocation, + SpreadPattern, + ValueKind, + ValueReason, +} from '../HIR'; +import {FunctionSignature} from '../HIR/ObjectShape'; +import {printSourceLocation} from '../HIR/PrintHIR'; + +/** + * `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or + * more values in a program. These effects include mutation of values, freezing values, + * tracking data flow between values, and other specialized cases. + */ +export type AliasingEffect = + /** + * Marks the given value and its direct aliases as frozen. + * + * Captured values are *not* considered frozen, because we cannot be sure that a previously + * captured value will still be captured at the point of the freeze. + * + * For example: + * const x = {}; + * const y = [x]; + * y.pop(); // y dosn't contain x anymore! + * freeze(y); + * mutate(x); // safe to mutate! + * + * The exception to this is FunctionExpressions - since it is impossible to change which + * value a function closes over[1] we can transitively freeze functions and their captures. + * + * [1] Except for `let` values that are reassigned and closed over by a function, but we + * handle this explicitly with StoreContext/LoadContext. + */ + | {kind: 'Freeze'; value: Place; reason: ValueReason} + /** + * Mutate the value and any direct aliases (not captures). Errors if the value is not mutable. + */ + | {kind: 'Mutate'; value: Place} + /** + * Mutate the value and any direct aliases (not captures), but only if the value is known mutable. + * This should be rare. + * + * TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more + * correct for iterators of unknown types. + */ + | {kind: 'MutateConditionally'; value: Place} + /** + * Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable. + */ + | {kind: 'MutateTransitive'; value: Place} + /** + * Mutates any of the value, its direct aliases, and its transitive captures that are mutable. + */ + | {kind: 'MutateTransitiveConditionally'; value: Place} + /** + * Records information flow from `from` to `into` in cases where local mutation of the destination + * will *not* mutate the source: + * + * - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a) + * - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a) + * + * Example: `array.push(item)`. Information from item is captured into array, but there is not a + * direct aliasing, and local mutations of array will not modify item. + */ + | {kind: 'Capture'; from: Place; into: Place} + /** + * Records information flow from `from` to `into` in cases where local mutation of the destination + * *will* mutate the source: + * + * - Alias a -> b and Mutate(b) => (does imply) Mutate(a) + * - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a) + * + * Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign. + * But we have to assume that it _could_ be returning its input, such that a local mutation of + * c could be mutating a. + */ + | {kind: 'Alias'; from: Place; into: Place} + /** + * Records direct assignment: `into = from`. + */ + | {kind: 'Assign'; from: Place; into: Place} + /** + * Creates a value of the given type at the given place + */ + | {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason} + /** + * Creates a new value with the same kind as the starting value. + */ + | {kind: 'CreateFrom'; from: Place; into: Place} + /** + * Immutable data flow, used for escape analysis. Does not influence mutable range analysis: + */ + | {kind: 'ImmutableCapture'; from: Place; into: Place} + /** + * Calls the function at the given place with the given arguments either captured or aliased, + * and captures/aliases the result into the given place. + */ + | { + kind: 'Apply'; + receiver: Place; + function: Place; + mutatesFunction: boolean; + args: Array; + into: Place; + signature: FunctionSignature | null; + loc: SourceLocation; + } + /** + * Constructs a function value with the given captures. The mutability of the function + * will be determined by the mutability of the capture values when evaluated. + */ + | { + kind: 'CreateFunction'; + captures: Array; + function: FunctionExpression | ObjectMethod; + into: Place; + } + /** + * Mutation of a value known to be immutable + */ + | {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions} + /** + * Mutation of a global + */ + | { + kind: 'MutateGlobal'; + place: Place; + error: CompilerErrorDetailOptions; + } + /** + * Indicates a side-effect that is not safe during render + */ + | {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions} + /** + * Indicates that a given place is accessed during render. Used to distingush + * hook arguments that are known to be called immediately vs those used for + * event handlers/effects, and for JSX values known to be called during render + * (tags, children) vs those that may be events/effect (other props). + */ + | { + kind: 'Render'; + place: Place; + }; + +export function hashEffect(effect: AliasingEffect): string { + switch (effect.kind) { + case 'Apply': { + return [ + effect.kind, + effect.receiver.identifier.id, + effect.function.identifier.id, + effect.mutatesFunction, + effect.args + .map(a => { + if (a.kind === 'Hole') { + return ''; + } else if (a.kind === 'Identifier') { + return a.identifier.id; + } else { + return `...${a.place.identifier.id}`; + } + }) + .join(','), + effect.into.identifier.id, + ].join(':'); + } + case 'CreateFrom': + case 'ImmutableCapture': + case 'Assign': + case 'Alias': + case 'Capture': { + return [ + effect.kind, + effect.from.identifier.id, + effect.into.identifier.id, + ].join(':'); + } + case 'Create': { + return [ + effect.kind, + effect.into.identifier.id, + effect.value, + effect.reason, + ].join(':'); + } + case 'Freeze': { + return [effect.kind, effect.value.identifier.id, effect.reason].join(':'); + } + case 'Impure': + case 'Render': { + return [effect.kind, effect.place.identifier.id].join(':'); + } + case 'MutateFrozen': + case 'MutateGlobal': { + return [ + effect.kind, + effect.place.identifier.id, + effect.error.severity, + effect.error.reason, + effect.error.description, + printSourceLocation(effect.error.loc ?? GeneratedSource), + ].join(':'); + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + return [effect.kind, effect.value.identifier.id].join(':'); + } + case 'CreateFunction': { + return [ + effect.kind, + effect.into.identifier.id, + // return places are a unique way to identify functions themselves + effect.function.loweredFunc.func.returns.identifier.id, + effect.captures.map(p => p.identifier.id).join(','), + ].join(':'); + } + } +} + +export type AliasingSignature = { + receiver: IdentifierId; + params: Array; + rest: IdentifierId | null; + returns: IdentifierId; + effects: Array; + temporaries: Array; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts index a439b4cd01232..7052cb2dd8a52 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -10,6 +10,7 @@ import { Effect, HIRFunction, Identifier, + IdentifierId, LoweredFunction, isRefOrRefValue, makeInstructionId, @@ -19,6 +20,9 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes'; import {rewriteInstructionKindsBasedOnReassignment} from '../SSA'; import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; +import {assertExhaustive} from '../Utils/utils'; +import {inferMutationAliasingEffects} from './InferMutationAliasingEffects'; +import {inferMutationAliasingRanges} from './InferMutationAliasingRanges'; export default function analyseFunctions(func: HIRFunction): void { for (const [_, block] of func.body.blocks) { @@ -26,15 +30,27 @@ export default function analyseFunctions(func: HIRFunction): void { switch (instr.value.kind) { case 'ObjectMethod': case 'FunctionExpression': { - lower(instr.value.loweredFunc.func); - infer(instr.value.loweredFunc); + if (!func.env.config.enableNewMutationAliasingModel) { + lower(instr.value.loweredFunc.func); + infer(instr.value.loweredFunc); + } else { + lowerWithMutationAliasing(instr.value.loweredFunc.func); + } /** * Reset mutable range for outer inferReferenceEffects */ for (const operand of instr.value.loweredFunc.func.context) { - operand.identifier.mutableRange.start = makeInstructionId(0); - operand.identifier.mutableRange.end = makeInstructionId(0); + /** + * NOTE: inferReactiveScopeVariables makes identifiers in the scope + * point to the *same* mutableRange instance. Resetting start/end + * here is insufficient, because a later mutation of the range + * for any one identifier could affect the range for other identifiers. + */ + operand.identifier.mutableRange = { + start: makeInstructionId(0), + end: makeInstructionId(0), + }; operand.identifier.scope = null; } break; @@ -44,6 +60,86 @@ export default function analyseFunctions(func: HIRFunction): void { } } +function lowerWithMutationAliasing(fn: HIRFunction): void { + /** + * Phase 1: similar to lower(), but using the new mutation/aliasing inference + */ + analyseFunctions(fn); + inferMutationAliasingEffects(fn, {isFunctionExpression: true}); + deadCodeElimination(fn); + const functionEffects = inferMutationAliasingRanges(fn, { + isFunctionExpression: true, + }).unwrap(); + rewriteInstructionKindsBasedOnReassignment(fn); + inferReactiveScopeVariables(fn); + fn.aliasingEffects = functionEffects; + + /** + * Phase 2: populate the Effect of each context variable to use in inferring + * the outer function. For example, InferMutationAliasingEffects uses context variable + * effects to decide if the function may be mutable or not. + */ + const capturedOrMutated = new Set(); + for (const effect of functionEffects) { + switch (effect.kind) { + case 'Assign': + case 'Alias': + case 'Capture': + case 'CreateFrom': { + capturedOrMutated.add(effect.from.identifier.id); + break; + } + case 'Apply': { + CompilerError.invariant(false, { + reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`, + loc: effect.function.loc, + }); + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + capturedOrMutated.add(effect.value.identifier.id); + break; + } + case 'Impure': + case 'Render': + case 'MutateFrozen': + case 'MutateGlobal': + case 'CreateFunction': + case 'Create': + case 'Freeze': + case 'ImmutableCapture': { + // no-op + break; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind ${(effect as any).kind}`, + ); + } + } + } + + for (const operand of fn.context) { + if ( + capturedOrMutated.has(operand.identifier.id) || + operand.effect === Effect.Capture + ) { + operand.effect = Effect.Capture; + } else { + operand.effect = Effect.Read; + } + } + + fn.env.logger?.debugLogIRs?.({ + kind: 'hir', + name: 'AnalyseFunction (inner)', + value: fn, + }); +} + function lower(func: HIRFunction): void { analyseFunctions(func); inferReferenceEffects(func, {isFunctionExpression: true}); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts index 8d123845c3739..306e636b12b3a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -197,6 +197,7 @@ function makeManualMemoizationMarkers( deps: depsList, loc: fnExpr.loc, }, + effects: null, loc: fnExpr.loc, }, { @@ -208,6 +209,7 @@ function makeManualMemoizationMarkers( decl: {...memoDecl}, loc: fnExpr.loc, }, + effects: null, loc: fnExpr.loc, }, ]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts index 4daa2f9fbaee7..4d4531e1cbe0c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts @@ -31,6 +31,7 @@ import { HIR, BasicBlock, BlockId, + isEffectEventFunctionType, } from '../HIR'; import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads'; import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies'; @@ -209,7 +210,8 @@ export function inferEffectDependencies(fn: HIRFunction): void { ((isUseRefType(maybeDep.identifier) || isSetStateType(maybeDep.identifier)) && !reactiveIds.has(maybeDep.identifier.id)) || - isFireFunctionType(maybeDep.identifier) + isFireFunctionType(maybeDep.identifier) || + isEffectEventFunctionType(maybeDep.identifier) ) { // exclude non-reactive hook results, which will never be in a memo block continue; @@ -255,6 +257,7 @@ export function inferEffectDependencies(fn: HIRFunction): void { loc: GeneratedSource, lvalue: {...depsPlace, effect: Effect.Mutate}, value: deps, + effects: null, }, }); value.args.push({...depsPlace, effect: Effect.Freeze}); @@ -269,6 +272,7 @@ export function inferEffectDependencies(fn: HIRFunction): void { loc: GeneratedSource, lvalue: {...depsPlace, effect: Effect.Mutate}, value: deps, + effects: null, }, }); value.args.push({...depsPlace, effect: Effect.Freeze}); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts index a58ae440219b9..9b347ebb6c4c9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts @@ -324,7 +324,7 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean { return effect.kind === 'GlobalMutation'; } -function getWriteErrorReason(abstractValue: AbstractValue): string { +export function getWriteErrorReason(abstractValue: AbstractValue): string { if (abstractValue.reason.has(ValueReason.Global)) { return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect'; } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) { @@ -339,6 +339,12 @@ function getWriteErrorReason(abstractValue: AbstractValue): string { return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead"; } else if (abstractValue.reason.has(ValueReason.ReducerState)) { return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead"; + } else if (abstractValue.reason.has(ValueReason.Effect)) { + return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()'; + } else if (abstractValue.reason.has(ValueReason.HookCaptured)) { + return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook'; + } else if (abstractValue.reason.has(ValueReason.HookReturn)) { + return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed'; } else { return 'This mutates a variable that React considers immutable'; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts index 624c302fbf7ee..571a19290ea60 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts @@ -86,7 +86,7 @@ export function inferMutableRanges(ir: HIRFunction): void { } } -function areEqualMaps(a: Map, b: Map): boolean { +function areEqualMaps(a: Map, b: Map): boolean { if (a.size !== b.size) { return false; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts new file mode 100644 index 0000000000000..b91b606d507e6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -0,0 +1,2515 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + CompilerError, + Effect, + ErrorSeverity, + SourceLocation, + ValueKind, +} from '..'; +import { + BasicBlock, + BlockId, + DeclarationId, + Environment, + FunctionExpression, + HIRFunction, + Hole, + IdentifierId, + Instruction, + InstructionKind, + InstructionValue, + isArrayType, + isMapType, + isPrimitiveType, + isRefOrRefValue, + isSetType, + makeIdentifierId, + Phi, + Place, + SpreadPattern, + ValueReason, +} from '../HIR'; +import { + eachInstructionValueLValue, + eachInstructionValueOperand, + eachTerminalOperand, + eachTerminalSuccessor, +} from '../HIR/visitors'; +import {Ok, Result} from '../Utils/Result'; +import { + getArgumentEffect, + getFunctionCallSignature, + isKnownMutableEffect, + mergeValueKinds, +} from './InferReferenceEffects'; +import { + assertExhaustive, + getOrInsertDefault, + getOrInsertWith, + Set_isSuperset, +} from '../Utils/utils'; +import { + printAliasingEffect, + printAliasingSignature, + printIdentifier, + printInstruction, + printInstructionValue, + printPlace, + printSourceLocation, +} from '../HIR/PrintHIR'; +import {FunctionSignature} from '../HIR/ObjectShape'; +import {getWriteErrorReason} from './InferFunctionEffects'; +import prettyFormat from 'pretty-format'; +import {createTemporaryPlace} from '../HIR/HIRBuilder'; +import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects'; + +const DEBUG = false; + +/** + * Infers the mutation/aliasing effects for instructions and terminals and annotates + * them on the HIR, making the effects of builtin instructions/functions as well as + * user-defined functions explicit. These effects then form the basis for subsequent + * analysis to determine the mutable range of each value in the program — the set of + * instructions over which the value is created and mutated — as well as validation + * against invalid code. + * + * At a high level the approach is: + * - Determine a set of candidate effects based purely on the syntax of the instruction + * and the types involved. These candidate effects are cached the first time each + * instruction is visited. The idea is to reason about the semantics of the instruction + * or function in isolation, separately from how those effects may interact with later + * abstract interpretation. + * - Then we do abstract interpretation over the HIR, iterating until reaching a fixpoint. + * This phase tracks the abstract kind of each value (mutable, primitive, frozen, etc) + * and the set of values pointed to by each identifier. Each candidate effect is "applied" + * to the current abtract state, and effects may be dropped or rewritten accordingly. + * For example, a "MutateConditionally " effect may be dropped if x is not a mutable + * value. A "Mutate " effect may get converted into a "MutateFrozen " effect + * if y is mutable, etc. + */ +export function inferMutationAliasingEffects( + fn: HIRFunction, + {isFunctionExpression}: {isFunctionExpression: boolean} = { + isFunctionExpression: false, + }, +): Result { + const initialState = InferenceState.empty(fn.env, isFunctionExpression); + + // Map of blocks to the last (merged) incoming state that was processed + const statesByBlock: Map = new Map(); + + for (const ref of fn.context) { + // TODO: using InstructionValue as a bit of a hack, but it's pragmatic + const value: InstructionValue = { + kind: 'ObjectExpression', + properties: [], + loc: ref.loc, + }; + initialState.initialize(value, { + kind: ValueKind.Context, + reason: new Set([ValueReason.Other]), + }); + initialState.define(ref, value); + } + + const paramKind: AbstractValue = isFunctionExpression + ? { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + } + : { + kind: ValueKind.Frozen, + reason: new Set([ValueReason.ReactiveFunctionArgument]), + }; + + if (fn.fnType === 'Component') { + CompilerError.invariant(fn.params.length <= 2, { + reason: + 'Expected React component to have not more than two parameters: one for props and for ref', + description: null, + loc: fn.loc, + suggestions: null, + }); + const [props, ref] = fn.params; + if (props != null) { + inferParam(props, initialState, paramKind); + } + if (ref != null) { + const place = ref.kind === 'Identifier' ? ref : ref.place; + const value: InstructionValue = { + kind: 'ObjectExpression', + properties: [], + loc: place.loc, + }; + initialState.initialize(value, { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + }); + initialState.define(place, value); + } + } else { + for (const param of fn.params) { + inferParam(param, initialState, paramKind); + } + } + + /* + * Multiple predecessors may be visited prior to reaching a given successor, + * so track the list of incoming state for each successor block. + * These are merged when reaching that block again. + */ + const queuedStates: Map = new Map(); + function queue(blockId: BlockId, state: InferenceState): void { + let queuedState = queuedStates.get(blockId); + if (queuedState != null) { + // merge the queued states for this block + state = queuedState.merge(state) ?? queuedState; + queuedStates.set(blockId, state); + } else { + /* + * this is the first queued state for this block, see whether + * there are changed relative to the last time it was processed. + */ + const prevState = statesByBlock.get(blockId); + const nextState = prevState != null ? prevState.merge(state) : state; + if (nextState != null) { + queuedStates.set(blockId, nextState); + } + } + } + queue(fn.body.entry, initialState); + + const hoistedContextDeclarations = findHoistedContextDeclarations(fn); + + const context = new Context( + isFunctionExpression, + fn, + hoistedContextDeclarations, + ); + + let iterationCount = 0; + while (queuedStates.size !== 0) { + iterationCount++; + if (iterationCount > 100) { + CompilerError.invariant(false, { + reason: `[InferMutationAliasingEffects] Potential infinite loop`, + description: `A value, temporary place, or effect was not cached properly`, + loc: fn.loc, + }); + } + for (const [blockId, block] of fn.body.blocks) { + const incomingState = queuedStates.get(blockId); + queuedStates.delete(blockId); + if (incomingState == null) { + continue; + } + + statesByBlock.set(blockId, incomingState); + const state = incomingState.clone(); + inferBlock(context, state, block); + + for (const nextBlockId of eachTerminalSuccessor(block.terminal)) { + queue(nextBlockId, state); + } + } + } + return Ok(undefined); +} + +function findHoistedContextDeclarations( + fn: HIRFunction, +): Map { + const hoisted = new Map(); + function visit(place: Place): void { + if ( + hoisted.has(place.identifier.declarationId) && + hoisted.get(place.identifier.declarationId) == null + ) { + // If this is the first load of the value, store the location + hoisted.set(place.identifier.declarationId, place); + } + } + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + if (instr.value.kind === 'DeclareContext') { + const kind = instr.value.lvalue.kind; + if ( + kind == InstructionKind.HoistedConst || + kind == InstructionKind.HoistedFunction || + kind == InstructionKind.HoistedLet + ) { + hoisted.set(instr.value.lvalue.place.identifier.declarationId, null); + } + } else { + for (const operand of eachInstructionValueOperand(instr.value)) { + visit(operand); + } + } + } + for (const operand of eachTerminalOperand(block.terminal)) { + visit(operand); + } + } + return hoisted; +} + +class Context { + internedEffects: Map = new Map(); + instructionSignatureCache: Map = new Map(); + effectInstructionValueCache: Map = + new Map(); + applySignatureCache: Map< + AliasingSignature, + Map | null> + > = new Map(); + catchHandlers: Map = new Map(); + functionSignatureCache: Map = + new Map(); + isFuctionExpression: boolean; + fn: HIRFunction; + hoistedContextDeclarations: Map; + + constructor( + isFunctionExpression: boolean, + fn: HIRFunction, + hoistedContextDeclarations: Map, + ) { + this.isFuctionExpression = isFunctionExpression; + this.fn = fn; + this.hoistedContextDeclarations = hoistedContextDeclarations; + } + + cacheApplySignature( + signature: AliasingSignature, + effect: Extract, + f: () => Array | null, + ): Array | null { + const inner = getOrInsertDefault( + this.applySignatureCache, + signature, + new Map(), + ); + return getOrInsertWith(inner, effect, f); + } + + internEffect(effect: AliasingEffect): AliasingEffect { + const hash = hashEffect(effect); + let interned = this.internedEffects.get(hash); + if (interned == null) { + this.internedEffects.set(hash, effect); + interned = effect; + } + return interned; + } +} + +function inferParam( + param: Place | SpreadPattern, + initialState: InferenceState, + paramKind: AbstractValue, +): void { + const place = param.kind === 'Identifier' ? param : param.place; + const value: InstructionValue = { + kind: 'Primitive', + loc: place.loc, + value: undefined, + }; + initialState.initialize(value, paramKind); + initialState.define(place, value); +} + +function inferBlock( + context: Context, + state: InferenceState, + block: BasicBlock, +): void { + for (const phi of block.phis) { + state.inferPhi(phi); + } + + for (const instr of block.instructions) { + let instructionSignature = context.instructionSignatureCache.get(instr); + if (instructionSignature == null) { + instructionSignature = computeSignatureForInstruction( + context, + state.env, + instr, + ); + context.instructionSignatureCache.set(instr, instructionSignature); + } + const effects = applySignature(context, state, instructionSignature, instr); + instr.effects = effects; + } + const terminal = block.terminal; + if (terminal.kind === 'try' && terminal.handlerBinding != null) { + context.catchHandlers.set(terminal.handler, terminal.handlerBinding); + } else if (terminal.kind === 'maybe-throw') { + const handlerParam = context.catchHandlers.get(terminal.handler); + if (handlerParam != null) { + CompilerError.invariant(state.kind(handlerParam) != null, { + reason: + 'Expected catch binding to be intialized with a DeclareLocal Catch instruction', + loc: terminal.loc, + }); + const effects: Array = []; + for (const instr of block.instructions) { + if ( + instr.value.kind === 'CallExpression' || + instr.value.kind === 'MethodCall' + ) { + /** + * Many instructions can error, but only calls can throw their result as the error + * itself. For example, `c = a.b` can throw if `a` is nullish, but the thrown value + * is an error object synthesized by the JS runtime. Whereas `throwsInput(x)` can + * throw (effectively) the result of the call. + * + * TODO: call applyEffect() instead. This meant that the catch param wasn't inferred + * as a mutable value, though. See `try-catch-try-value-modified-in-catch-escaping.js` + * fixture as an example + */ + state.appendAlias(handlerParam, instr.lvalue); + const kind = state.kind(instr.lvalue).kind; + if (kind === ValueKind.Mutable || kind == ValueKind.Context) { + effects.push( + context.internEffect({ + kind: 'Alias', + from: instr.lvalue, + into: handlerParam, + }), + ); + } + } + } + terminal.effects = effects.length !== 0 ? effects : null; + } + } else if (terminal.kind === 'return') { + if (!context.isFuctionExpression) { + terminal.effects = [ + context.internEffect({ + kind: 'Freeze', + value: terminal.value, + reason: ValueReason.JsxCaptured, + }), + ]; + } + } +} + +/** + * Applies the signature to the given state to determine the precise set of effects + * that will occur in practice. This takes into account the inferred state of each + * variable. For example, the signature may have a `ConditionallyMutate x` effect. + * Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable + * or no effect if x is a primitive, global, or frozen. + * + * This phase may also emit errors, for example MutateLocal on a frozen value is invalid. + */ +function applySignature( + context: Context, + state: InferenceState, + signature: InstructionSignature, + instruction: Instruction, +): Array | null { + const effects: Array = []; + /** + * For function instructions, eagerly validate that they aren't mutating + * a known-frozen value. + * + * TODO: make sure we're also validating against global mutations somewhere, but + * account for this being allowed in effects/event handlers. + */ + if ( + instruction.value.kind === 'FunctionExpression' || + instruction.value.kind === 'ObjectMethod' + ) { + const aliasingEffects = + instruction.value.loweredFunc.func.aliasingEffects ?? []; + const context = new Set( + instruction.value.loweredFunc.func.context.map(p => p.identifier.id), + ); + for (const effect of aliasingEffects) { + if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') { + if (!context.has(effect.value.identifier.id)) { + continue; + } + const value = state.kind(effect.value); + switch (value.kind) { + case ValueKind.Frozen: { + const reason = getWriteErrorReason({ + kind: value.kind, + reason: value.reason, + context: new Set(), + }); + effects.push({ + kind: 'MutateFrozen', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason, + description: + effect.value.identifier.name !== null && + effect.value.identifier.name.kind === 'named' + ? `Found mutation of \`${effect.value.identifier.name.value}\`` + : null, + loc: effect.value.loc, + suggestions: null, + }, + }); + } + } + } + } + } + + /* + * Track which values we've already aliased once, so that we can switch to + * appendAlias() for subsequent aliases into the same value + */ + const initialized = new Set(); + + if (DEBUG) { + console.log(printInstruction(instruction)); + } + + for (const effect of signature.effects) { + applyEffect(context, state, effect, initialized, effects); + } + if (DEBUG) { + console.log( + prettyFormat(state.debugAbstractValue(state.kind(instruction.lvalue))), + ); + console.log( + effects.map(effect => ` ${printAliasingEffect(effect)}`).join('\n'), + ); + } + if ( + !(state.isDefined(instruction.lvalue) && state.kind(instruction.lvalue)) + ) { + CompilerError.invariant(false, { + reason: `Expected instruction lvalue to be initialized`, + loc: instruction.loc, + }); + } + return effects.length !== 0 ? effects : null; +} + +function applyEffect( + context: Context, + state: InferenceState, + _effect: AliasingEffect, + initialized: Set, + effects: Array, +): void { + const effect = context.internEffect(_effect); + if (DEBUG) { + console.log(printAliasingEffect(effect)); + } + switch (effect.kind) { + case 'Freeze': { + const didFreeze = state.freeze(effect.value, effect.reason); + if (didFreeze) { + effects.push(effect); + } + break; + } + case 'Create': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'ObjectExpression', + properties: [], + loc: effect.into.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: effect.value, + reason: new Set([effect.reason]), + }); + state.define(effect.into, value); + effects.push(effect); + break; + } + case 'ImmutableCapture': { + const kind = state.kind(effect.from).kind; + switch (kind) { + case ValueKind.Global: + case ValueKind.Primitive: { + // no-op: we don't need to track data flow for copy types + break; + } + default: { + effects.push(effect); + } + } + break; + } + case 'CreateFrom': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + const fromValue = state.kind(effect.from); + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'ObjectExpression', + properties: [], + loc: effect.into.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: fromValue.kind, + reason: new Set(fromValue.reason), + }); + state.define(effect.into, value); + switch (fromValue.kind) { + case ValueKind.Primitive: + case ValueKind.Global: { + effects.push({ + kind: 'Create', + value: fromValue.kind, + into: effect.into, + reason: [...fromValue.reason][0] ?? ValueReason.Other, + }); + break; + } + case ValueKind.Frozen: { + effects.push({ + kind: 'Create', + value: fromValue.kind, + into: effect.into, + reason: [...fromValue.reason][0] ?? ValueReason.Other, + }); + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); + break; + } + default: { + effects.push(effect); + } + } + break; + } + case 'CreateFunction': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + effects.push(effect); + /** + * We consider the function mutable if it has any mutable context variables or + * any side-effects that need to be tracked if the function is called. + */ + const hasCaptures = effect.captures.some(capture => { + switch (state.kind(capture).kind) { + case ValueKind.Context: + case ValueKind.Mutable: { + return true; + } + default: { + return false; + } + } + }); + const hasTrackedSideEffects = + effect.function.loweredFunc.func.aliasingEffects?.some( + effect => + // TODO; include "render" here? + effect.kind === 'MutateFrozen' || + effect.kind === 'MutateGlobal' || + effect.kind === 'Impure', + ); + // For legacy compatibility + const capturesRef = effect.function.loweredFunc.func.context.some( + operand => isRefOrRefValue(operand.identifier), + ); + const isMutable = hasCaptures || hasTrackedSideEffects || capturesRef; + for (const operand of effect.function.loweredFunc.func.context) { + if (operand.effect !== Effect.Capture) { + continue; + } + const kind = state.kind(operand).kind; + if ( + kind === ValueKind.Primitive || + kind == ValueKind.Frozen || + kind == ValueKind.Global + ) { + operand.effect = Effect.Read; + } + } + state.initialize(effect.function, { + kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen, + reason: new Set([]), + }); + state.define(effect.into, effect.function); + for (const capture of effect.captures) { + applyEffect( + context, + state, + { + kind: 'Capture', + from: capture, + into: effect.into, + }, + initialized, + effects, + ); + } + break; + } + case 'Alias': + case 'Capture': { + CompilerError.invariant( + effect.kind === 'Capture' || initialized.has(effect.into.identifier.id), + { + reason: `Expected destination value to already be initialized within this instruction for Alias effect`, + description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`, + loc: effect.into.loc, + }, + ); + /* + * Capture describes potential information flow: storing a pointer to one value + * within another. If the destination is not mutable, or the source value has + * copy-on-write semantics, then we can prune the effect + */ + const intoKind = state.kind(effect.into).kind; + let isMutableDesination: boolean; + switch (intoKind) { + case ValueKind.Context: + case ValueKind.Mutable: + case ValueKind.MaybeFrozen: { + isMutableDesination = true; + break; + } + default: { + isMutableDesination = false; + break; + } + } + const fromKind = state.kind(effect.from).kind; + let isMutableReferenceType: boolean; + switch (fromKind) { + case ValueKind.Global: + case ValueKind.Primitive: { + isMutableReferenceType = false; + break; + } + case ValueKind.Frozen: { + isMutableReferenceType = false; + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); + break; + } + default: { + isMutableReferenceType = true; + break; + } + } + if (isMutableDesination && isMutableReferenceType) { + effects.push(effect); + } + break; + } + case 'Assign': { + CompilerError.invariant(!initialized.has(effect.into.identifier.id), { + reason: `Cannot re-initialize variable within an instruction`, + description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`, + loc: effect.into.loc, + }); + initialized.add(effect.into.identifier.id); + + /* + * Alias represents potential pointer aliasing. If the type is a global, + * a primitive (copy-on-write semantics) then we can prune the effect + */ + const fromValue = state.kind(effect.from); + const fromKind = fromValue.kind; + switch (fromKind) { + case ValueKind.Frozen: { + applyEffect( + context, + state, + { + kind: 'ImmutableCapture', + from: effect.from, + into: effect.into, + }, + initialized, + effects, + ); + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'Primitive', + value: undefined, + loc: effect.from.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: fromKind, + reason: new Set(fromValue.reason), + }); + state.define(effect.into, value); + break; + } + case ValueKind.Global: + case ValueKind.Primitive: { + let value = context.effectInstructionValueCache.get(effect); + if (value == null) { + value = { + kind: 'Primitive', + value: undefined, + loc: effect.from.loc, + }; + context.effectInstructionValueCache.set(effect, value); + } + state.initialize(value, { + kind: fromKind, + reason: new Set(fromValue.reason), + }); + state.define(effect.into, value); + break; + } + default: { + state.assign(effect.into, effect.from); + effects.push(effect); + break; + } + } + break; + } + case 'Apply': { + const functionValues = state.values(effect.function); + if ( + functionValues.length === 1 && + functionValues[0].kind === 'FunctionExpression' && + functionValues[0].loweredFunc.func.aliasingEffects != null + ) { + /* + * We're calling a locally declared function, we already know it's effects! + * We just have to substitute in the args for the params + */ + const functionExpr = functionValues[0]; + let signature = context.functionSignatureCache.get(functionExpr); + if (signature == null) { + signature = buildSignatureFromFunctionExpression( + state.env, + functionExpr, + ); + context.functionSignatureCache.set(functionExpr, signature); + } + if (DEBUG) { + console.log( + `constructed alias signature:\n${printAliasingSignature(signature)}`, + ); + } + const signatureEffects = context.cacheApplySignature( + signature, + effect, + () => + computeEffectsForSignature( + state.env, + signature, + effect.into, + effect.receiver, + effect.args, + functionExpr.loweredFunc.func.context, + effect.loc, + ), + ); + if (signatureEffects != null) { + applyEffect( + context, + state, + {kind: 'MutateTransitiveConditionally', value: effect.function}, + initialized, + effects, + ); + for (const signatureEffect of signatureEffects) { + applyEffect(context, state, signatureEffect, initialized, effects); + } + break; + } + } + let signatureEffects = null; + if (effect.signature?.aliasing != null) { + const signature = effect.signature.aliasing; + signatureEffects = context.cacheApplySignature( + effect.signature.aliasing, + effect, + () => + computeEffectsForSignature( + state.env, + signature, + effect.into, + effect.receiver, + effect.args, + [], + effect.loc, + ), + ); + } + if (signatureEffects != null) { + for (const signatureEffect of signatureEffects) { + applyEffect(context, state, signatureEffect, initialized, effects); + } + } else if (effect.signature != null) { + const legacyEffects = computeEffectsForLegacySignature( + state, + effect.signature, + effect.into, + effect.receiver, + effect.args, + effect.loc, + ); + for (const legacyEffect of legacyEffects) { + applyEffect(context, state, legacyEffect, initialized, effects); + } + } else { + applyEffect( + context, + state, + { + kind: 'Create', + into: effect.into, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }, + initialized, + effects, + ); + /* + * If no signature then by default: + * - All operands are conditionally mutated, except some instruction + * variants are assumed to not mutate the callee (such as `new`) + * - All operands are captured into (but not directly aliased as) + * every other argument. + */ + for (const arg of [effect.receiver, effect.function, ...effect.args]) { + if (arg.kind === 'Hole') { + continue; + } + const operand = arg.kind === 'Identifier' ? arg : arg.place; + if (operand !== effect.function || effect.mutatesFunction) { + applyEffect( + context, + state, + { + kind: 'MutateTransitiveConditionally', + value: operand, + }, + initialized, + effects, + ); + } + const mutateIterator = + arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null; + if (mutateIterator) { + applyEffect(context, state, mutateIterator, initialized, effects); + } + applyEffect( + context, + state, + // OK: recording information flow + {kind: 'Alias', from: operand, into: effect.into}, + initialized, + effects, + ); + for (const otherArg of [ + effect.receiver, + effect.function, + ...effect.args, + ]) { + if (otherArg.kind === 'Hole') { + continue; + } + const other = + otherArg.kind === 'Identifier' ? otherArg : otherArg.place; + if (other === arg) { + continue; + } + applyEffect( + context, + state, + { + /* + * OK: a function might store one operand into another, + * but it can't force one to alias another + */ + kind: 'Capture', + from: operand, + into: other, + }, + initialized, + effects, + ); + } + } + } + break; + } + case 'Mutate': + case 'MutateConditionally': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': { + const mutationKind = state.mutate(effect.kind, effect.value); + if (mutationKind === 'mutate') { + effects.push(effect); + } else if (mutationKind === 'mutate-ref') { + // no-op + } else if ( + mutationKind !== 'none' && + (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') + ) { + const value = state.kind(effect.value); + if (DEBUG) { + console.log(`invalid mutation: ${printAliasingEffect(effect)}`); + console.log(prettyFormat(state.debugAbstractValue(value))); + } + + if ( + mutationKind === 'mutate-frozen' && + context.hoistedContextDeclarations.has( + effect.value.identifier.declarationId, + ) + ) { + const description = + effect.value.identifier.name !== null && + effect.value.identifier.name.kind === 'named' + ? `Variable \`${effect.value.identifier.name.value}\` is accessed before it is declared` + : null; + const hoistedAccess = context.hoistedContextDeclarations.get( + effect.value.identifier.declarationId, + ); + if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) { + applyEffect( + context, + state, + { + kind: 'MutateFrozen', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason: `This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time`, + description, + loc: hoistedAccess.loc, + suggestions: null, + }, + }, + initialized, + effects, + ); + } + + applyEffect( + context, + state, + { + kind: 'MutateFrozen', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason: `This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`, + description, + loc: effect.value.loc, + suggestions: null, + }, + }, + initialized, + effects, + ); + } else { + const reason = getWriteErrorReason({ + kind: value.kind, + reason: value.reason, + context: new Set(), + }); + const description = + effect.value.identifier.name !== null && + effect.value.identifier.name.kind === 'named' + ? `Found mutation of \`${effect.value.identifier.name.value}\`` + : null; + applyEffect( + context, + state, + { + kind: + value.kind === ValueKind.Frozen + ? 'MutateFrozen' + : 'MutateGlobal', + place: effect.value, + error: { + severity: ErrorSeverity.InvalidReact, + reason, + description, + loc: effect.value.loc, + suggestions: null, + }, + }, + initialized, + effects, + ); + } + } + break; + } + case 'Impure': + case 'Render': + case 'MutateFrozen': + case 'MutateGlobal': { + effects.push(effect); + break; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind as any}'`, + ); + } + } +} + +class InferenceState { + env: Environment; + #isFunctionExpression: boolean; + + // The kind of each value, based on its allocation site + #values: Map; + /* + * The set of values pointed to by each identifier. This is a set + * to accomodate phi points (where a variable may have different + * values from different control flow paths). + */ + #variables: Map>; + + constructor( + env: Environment, + isFunctionExpression: boolean, + values: Map, + variables: Map>, + ) { + this.env = env; + this.#isFunctionExpression = isFunctionExpression; + this.#values = values; + this.#variables = variables; + } + + static empty( + env: Environment, + isFunctionExpression: boolean, + ): InferenceState { + return new InferenceState(env, isFunctionExpression, new Map(), new Map()); + } + + get isFunctionExpression(): boolean { + return this.#isFunctionExpression; + } + + // (Re)initializes a @param value with its default @param kind. + initialize(value: InstructionValue, kind: AbstractValue): void { + CompilerError.invariant(value.kind !== 'LoadLocal', { + reason: + '[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values', + description: null, + loc: value.loc, + suggestions: null, + }); + this.#values.set(value, kind); + } + + values(place: Place): Array { + const values = this.#variables.get(place.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`, + description: `${printPlace(place)}`, + loc: place.loc, + suggestions: null, + }); + return Array.from(values); + } + + // Lookup the kind of the given @param value. + kind(place: Place): AbstractValue { + const values = this.#variables.get(place.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`, + description: `${printPlace(place)}`, + loc: place.loc, + suggestions: null, + }); + let mergedKind: AbstractValue | null = null; + for (const value of values) { + const kind = this.#values.get(value)!; + mergedKind = + mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind; + } + CompilerError.invariant(mergedKind !== null, { + reason: `[InferMutationAliasingEffects] Expected at least one value`, + description: `No value found at \`${printPlace(place)}\``, + loc: place.loc, + suggestions: null, + }); + return mergedKind; + } + + // Updates the value at @param place to point to the same value as @param value. + assign(place: Place, value: Place): void { + const values = this.#variables.get(value.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`, + description: `${printIdentifier(value.identifier)}`, + loc: value.loc, + suggestions: null, + }); + this.#variables.set(place.identifier.id, new Set(values)); + } + + appendAlias(place: Place, value: Place): void { + const values = this.#variables.get(value.identifier.id); + CompilerError.invariant(values != null, { + reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`, + description: `${printIdentifier(value.identifier)}`, + loc: value.loc, + suggestions: null, + }); + const prevValues = this.values(place); + this.#variables.set( + place.identifier.id, + new Set([...prevValues, ...values]), + ); + } + + // Defines (initializing or updating) a variable with a specific kind of value. + define(place: Place, value: InstructionValue): void { + CompilerError.invariant(this.#values.has(value), { + reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation( + value.loc, + )}'`, + description: printInstructionValue(value), + loc: value.loc, + suggestions: null, + }); + this.#variables.set(place.identifier.id, new Set([value])); + } + + isDefined(place: Place): boolean { + return this.#variables.has(place.identifier.id); + } + + /** + * Marks @param place as transitively frozen. Returns true if the value was not + * already frozen, false if the value is already frozen (or already known immutable). + */ + freeze(place: Place, reason: ValueReason): boolean { + const value = this.kind(place); + switch (value.kind) { + case ValueKind.Context: + case ValueKind.Mutable: + case ValueKind.MaybeFrozen: { + const values = this.values(place); + for (const instrValue of values) { + this.freezeValue(instrValue, reason); + } + return true; + } + case ValueKind.Frozen: + case ValueKind.Global: + case ValueKind.Primitive: { + return false; + } + default: { + assertExhaustive( + value.kind, + `Unexpected value kind '${(value as any).kind}'`, + ); + } + } + } + + freezeValue(value: InstructionValue, reason: ValueReason): void { + this.#values.set(value, { + kind: ValueKind.Frozen, + reason: new Set([reason]), + }); + if ( + value.kind === 'FunctionExpression' && + (this.env.config.enablePreserveExistingMemoizationGuarantees || + this.env.config.enableTransitivelyFreezeFunctionExpressions) + ) { + for (const place of value.loweredFunc.func.context) { + this.freeze(place, reason); + } + } + } + + mutate( + variant: + | 'Mutate' + | 'MutateConditionally' + | 'MutateTransitive' + | 'MutateTransitiveConditionally', + place: Place, + ): 'none' | 'mutate' | 'mutate-frozen' | 'mutate-global' | 'mutate-ref' { + if (isRefOrRefValue(place.identifier)) { + return 'mutate-ref'; + } + const kind = this.kind(place).kind; + switch (variant) { + case 'MutateConditionally': + case 'MutateTransitiveConditionally': { + switch (kind) { + case ValueKind.Mutable: + case ValueKind.Context: { + return 'mutate'; + } + default: { + return 'none'; + } + } + } + case 'Mutate': + case 'MutateTransitive': { + switch (kind) { + case ValueKind.Mutable: + case ValueKind.Context: { + return 'mutate'; + } + case ValueKind.Primitive: { + // technically an error, but it's not React specific + return 'none'; + } + case ValueKind.Frozen: { + return 'mutate-frozen'; + } + case ValueKind.Global: { + return 'mutate-global'; + } + case ValueKind.MaybeFrozen: { + return 'none'; + } + default: { + assertExhaustive(kind, `Unexpected kind ${kind}`); + } + } + } + default: { + assertExhaustive(variant, `Unexpected mutation variant ${variant}`); + } + } + } + + /* + * Combine the contents of @param this and @param other, returning a new + * instance with the combined changes _if_ there are any changes, or + * returning null if no changes would occur. Changes include: + * - new entries in @param other that did not exist in @param this + * - entries whose values differ in @param this and @param other, + * and where joining the values produces a different value than + * what was in @param this. + * + * Note that values are joined using a lattice operation to ensure + * termination. + */ + merge(other: InferenceState): InferenceState | null { + let nextValues: Map | null = null; + let nextVariables: Map> | null = null; + + for (const [id, thisValue] of this.#values) { + const otherValue = other.#values.get(id); + if (otherValue !== undefined) { + const mergedValue = mergeAbstractValues(thisValue, otherValue); + if (mergedValue !== thisValue) { + nextValues = nextValues ?? new Map(this.#values); + nextValues.set(id, mergedValue); + } + } + } + for (const [id, otherValue] of other.#values) { + if (this.#values.has(id)) { + // merged above + continue; + } + nextValues = nextValues ?? new Map(this.#values); + nextValues.set(id, otherValue); + } + + for (const [id, thisValues] of this.#variables) { + const otherValues = other.#variables.get(id); + if (otherValues !== undefined) { + let mergedValues: Set | null = null; + for (const otherValue of otherValues) { + if (!thisValues.has(otherValue)) { + mergedValues = mergedValues ?? new Set(thisValues); + mergedValues.add(otherValue); + } + } + if (mergedValues !== null) { + nextVariables = nextVariables ?? new Map(this.#variables); + nextVariables.set(id, mergedValues); + } + } + } + for (const [id, otherValues] of other.#variables) { + if (this.#variables.has(id)) { + continue; + } + nextVariables = nextVariables ?? new Map(this.#variables); + nextVariables.set(id, new Set(otherValues)); + } + + if (nextVariables === null && nextValues === null) { + return null; + } else { + return new InferenceState( + this.env, + this.#isFunctionExpression, + nextValues ?? new Map(this.#values), + nextVariables ?? new Map(this.#variables), + ); + } + } + + /* + * Returns a copy of this state. + * TODO: consider using persistent data structures to make + * clone cheaper. + */ + clone(): InferenceState { + return new InferenceState( + this.env, + this.#isFunctionExpression, + new Map(this.#values), + new Map(this.#variables), + ); + } + + /* + * For debugging purposes, dumps the state to a plain + * object so that it can printed as JSON. + */ + debug(): any { + const result: any = {values: {}, variables: {}}; + const objects: Map = new Map(); + function identify(value: InstructionValue): number { + let id = objects.get(value); + if (id == null) { + id = objects.size; + objects.set(value, id); + } + return id; + } + for (const [value, kind] of this.#values) { + const id = identify(value); + result.values[id] = { + abstract: this.debugAbstractValue(kind), + value: printInstructionValue(value), + }; + } + for (const [variable, values] of this.#variables) { + result.variables[`$${variable}`] = [...values].map(identify); + } + return result; + } + + debugAbstractValue(value: AbstractValue): any { + return { + kind: value.kind, + reason: [...value.reason], + }; + } + + inferPhi(phi: Phi): void { + const values: Set = new Set(); + for (const [_, operand] of phi.operands) { + const operandValues = this.#variables.get(operand.identifier.id); + // This is a backedge that will be handled later by State.merge + if (operandValues === undefined) continue; + for (const v of operandValues) { + values.add(v); + } + } + + if (values.size > 0) { + this.#variables.set(phi.place.identifier.id, values); + } + } +} + +/** + * Returns a value that represents the combined states of the two input values. + * If the two values are semantically equivalent, it returns the first argument. + */ +function mergeAbstractValues( + a: AbstractValue, + b: AbstractValue, +): AbstractValue { + const kind = mergeValueKinds(a.kind, b.kind); + if ( + kind === a.kind && + kind === b.kind && + Set_isSuperset(a.reason, b.reason) + ) { + return a; + } + const reason = new Set(a.reason); + for (const r of b.reason) { + reason.add(r); + } + return {kind, reason}; +} + +type InstructionSignature = { + effects: ReadonlyArray; +}; + +function conditionallyMutateIterator(place: Place): AliasingEffect | null { + if ( + !( + isArrayType(place.identifier) || + isSetType(place.identifier) || + isMapType(place.identifier) + ) + ) { + return { + kind: 'MutateTransitiveConditionally', + value: place, + }; + } + return null; +} + +/** + * Computes an effect signature for the instruction _without_ looking at the inference state, + * and only using the semantics of the instructions and the inferred types. The idea is to make + * it easy to check that the semantics of each instruction are preserved by describing only the + * effects and not making decisions based on the inference state. + * + * Then in applySignature(), above, we refine this signature based on the inference state. + * + * NOTE: this function is designed to be cached so it's only computed once upon first visiting + * an instruction. + */ +function computeSignatureForInstruction( + context: Context, + env: Environment, + instr: Instruction, +): InstructionSignature { + const {lvalue, value} = instr; + const effects: Array = []; + switch (value.kind) { + case 'ArrayExpression': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + // All elements are captured into part of the output value + for (const element of value.elements) { + if (element.kind === 'Identifier') { + effects.push({ + kind: 'Capture', + from: element, + into: lvalue, + }); + } else if (element.kind === 'Spread') { + const mutateIterator = conditionallyMutateIterator(element.place); + if (mutateIterator != null) { + effects.push(mutateIterator); + } + effects.push({ + kind: 'Capture', + from: element.place, + into: lvalue, + }); + } else { + continue; + } + } + break; + } + case 'ObjectExpression': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + for (const property of value.properties) { + if (property.kind === 'ObjectProperty') { + effects.push({ + kind: 'Capture', + from: property.place, + into: lvalue, + }); + } else { + effects.push({ + kind: 'Capture', + from: property.place, + into: lvalue, + }); + } + } + break; + } + case 'Await': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + // Potentially mutates the receiver (awaiting it changes its state and can run side effects) + effects.push({kind: 'MutateTransitiveConditionally', value: value.value}); + /** + * Data from the promise may be returned into the result, but await does not directly return + * the promise itself + */ + effects.push({ + kind: 'Capture', + from: value.value, + into: lvalue, + }); + break; + } + case 'NewExpression': + case 'CallExpression': + case 'MethodCall': { + let callee; + let receiver; + let mutatesCallee; + if (value.kind === 'NewExpression') { + callee = value.callee; + receiver = value.callee; + mutatesCallee = false; + } else if (value.kind === 'CallExpression') { + callee = value.callee; + receiver = value.callee; + mutatesCallee = true; + } else if (value.kind === 'MethodCall') { + callee = value.property; + receiver = value.receiver; + mutatesCallee = false; + } else { + assertExhaustive( + value, + `Unexpected value kind '${(value as any).kind}'`, + ); + } + const signature = getFunctionCallSignature(env, callee.identifier.type); + effects.push({ + kind: 'Apply', + receiver, + function: callee, + mutatesFunction: mutatesCallee, + args: value.args, + into: lvalue, + signature, + loc: value.loc, + }); + break; + } + case 'PropertyDelete': + case 'ComputedDelete': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + // Mutates the object by removing the property, no aliasing + effects.push({kind: 'Mutate', value: value.object}); + break; + } + case 'PropertyLoad': + case 'ComputedLoad': { + if (isPrimitiveType(lvalue.identifier)) { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + } else { + effects.push({ + kind: 'CreateFrom', + from: value.object, + into: lvalue, + }); + } + break; + } + case 'PropertyStore': + case 'ComputedStore': { + effects.push({kind: 'Mutate', value: value.object}); + effects.push({ + kind: 'Capture', + from: value.value, + into: value.object, + }); + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'ObjectMethod': + case 'FunctionExpression': { + /** + * We've already analyzed the function expression in AnalyzeFunctions. There, we assign + * a Capture effect to any context variable that appears (locally) to be aliased and/or + * mutated. The precise effects are annotated on the function expression's aliasingEffects + * property, but we don't want to execute those effects yet. We can only use those when + * we know exactly how the function is invoked — via an Apply effect from a custom signature. + * + * But in the general case, functions can be passed around and possibly called in ways where + * we don't know how to interpret their precise effects. For example: + * + * ``` + * const a = {}; + * + * // We don't want to consider a as mutating here, this just declares the function + * const f = () => { maybeMutate(a) }; + * + * // We don't want to consider a as mutating here either, it can't possibly call f yet + * const x = [f]; + * + * // Here we have to assume that f can be called (transitively), and have to consider a + * // as mutating + * callAllFunctionInArray(x); + * ``` + * + * So for any context variables that were inferred as captured or mutated, we record a + * Capture effect. If the resulting function is transitively mutated, this will mean + * that those operands are also considered mutated. If the function is never called, + * they won't be! + * + * This relies on the rule that: + * Capture a -> b and MutateTransitive(b) => Mutate(a) + * + * Substituting: + * Capture contextvar -> function and MutateTransitive(function) => Mutate(contextvar) + * + * Note that if the type of the context variables are frozen, global, or primitive, the + * Capture will either get pruned or downgraded to an ImmutableCapture. + */ + effects.push({ + kind: 'CreateFunction', + into: lvalue, + function: value, + captures: value.loweredFunc.func.context.filter( + operand => operand.effect === Effect.Capture, + ), + }); + break; + } + case 'GetIterator': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + if ( + isArrayType(value.collection.identifier) || + isMapType(value.collection.identifier) || + isSetType(value.collection.identifier) + ) { + /* + * Builtin collections are known to return a fresh iterator on each call, + * so the iterator does not alias the collection + */ + effects.push({ + kind: 'Capture', + from: value.collection, + into: lvalue, + }); + } else { + /* + * Otherwise, the object may return itself as the iterator, so we have to + * assume that the result directly aliases the collection. Further, the + * method to get the iterator could potentially mutate the collection + */ + effects.push({kind: 'Alias', from: value.collection, into: lvalue}); + effects.push({ + kind: 'MutateTransitiveConditionally', + value: value.collection, + }); + } + break; + } + case 'IteratorNext': { + /* + * Technically advancing an iterator will always mutate it (for any reasonable implementation) + * But because we create an alias from the collection to the iterator if we don't know the type, + * then it's possible the iterator is aliased to a frozen value and we wouldn't want to error. + * so we mark this as conditional mutation to allow iterating frozen values. + */ + effects.push({kind: 'MutateConditionally', value: value.iterator}); + // Extracts part of the original collection into the result + effects.push({ + kind: 'CreateFrom', + from: value.collection, + into: lvalue, + }); + break; + } + case 'NextPropertyOf': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'JsxExpression': + case 'JsxFragment': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Frozen, + reason: ValueReason.JsxCaptured, + }); + for (const operand of eachInstructionValueOperand(value)) { + effects.push({ + kind: 'Freeze', + value: operand, + reason: ValueReason.JsxCaptured, + }); + effects.push({ + kind: 'Capture', + from: operand, + into: lvalue, + }); + } + if (value.kind === 'JsxExpression') { + if (value.tag.kind === 'Identifier') { + // Tags are render function, by definition they're called during render + effects.push({ + kind: 'Render', + place: value.tag, + }); + } + if (value.children != null) { + // Children are typically called during render, not used as an event/effect callback + for (const child of value.children) { + effects.push({ + kind: 'Render', + place: child, + }); + } + } + } + break; + } + case 'DeclareLocal': { + // TODO check this + effects.push({ + kind: 'Create', + into: value.lvalue.place, + // TODO: what kind here??? + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + effects.push({ + kind: 'Create', + into: lvalue, + // TODO: what kind here??? + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'Destructure': { + for (const patternLValue of eachInstructionValueLValue(value)) { + if (isPrimitiveType(patternLValue.identifier)) { + effects.push({ + kind: 'Create', + into: patternLValue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + } else { + effects.push({ + kind: 'CreateFrom', + from: value.value, + into: patternLValue, + }); + } + } + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'LoadContext': { + /* + * Context variables are like mutable boxes. Loading from one + * is equivalent to a PropertyLoad from the box, so we model it + * with the same effect we use there (CreateFrom) + */ + effects.push({kind: 'CreateFrom', from: value.place, into: lvalue}); + break; + } + case 'DeclareContext': { + // Context variables are conceptually like mutable boxes + const kind = value.lvalue.kind; + if ( + !context.hoistedContextDeclarations.has( + value.lvalue.place.identifier.declarationId, + ) || + kind === InstructionKind.HoistedConst || + kind === InstructionKind.HoistedFunction || + kind === InstructionKind.HoistedLet + ) { + /** + * If this context variable is not hoisted, or this is the declaration doing the hoisting, + * then we create the box. + */ + effects.push({ + kind: 'Create', + into: value.lvalue.place, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + } else { + /** + * Otherwise this may be a "declare", but there was a previous DeclareContext that + * hoisted this variable, and we're mutating it here. + */ + effects.push({kind: 'Mutate', value: value.lvalue.place}); + } + effects.push({ + kind: 'Create', + into: lvalue, + // The result can't be referenced so this value doesn't matter + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'StoreContext': { + /* + * Context variables are like mutable boxes, so semantically + * we're either creating (let/const) or mutating (reassign) a box, + * and then capturing the value into it. + */ + if ( + value.lvalue.kind === InstructionKind.Reassign || + context.hoistedContextDeclarations.has( + value.lvalue.place.identifier.declarationId, + ) + ) { + effects.push({kind: 'Mutate', value: value.lvalue.place}); + } else { + effects.push({ + kind: 'Create', + into: value.lvalue.place, + value: ValueKind.Mutable, + reason: ValueReason.Other, + }); + } + effects.push({ + kind: 'Capture', + from: value.value, + into: value.lvalue.place, + }); + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'LoadLocal': { + effects.push({kind: 'Assign', from: value.place, into: lvalue}); + break; + } + case 'StoreLocal': { + effects.push({ + kind: 'Assign', + from: value.value, + into: value.lvalue.place, + }); + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'PostfixUpdate': + case 'PrefixUpdate': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + effects.push({ + kind: 'Create', + into: value.lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'StoreGlobal': { + effects.push({ + kind: 'MutateGlobal', + place: value.value, + error: { + reason: + 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)', + loc: instr.loc, + suggestions: null, + severity: ErrorSeverity.InvalidReact, + }, + }); + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'TypeCastExpression': { + effects.push({kind: 'Assign', from: value.value, into: lvalue}); + break; + } + case 'LoadGlobal': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Global, + reason: ValueReason.Global, + }); + break; + } + case 'StartMemoize': + case 'FinishMemoize': { + if (env.config.enablePreserveExistingMemoizationGuarantees) { + for (const operand of eachInstructionValueOperand(value)) { + effects.push({ + kind: 'Freeze', + value: operand, + reason: ValueReason.Other, + }); + } + } + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + case 'TaggedTemplateExpression': + case 'BinaryExpression': + case 'Debugger': + case 'JSXText': + case 'MetaProperty': + case 'Primitive': + case 'RegExpLiteral': + case 'TemplateLiteral': + case 'UnaryExpression': + case 'UnsupportedNode': { + effects.push({ + kind: 'Create', + into: lvalue, + value: ValueKind.Primitive, + reason: ValueReason.Other, + }); + break; + } + } + return { + effects, + }; +} + +/** + * Creates a set of aliasing effects given a legacy FunctionSignature. This makes all of the + * old implicit behaviors from the signatures and InferReferenceEffects explicit, see comments + * in the body for details. + * + * The goal of this method is to make it easier to migrate incrementally to the new system, + * so we don't have to immediately write new signatures for all the methods to get expected + * compilation output. + */ +function computeEffectsForLegacySignature( + state: InferenceState, + signature: FunctionSignature, + lvalue: Place, + receiver: Place, + args: Array, + loc: SourceLocation, +): Array { + const returnValueReason = signature.returnValueReason ?? ValueReason.Other; + const effects: Array = []; + effects.push({ + kind: 'Create', + into: lvalue, + value: signature.returnValueKind, + reason: returnValueReason, + }); + if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) { + effects.push({ + kind: 'Impure', + place: receiver, + error: { + reason: + 'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)', + description: + signature.canonicalName != null + ? `\`${signature.canonicalName}\` is an impure function whose results may change on every call` + : null, + severity: ErrorSeverity.InvalidReact, + loc, + suggestions: null, + }, + }); + } + const stores: Array = []; + const captures: Array = []; + function visit(place: Place, effect: Effect): void { + switch (effect) { + case Effect.Store: { + effects.push({ + kind: 'Mutate', + value: place, + }); + stores.push(place); + break; + } + case Effect.Capture: { + captures.push(place); + break; + } + case Effect.ConditionallyMutate: { + effects.push({ + kind: 'MutateTransitiveConditionally', + value: place, + }); + break; + } + case Effect.ConditionallyMutateIterator: { + const mutateIterator = conditionallyMutateIterator(place); + if (mutateIterator != null) { + effects.push(mutateIterator); + } + effects.push({ + kind: 'Capture', + from: place, + into: lvalue, + }); + break; + } + case Effect.Freeze: { + effects.push({ + kind: 'Freeze', + value: place, + reason: returnValueReason, + }); + break; + } + case Effect.Mutate: { + effects.push({kind: 'MutateTransitive', value: place}); + break; + } + case Effect.Read: { + effects.push({ + kind: 'ImmutableCapture', + from: place, + into: lvalue, + }); + break; + } + } + } + + if ( + signature.mutableOnlyIfOperandsAreMutable && + areArgumentsImmutableAndNonMutating(state, args) + ) { + effects.push({ + kind: 'Alias', + from: receiver, + into: lvalue, + }); + for (const arg of args) { + if (arg.kind === 'Hole') { + continue; + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + effects.push({ + kind: 'ImmutableCapture', + from: place, + into: lvalue, + }); + } + return effects; + } + + if (signature.calleeEffect !== Effect.Capture) { + /* + * InferReferenceEffects and FunctionSignature have an implicit assumption that the receiver + * is captured into the return value. Consider for example the signature for Array.proto.pop: + * the calleeEffect is Store, since it's a known mutation but non-transitive. But the return + * of the pop() captures from the receiver! This isn't specified explicitly. So we add this + * here, and rely on applySignature() to downgrade this to ImmutableCapture (or prune) if + * the type doesn't actually need to be captured based on the input and return type. + */ + effects.push({ + kind: 'Alias', + from: receiver, + into: lvalue, + }); + } + visit(receiver, signature.calleeEffect); + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.kind === 'Hole') { + continue; + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + const signatureEffect = + arg.kind === 'Identifier' && i < signature.positionalParams.length + ? signature.positionalParams[i]! + : (signature.restParam ?? Effect.ConditionallyMutate); + const effect = getArgumentEffect(signatureEffect, arg); + + visit(place, effect); + } + if (captures.length !== 0) { + if (stores.length === 0) { + // If no stores, then capture into the return value + for (const capture of captures) { + effects.push({kind: 'Alias', from: capture, into: lvalue}); + } + } else { + // Else capture into the stores + for (const capture of captures) { + for (const store of stores) { + effects.push({kind: 'Capture', from: capture, into: store}); + } + } + } + } + return effects; +} + +/** + * Returns true if all of the arguments are both non-mutable (immutable or frozen) + * _and_ are not functions which might mutate their arguments. Note that function + * expressions count as frozen so long as they do not mutate free variables: this + * function checks that such functions also don't mutate their inputs. + */ +function areArgumentsImmutableAndNonMutating( + state: InferenceState, + args: Array, +): boolean { + for (const arg of args) { + if (arg.kind === 'Hole') { + continue; + } + if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') { + const fnShape = state.env.getFunctionSignature(arg.identifier.type); + if (fnShape != null) { + return ( + !fnShape.positionalParams.some(isKnownMutableEffect) && + (fnShape.restParam == null || + !isKnownMutableEffect(fnShape.restParam)) + ); + } + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + + const kind = state.kind(place).kind; + switch (kind) { + case ValueKind.Primitive: + case ValueKind.Frozen: { + /* + * Only immutable values, or frozen lambdas are allowed. + * A lambda may appear frozen even if it may mutate its inputs, + * so we have a second check even for frozen value types + */ + break; + } + default: { + /** + * Globals, module locals, and other locally defined functions may + * mutate their arguments. + */ + return false; + } + } + const values = state.values(place); + for (const value of values) { + if ( + value.kind === 'FunctionExpression' && + value.loweredFunc.func.params.some(param => { + const place = param.kind === 'Identifier' ? param : param.place; + const range = place.identifier.mutableRange; + return range.end > range.start + 1; + }) + ) { + // This is a function which may mutate its inputs + return false; + } + } + } + return true; +} + +function computeEffectsForSignature( + env: Environment, + signature: AliasingSignature, + lvalue: Place, + receiver: Place, + args: Array, + // Used for signatures constructed dynamically which reference context variables + context: Array = [], + loc: SourceLocation, +): Array | null { + if ( + // Not enough args + signature.params.length > args.length || + // Too many args and there is no rest param to hold them + (args.length > signature.params.length && signature.rest == null) + ) { + return null; + } + // Build substitutions + const mutableSpreads = new Set(); + const substitutions: Map> = new Map(); + substitutions.set(signature.receiver, [receiver]); + substitutions.set(signature.returns, [lvalue]); + const params = signature.params; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.kind === 'Hole') { + continue; + } else if (params == null || i >= params.length || arg.kind === 'Spread') { + if (signature.rest == null) { + return null; + } + const place = arg.kind === 'Identifier' ? arg : arg.place; + getOrInsertWith(substitutions, signature.rest, () => []).push(place); + + if (arg.kind === 'Spread') { + const mutateIterator = conditionallyMutateIterator(arg.place); + if (mutateIterator != null) { + mutableSpreads.add(arg.place.identifier.id); + } + } + } else { + const param = params[i]; + substitutions.set(param, [arg]); + } + } + + /* + * Signatures constructed dynamically from function expressions will reference values + * other than their receiver/args/etc. We populate the substitution table with these + * values so that we can still exit for unpopulated substitutions + */ + for (const operand of context) { + substitutions.set(operand.identifier.id, [operand]); + } + + const effects: Array = []; + for (const signatureTemporary of signature.temporaries) { + const temp = createTemporaryPlace(env, receiver.loc); + substitutions.set(signatureTemporary.identifier.id, [temp]); + } + + // Apply substitutions + for (const effect of signature.effects) { + switch (effect.kind) { + case 'Assign': + case 'ImmutableCapture': + case 'Alias': + case 'CreateFrom': + case 'Capture': { + const from = substitutions.get(effect.from.identifier.id) ?? []; + const to = substitutions.get(effect.into.identifier.id) ?? []; + for (const fromId of from) { + for (const toId of to) { + effects.push({ + kind: effect.kind, + from: fromId, + into: toId, + }); + } + } + break; + } + case 'Impure': + case 'MutateFrozen': + case 'MutateGlobal': { + const values = substitutions.get(effect.place.identifier.id) ?? []; + for (const value of values) { + effects.push({kind: effect.kind, place: value, error: effect.error}); + } + break; + } + case 'Render': { + const values = substitutions.get(effect.place.identifier.id) ?? []; + for (const value of values) { + effects.push({kind: effect.kind, place: value}); + } + break; + } + case 'Mutate': + case 'MutateTransitive': + case 'MutateTransitiveConditionally': + case 'MutateConditionally': { + const values = substitutions.get(effect.value.identifier.id) ?? []; + for (const id of values) { + effects.push({kind: effect.kind, value: id}); + } + break; + } + case 'Freeze': { + const values = substitutions.get(effect.value.identifier.id) ?? []; + for (const value of values) { + if (mutableSpreads.has(value.identifier.id)) { + CompilerError.throwTodo({ + reason: 'Support spread syntax for hook arguments', + loc: value.loc, + }); + } + effects.push({kind: 'Freeze', value, reason: effect.reason}); + } + break; + } + case 'Create': { + const into = substitutions.get(effect.into.identifier.id) ?? []; + for (const value of into) { + effects.push({ + kind: 'Create', + into: value, + value: effect.value, + reason: effect.reason, + }); + } + break; + } + case 'Apply': { + const applyReceiver = substitutions.get(effect.receiver.identifier.id); + if (applyReceiver == null || applyReceiver.length !== 1) { + return null; + } + const applyFunction = substitutions.get(effect.function.identifier.id); + if (applyFunction == null || applyFunction.length !== 1) { + return null; + } + const applyInto = substitutions.get(effect.into.identifier.id); + if (applyInto == null || applyInto.length !== 1) { + return null; + } + const applyArgs: Array = []; + for (const arg of effect.args) { + if (arg.kind === 'Hole') { + applyArgs.push(arg); + } else if (arg.kind === 'Identifier') { + const applyArg = substitutions.get(arg.identifier.id); + if (applyArg == null || applyArg.length !== 1) { + return null; + } + applyArgs.push(applyArg[0]); + } else { + const applyArg = substitutions.get(arg.place.identifier.id); + if (applyArg == null || applyArg.length !== 1) { + return null; + } + applyArgs.push({kind: 'Spread', place: applyArg[0]}); + } + } + effects.push({ + kind: 'Apply', + mutatesFunction: effect.mutatesFunction, + receiver: applyReceiver[0], + args: applyArgs, + function: applyFunction[0], + into: applyInto[0], + signature: effect.signature, + loc, + }); + break; + } + case 'CreateFunction': { + CompilerError.throwTodo({ + reason: `Support CreateFrom effects in signatures`, + loc: receiver.loc, + }); + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind '${(effect as any).kind}'`, + ); + } + } + } + return effects; +} + +function buildSignatureFromFunctionExpression( + env: Environment, + fn: FunctionExpression, +): AliasingSignature { + let rest: IdentifierId | null = null; + const params: Array = []; + for (const param of fn.loweredFunc.func.params) { + if (param.kind === 'Identifier') { + params.push(param.identifier.id); + } else { + rest = param.place.identifier.id; + } + } + return { + receiver: makeIdentifierId(0), + params, + rest: rest ?? createTemporaryPlace(env, fn.loc).identifier.id, + returns: fn.loweredFunc.func.returns.identifier.id, + effects: fn.loweredFunc.func.aliasingEffects ?? [], + temporaries: [], + }; +} + +export type AbstractValue = { + kind: ValueKind; + reason: ReadonlySet; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts new file mode 100644 index 0000000000000..79f8cf8c0e85b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts @@ -0,0 +1,766 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {CompilerError, SourceLocation} from '..'; +import { + BlockId, + Effect, + HIRFunction, + Identifier, + IdentifierId, + InstructionId, + isJsxType, + makeInstructionId, + ValueKind, + ValueReason, + Place, + isPrimitiveType, +} from '../HIR/HIR'; +import { + eachInstructionLValue, + eachInstructionValueOperand, + eachTerminalOperand, +} from '../HIR/visitors'; +import {assertExhaustive, getOrInsertWith} from '../Utils/utils'; +import {Err, Ok, Result} from '../Utils/Result'; +import {AliasingEffect} from './AliasingEffects'; + +/** + * This pass builds an abstract model of the heap and interprets the effects of the + * given function in order to determine the following: + * - The mutable ranges of all identifiers in the function + * - The externally-visible effects of the function, such as mutations of params and + * context-vars, aliasing between params/context-vars/return-value, and impure side + * effects. + * - The legacy `Effect` to store on each Place. + * + * This pass builds a data flow graph using the effects, tracking an abstract notion + * of "when" each effect occurs relative to the others. It then walks each mutation + * effect against the graph, updating the range of each node that would be reachable + * at the "time" that the effect occurred. + * + * This pass also validates against invalid effects: any function that is reachable + * by being called, or via a Render effect, is validated against mutating globals + * or calling impure code. + * + * Note that this function also populates the outer function's aliasing effects with + * any mutations that apply to its params or context variables. + * + * ## Example + * A function expression such as the following: + * + * ``` + * (x) => { x.y = true } + * ``` + * + * Would populate a `Mutate x` aliasing effect on the outer function. + * + * ## Returned Function Effects + * + * The function returns (if successful) a list of externally-visible effects. + * This is determined by simulating a conditional, transitive mutation against + * each param, context variable, and return value in turn, and seeing which other + * such values are affected. If they're affected, they must be captured, so we + * record a Capture. + * + * The only tricky bit is the return value, which could _alias_ (or even assign) + * one or more of the params/context-vars rather than just capturing. So we have + * to do a bit more tracking for returns. + */ +export function inferMutationAliasingRanges( + fn: HIRFunction, + {isFunctionExpression}: {isFunctionExpression: boolean}, +): Result, CompilerError> { + // The set of externally-visible effects + const functionEffects: Array = []; + + /** + * Part 1: Infer mutable ranges for values. We build an abstract model of + * values, the alias/capture edges between them, and the set of mutations. + * Edges and mutations are ordered, with mutations processed against the + * abstract model only after it is fully constructed by visiting all blocks + * _and_ connecting phis. Phis are considered ordered at the time of the + * phi node. + * + * This should (may?) mean that mutations are able to see the full state + * of the graph and mark all the appropriate identifiers as mutated at + * the correct point, accounting for both backward and forward edges. + * Ie a mutation of x accounts for both values that flowed into x, + * and values that x flowed into. + */ + const state = new AliasingState(); + type PendingPhiOperand = {from: Place; into: Place; index: number}; + const pendingPhis = new Map>(); + const mutations: Array<{ + index: number; + id: InstructionId; + transitive: boolean; + kind: MutationKind; + place: Place; + }> = []; + const renders: Array<{index: number; place: Place}> = []; + + let index = 0; + + const errors = new CompilerError(); + + for (const param of [...fn.params, ...fn.context, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + state.create(place, {kind: 'Object'}); + } + const seenBlocks = new Set(); + for (const block of fn.body.blocks.values()) { + for (const phi of block.phis) { + state.create(phi.place, {kind: 'Phi'}); + for (const [pred, operand] of phi.operands) { + if (!seenBlocks.has(pred)) { + // NOTE: annotation required to actually typecheck and not silently infer `any` + const blockPhis = getOrInsertWith>( + pendingPhis, + pred, + () => [], + ); + blockPhis.push({from: operand, into: phi.place, index: index++}); + } else { + state.assign(index++, operand, phi.place); + } + } + } + seenBlocks.add(block.id); + + for (const instr of block.instructions) { + if (instr.effects == null) continue; + for (const effect of instr.effects) { + if (effect.kind === 'Create') { + state.create(effect.into, {kind: 'Object'}); + } else if (effect.kind === 'CreateFunction') { + state.create(effect.into, { + kind: 'Function', + function: effect.function.loweredFunc.func, + }); + } else if (effect.kind === 'CreateFrom') { + state.createFrom(index++, effect.from, effect.into); + } else if (effect.kind === 'Assign') { + /** + * TODO: Invariant that the node is not initialized yet + * + * InferFunctionExpressionAliasingEffectSignatures currently infers + * Assign effects in some places that should be Alias, leading to + * Assign effects that reinitialize a value. The end result appears to + * be fine, but we should fix that inference pass so that we add the + * invariant here. + */ + if (!state.nodes.has(effect.into.identifier)) { + state.create(effect.into, {kind: 'Object'}); + } + state.assign(index++, effect.from, effect.into); + } else if (effect.kind === 'Alias') { + state.assign(index++, effect.from, effect.into); + } else if (effect.kind === 'Capture') { + state.capture(index++, effect.from, effect.into); + } else if ( + effect.kind === 'MutateTransitive' || + effect.kind === 'MutateTransitiveConditionally' + ) { + mutations.push({ + index: index++, + id: instr.id, + transitive: true, + kind: + effect.kind === 'MutateTransitive' + ? MutationKind.Definite + : MutationKind.Conditional, + place: effect.value, + }); + } else if ( + effect.kind === 'Mutate' || + effect.kind === 'MutateConditionally' + ) { + mutations.push({ + index: index++, + id: instr.id, + transitive: false, + kind: + effect.kind === 'Mutate' + ? MutationKind.Definite + : MutationKind.Conditional, + place: effect.value, + }); + } else if ( + effect.kind === 'MutateFrozen' || + effect.kind === 'MutateGlobal' || + effect.kind === 'Impure' + ) { + errors.push(effect.error); + functionEffects.push(effect); + } else if (effect.kind === 'Render') { + renders.push({index: index++, place: effect.place}); + functionEffects.push(effect); + } + } + } + const blockPhis = pendingPhis.get(block.id); + if (blockPhis != null) { + for (const {from, into, index} of blockPhis) { + state.assign(index, from, into); + } + } + if (block.terminal.kind === 'return') { + state.assign(index++, block.terminal.value, fn.returns); + } + + if ( + (block.terminal.kind === 'maybe-throw' || + block.terminal.kind === 'return') && + block.terminal.effects != null + ) { + for (const effect of block.terminal.effects) { + if (effect.kind === 'Alias') { + state.assign(index++, effect.from, effect.into); + } else { + CompilerError.invariant(effect.kind === 'Freeze', { + reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`, + loc: block.terminal.loc, + }); + } + } + } + } + + for (const mutation of mutations) { + state.mutate( + mutation.index, + mutation.place.identifier, + makeInstructionId(mutation.id + 1), + mutation.transitive, + mutation.kind, + mutation.place.loc, + errors, + ); + } + for (const render of renders) { + state.render(render.index, render.place.identifier, errors); + } + for (const param of [...fn.context, ...fn.params]) { + const place = param.kind === 'Identifier' ? param : param.place; + const node = state.nodes.get(place.identifier); + if (node == null) { + continue; + } + let mutated = false; + if (node.local != null) { + if (node.local.kind === MutationKind.Conditional) { + mutated = true; + functionEffects.push({ + kind: 'MutateConditionally', + value: {...place, loc: node.local.loc}, + }); + } else if (node.local.kind === MutationKind.Definite) { + mutated = true; + functionEffects.push({ + kind: 'Mutate', + value: {...place, loc: node.local.loc}, + }); + } + } + if (node.transitive != null) { + if (node.transitive.kind === MutationKind.Conditional) { + mutated = true; + functionEffects.push({ + kind: 'MutateTransitiveConditionally', + value: {...place, loc: node.transitive.loc}, + }); + } else if (node.transitive.kind === MutationKind.Definite) { + mutated = true; + functionEffects.push({ + kind: 'MutateTransitive', + value: {...place, loc: node.transitive.loc}, + }); + } + } + if (mutated) { + place.effect = Effect.Capture; + } + } + + /** + * Part 2 + * Add legacy operand-specific effects based on instruction effects and mutable ranges. + * Also fixes up operand mutable ranges, making sure that start is non-zero if the value + * is mutated (depended on by later passes like InferReactiveScopeVariables which uses this + * to filter spurious mutations of globals, which we now guard against more precisely) + */ + for (const block of fn.body.blocks.values()) { + for (const phi of block.phis) { + // TODO: we don't actually set these effects today! + phi.place.effect = Effect.Store; + const isPhiMutatedAfterCreation: boolean = + phi.place.identifier.mutableRange.end > + (block.instructions.at(0)?.id ?? block.terminal.id); + for (const operand of phi.operands.values()) { + operand.effect = isPhiMutatedAfterCreation + ? Effect.Capture + : Effect.Read; + } + if ( + isPhiMutatedAfterCreation && + phi.place.identifier.mutableRange.start === 0 + ) { + /* + * TODO: ideally we'd construct a precise start range, but what really + * matters is that the phi's range appears mutable (end > start + 1) + * so we just set the start to the previous instruction before this block + */ + const firstInstructionIdOfBlock = + block.instructions.at(0)?.id ?? block.terminal.id; + phi.place.identifier.mutableRange.start = makeInstructionId( + firstInstructionIdOfBlock - 1, + ); + } + } + for (const instr of block.instructions) { + for (const lvalue of eachInstructionLValue(instr)) { + lvalue.effect = Effect.ConditionallyMutate; + if (lvalue.identifier.mutableRange.start === 0) { + lvalue.identifier.mutableRange.start = instr.id; + } + if (lvalue.identifier.mutableRange.end === 0) { + lvalue.identifier.mutableRange.end = makeInstructionId( + Math.max(instr.id + 1, lvalue.identifier.mutableRange.end), + ); + } + } + for (const operand of eachInstructionValueOperand(instr.value)) { + operand.effect = Effect.Read; + } + if (instr.effects == null) { + continue; + } + const operandEffects = new Map(); + for (const effect of instr.effects) { + switch (effect.kind) { + case 'Assign': + case 'Alias': + case 'Capture': + case 'CreateFrom': { + const isMutatedOrReassigned = + effect.into.identifier.mutableRange.end > instr.id; + if (isMutatedOrReassigned) { + operandEffects.set(effect.from.identifier.id, Effect.Capture); + operandEffects.set(effect.into.identifier.id, Effect.Store); + } else { + operandEffects.set(effect.from.identifier.id, Effect.Read); + operandEffects.set(effect.into.identifier.id, Effect.Store); + } + break; + } + case 'CreateFunction': + case 'Create': { + break; + } + case 'Mutate': { + operandEffects.set(effect.value.identifier.id, Effect.Store); + break; + } + case 'Apply': { + CompilerError.invariant(false, { + reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`, + loc: effect.function.loc, + }); + } + case 'MutateTransitive': + case 'MutateConditionally': + case 'MutateTransitiveConditionally': { + operandEffects.set( + effect.value.identifier.id, + Effect.ConditionallyMutate, + ); + break; + } + case 'Freeze': { + operandEffects.set(effect.value.identifier.id, Effect.Freeze); + break; + } + case 'ImmutableCapture': { + // no-op, Read is the default + break; + } + case 'Impure': + case 'Render': + case 'MutateFrozen': + case 'MutateGlobal': { + // no-op + break; + } + default: { + assertExhaustive( + effect, + `Unexpected effect kind ${(effect as any).kind}`, + ); + } + } + } + for (const lvalue of eachInstructionLValue(instr)) { + const effect = + operandEffects.get(lvalue.identifier.id) ?? + Effect.ConditionallyMutate; + lvalue.effect = effect; + } + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + operand.identifier.mutableRange.end > instr.id && + operand.identifier.mutableRange.start === 0 + ) { + operand.identifier.mutableRange.start = instr.id; + } + const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read; + operand.effect = effect; + } + + /** + * This case is targeted at hoisted functions like: + * + * ``` + * x(); + * function x() { ... } + * ``` + * + * Which turns into: + * + * t0 = DeclareContext HoistedFunction x + * t1 = LoadContext x + * t2 = CallExpression t1 ( ) + * t3 = FunctionExpression ... + * t4 = StoreContext Function x = t3 + * + * If the function had captured mutable values, it would already have its + * range extended to include the StoreContext. But if the function doesn't + * capture any mutable values its range won't have been extended yet. We + * want to ensure that the value is memoized along with the context variable, + * not independently of it (bc of the way we do codegen for hoisted functions). + * So here we check for StoreContext rvalues and if they haven't already had + * their range extended to at least this instruction, we extend it. + */ + if ( + instr.value.kind === 'StoreContext' && + instr.value.value.identifier.mutableRange.end <= instr.id + ) { + instr.value.value.identifier.mutableRange.end = makeInstructionId( + instr.id + 1, + ); + } + } + if (block.terminal.kind === 'return') { + block.terminal.value.effect = isFunctionExpression + ? Effect.Read + : Effect.Freeze; + } else { + for (const operand of eachTerminalOperand(block.terminal)) { + operand.effect = Effect.Read; + } + } + } + + /** + * Part 3 + * Finish populating the externally visible effects. Above we bubble-up the side effects + * (MutateFrozen/MutableGlobal/Impure/Render) as well as mutations of context variables. + * Here we populate an effect to create the return value as well as populating alias/capture + * effects for how data flows between the params, context vars, and return. + */ + const returns = fn.returns.identifier; + functionEffects.push({ + kind: 'Create', + into: fn.returns, + value: isPrimitiveType(returns) + ? ValueKind.Primitive + : isJsxType(returns.type) + ? ValueKind.Frozen + : ValueKind.Mutable, + reason: ValueReason.KnownReturnSignature, + }); + /** + * Determine precise data-flow effects by simulating transitive mutations of the params/ + * captures and seeing what other params/context variables are affected. Anything that + * would be transitively mutated needs a capture relationship. + */ + const tracked: Array = []; + const ignoredErrors = new CompilerError(); + for (const param of [...fn.params, ...fn.context, fn.returns]) { + const place = param.kind === 'Identifier' ? param : param.place; + tracked.push(place); + } + for (const into of tracked) { + const mutationIndex = index++; + state.mutate( + mutationIndex, + into.identifier, + null, + true, + MutationKind.Conditional, + into.loc, + ignoredErrors, + ); + for (const from of tracked) { + if ( + from.identifier.id === into.identifier.id || + from.identifier.id === fn.returns.identifier.id + ) { + continue; + } + const fromNode = state.nodes.get(from.identifier); + CompilerError.invariant(fromNode != null, { + reason: `Expected a node to exist for all parameters and context variables`, + loc: into.loc, + }); + if (fromNode.lastMutated === mutationIndex) { + if (into.identifier.id === fn.returns.identifier.id) { + // The return value could be any of the params/context variables + functionEffects.push({ + kind: 'Alias', + from, + into, + }); + } else { + // Otherwise params/context-vars can only capture each other + functionEffects.push({ + kind: 'Capture', + from, + into, + }); + } + } + } + } + + if (errors.hasErrors() && !isFunctionExpression) { + return Err(errors); + } + return Ok(functionEffects); +} + +function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void { + for (const effect of fn.aliasingEffects ?? []) { + switch (effect.kind) { + case 'Impure': + case 'MutateFrozen': + case 'MutateGlobal': { + errors.push(effect.error); + break; + } + } + } +} + +export enum MutationKind { + None = 0, + Conditional = 1, + Definite = 2, +} + +type Node = { + id: Identifier; + createdFrom: Map; + captures: Map; + aliases: Map; + edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>; + transitive: {kind: MutationKind; loc: SourceLocation} | null; + local: {kind: MutationKind; loc: SourceLocation} | null; + lastMutated: number; + value: + | {kind: 'Object'} + | {kind: 'Phi'} + | {kind: 'Function'; function: HIRFunction}; +}; +class AliasingState { + nodes: Map = new Map(); + + create(place: Place, value: Node['value']): void { + this.nodes.set(place.identifier, { + id: place.identifier, + createdFrom: new Map(), + captures: new Map(), + aliases: new Map(), + edges: [], + transitive: null, + local: null, + lastMutated: 0, + value, + }); + } + + createFrom(index: number, from: Place, into: Place): void { + this.create(into, {kind: 'Object'}); + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'alias'}); + if (!toNode.createdFrom.has(from.identifier)) { + toNode.createdFrom.set(from.identifier, index); + } + } + + capture(index: number, from: Place, into: Place): void { + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'capture'}); + if (!toNode.captures.has(from.identifier)) { + toNode.captures.set(from.identifier, index); + } + } + + assign(index: number, from: Place, into: Place): void { + const fromNode = this.nodes.get(from.identifier); + const toNode = this.nodes.get(into.identifier); + if (fromNode == null || toNode == null) { + return; + } + fromNode.edges.push({index, node: into.identifier, kind: 'alias'}); + if (!toNode.aliases.has(from.identifier)) { + toNode.aliases.set(from.identifier, index); + } + } + + render(index: number, start: Identifier, errors: CompilerError): void { + const seen = new Set(); + const queue: Array = [start]; + while (queue.length !== 0) { + const current = queue.pop()!; + if (seen.has(current)) { + continue; + } + seen.add(current); + const node = this.nodes.get(current); + if (node == null || node.transitive != null || node.local != null) { + continue; + } + if (node.value.kind === 'Function') { + appendFunctionErrors(errors, node.value.function); + } + for (const [alias, when] of node.createdFrom) { + if (when >= index) { + continue; + } + queue.push(alias); + } + for (const [alias, when] of node.aliases) { + if (when >= index) { + continue; + } + queue.push(alias); + } + for (const [capture, when] of node.captures) { + if (when >= index) { + continue; + } + queue.push(capture); + } + } + } + + mutate( + index: number, + start: Identifier, + // Null is used for simulated mutations + end: InstructionId | null, + transitive: boolean, + kind: MutationKind, + loc: SourceLocation, + errors: CompilerError, + ): void { + const seen = new Set(); + const queue: Array<{ + place: Identifier; + transitive: boolean; + direction: 'backwards' | 'forwards'; + }> = [{place: start, transitive, direction: 'backwards'}]; + while (queue.length !== 0) { + const {place: current, transitive, direction} = queue.pop()!; + if (seen.has(current)) { + continue; + } + seen.add(current); + const node = this.nodes.get(current); + if (node == null) { + continue; + } + node.lastMutated = Math.max(node.lastMutated, index); + if (end != null) { + node.id.mutableRange.end = makeInstructionId( + Math.max(node.id.mutableRange.end, end), + ); + } + if ( + node.value.kind === 'Function' && + node.transitive == null && + node.local == null + ) { + appendFunctionErrors(errors, node.value.function); + } + if (transitive) { + if (node.transitive == null || node.transitive.kind < kind) { + node.transitive = {kind, loc}; + } + } else { + if (node.local == null || node.local.kind < kind) { + node.local = {kind, loc}; + } + } + /** + * all mutations affect "forward" edges by the rules: + * - Capture a -> b, mutate(a) => mutate(b) + * - Alias a -> b, mutate(a) => mutate(b) + */ + for (const edge of node.edges) { + if (edge.index >= index) { + break; + } + queue.push({place: edge.node, transitive, direction: 'forwards'}); + } + for (const [alias, when] of node.createdFrom) { + if (when >= index) { + continue; + } + queue.push({place: alias, transitive: true, direction: 'backwards'}); + } + if (direction === 'backwards' || node.value.kind !== 'Phi') { + /** + * all mutations affect backward alias edges by the rules: + * - Alias a -> b, mutate(b) => mutate(a) + * - Alias a -> b, mutateTransitive(b) => mutate(a) + * + * However, if we reached a phi because one of its inputs was mutated + * (and we're advancing "forwards" through that node's edges), then + * we know we've already processed the mutation at its source. The + * phi's other inputs can't be affected. + */ + for (const [alias, when] of node.aliases) { + if (when >= index) { + continue; + } + queue.push({place: alias, transitive, direction: 'backwards'}); + } + } + /** + * but only transitive mutations affect captures + */ + if (transitive) { + for (const [capture, when] of node.captures) { + if (when >= index) { + continue; + } + queue.push({place: capture, transitive, direction: 'backwards'}); + } + } + } + } +} 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 d1546038edcbe..1b0856791a180 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -48,7 +48,7 @@ import { eachTerminalOperand, eachTerminalSuccessor, } from '../HIR/visitors'; -import {assertExhaustive} from '../Utils/utils'; +import {assertExhaustive, Set_isSuperset} from '../Utils/utils'; import { inferTerminalFunctionEffects, inferInstructionFunctionEffects, @@ -779,7 +779,7 @@ function inferParam( * │ Mutable │───┘ * └──────────────────────────┘ */ -function mergeValues(a: ValueKind, b: ValueKind): ValueKind { +export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind { if (a === b) { return a; } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) { @@ -821,28 +821,16 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind { } } -/** - * @returns `true` if `a` is a superset of `b`. - */ -function isSuperset(a: ReadonlySet, b: ReadonlySet): boolean { - for (const v of b) { - if (!a.has(v)) { - return false; - } - } - return true; -} - function mergeAbstractValues( a: AbstractValue, b: AbstractValue, ): AbstractValue { - const kind = mergeValues(a.kind, b.kind); + const kind = mergeValueKinds(a.kind, b.kind); if ( kind === a.kind && kind === b.kind && - isSuperset(a.reason, b.reason) && - isSuperset(a.context, b.context) + Set_isSuperset(a.reason, b.reason) && + Set_isSuperset(a.context, b.context) ) { return a; } @@ -1989,7 +1977,7 @@ function areArgumentsImmutableAndNonMutating( return true; } -function getArgumentEffect( +export function getArgumentEffect( signatureEffect: Effect | null, arg: Place | SpreadPattern, ): Effect { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts index c6c6f2f54fbef..d71f6ebc8a063 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts @@ -11,13 +11,16 @@ import { Environment, FunctionExpression, GeneratedSource, + GotoTerminal, GotoVariant, HIRFunction, IdentifierId, InstructionKind, LabelTerminal, Place, + isStatementBlockKind, makeInstructionId, + mergeConsecutiveBlocks, promoteTemporary, reversePostorderBlocks, } from '../HIR'; @@ -72,6 +75,10 @@ import {retainWhere} from '../Utils/utils'; * - All return statements in the original function expression are replaced with a * StoreLocal to the temporary we allocated before plus a Goto to the fallthrough * block (code following the CallExpression). + * + * Note that if the inliined function has only one return, we avoid the labeled block + * and fully inline the code. The original return is replaced with an assignmen to the + * IIFE's call expression lvalue. */ export function inlineImmediatelyInvokedFunctionExpressions( fn: HIRFunction, @@ -90,100 +97,144 @@ export function inlineImmediatelyInvokedFunctionExpressions( */ const queue = Array.from(fn.body.blocks.values()); queue: for (const block of queue) { - for (let ii = 0; ii < block.instructions.length; ii++) { - const instr = block.instructions[ii]!; - switch (instr.value.kind) { - case 'FunctionExpression': { - if (instr.lvalue.identifier.name === null) { - functions.set(instr.lvalue.identifier.id, instr.value); - } - break; - } - case 'CallExpression': { - if (instr.value.args.length !== 0) { - // We don't support inlining when there are arguments - continue; - } - const body = functions.get(instr.value.callee.identifier.id); - if (body === undefined) { - // Not invoking a local function expression, can't inline - continue; + /* + * We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an + * expression block. + */ + if (isStatementBlockKind(block.kind)) { + for (let ii = 0; ii < block.instructions.length; ii++) { + const instr = block.instructions[ii]!; + switch (instr.value.kind) { + case 'FunctionExpression': { + if (instr.lvalue.identifier.name === null) { + functions.set(instr.lvalue.identifier.id, instr.value); + } + break; } + case 'CallExpression': { + if (instr.value.args.length !== 0) { + // We don't support inlining when there are arguments + continue; + } + const body = functions.get(instr.value.callee.identifier.id); + if (body === undefined) { + // Not invoking a local function expression, can't inline + continue; + } - if ( - body.loweredFunc.func.params.length > 0 || - body.loweredFunc.func.async || - body.loweredFunc.func.generator - ) { - // Can't inline functions with params, or async/generator functions - continue; - } + if ( + body.loweredFunc.func.params.length > 0 || + body.loweredFunc.func.async || + body.loweredFunc.func.generator + ) { + // Can't inline functions with params, or async/generator functions + continue; + } - // We know this function is used for an IIFE and can prune it later - inlinedFunctions.add(instr.value.callee.identifier.id); + // We know this function is used for an IIFE and can prune it later + inlinedFunctions.add(instr.value.callee.identifier.id); - // Create a new block which will contain code following the IIFE call - const continuationBlockId = fn.env.nextBlockId; - const continuationBlock: BasicBlock = { - id: continuationBlockId, - instructions: block.instructions.slice(ii + 1), - kind: block.kind, - phis: new Set(), - preds: new Set(), - terminal: block.terminal, - }; - fn.body.blocks.set(continuationBlockId, continuationBlock); + // Create a new block which will contain code following the IIFE call + const continuationBlockId = fn.env.nextBlockId; + const continuationBlock: BasicBlock = { + id: continuationBlockId, + instructions: block.instructions.slice(ii + 1), + kind: block.kind, + phis: new Set(), + preds: new Set(), + terminal: block.terminal, + }; + fn.body.blocks.set(continuationBlockId, continuationBlock); - /* - * Trim the original block to contain instructions up to (but not including) - * the IIFE - */ - block.instructions.length = ii; + /* + * Trim the original block to contain instructions up to (but not including) + * the IIFE + */ + block.instructions.length = ii; - /* - * To account for complex control flow within the lambda, we treat the lambda - * as if it were a single labeled statement, and replace all returns with gotos - * to the label fallthrough. - */ - const newTerminal: LabelTerminal = { - block: body.loweredFunc.func.body.entry, - id: makeInstructionId(0), - kind: 'label', - fallthrough: continuationBlockId, - loc: block.terminal.loc, - }; - block.terminal = newTerminal; + if (hasSingleExitReturnTerminal(body.loweredFunc.func)) { + block.terminal = { + kind: 'goto', + block: body.loweredFunc.func.body.entry, + id: block.terminal.id, + loc: block.terminal.loc, + variant: GotoVariant.Break, + } as GotoTerminal; + for (const block of body.loweredFunc.func.body.blocks.values()) { + if (block.terminal.kind === 'return') { + block.instructions.push({ + id: makeInstructionId(0), + loc: block.terminal.loc, + lvalue: instr.lvalue, + value: { + kind: 'LoadLocal', + loc: block.terminal.loc, + place: block.terminal.value, + }, + effects: null, + }); + block.terminal = { + kind: 'goto', + block: continuationBlockId, + id: block.terminal.id, + loc: block.terminal.loc, + variant: GotoVariant.Break, + } as GotoTerminal; + } + } + for (const [id, block] of body.loweredFunc.func.body.blocks) { + block.preds.clear(); + fn.body.blocks.set(id, block); + } + } else { + /* + * To account for multiple returns within the lambda, we treat the lambda + * as if it were a single labeled statement, and replace all returns with gotos + * to the label fallthrough. + */ + const newTerminal: LabelTerminal = { + block: body.loweredFunc.func.body.entry, + id: makeInstructionId(0), + kind: 'label', + fallthrough: continuationBlockId, + loc: block.terminal.loc, + }; + block.terminal = newTerminal; - // We store the result in the IIFE temporary - const result = instr.lvalue; + // We store the result in the IIFE temporary + const result = instr.lvalue; - // Declare the IIFE temporary - declareTemporary(fn.env, block, result); + // Declare the IIFE temporary + declareTemporary(fn.env, block, result); - // Promote the temporary with a name as we require this to persist - promoteTemporary(result.identifier); + // Promote the temporary with a name as we require this to persist + if (result.identifier.name == null) { + promoteTemporary(result.identifier); + } - /* - * Rewrite blocks from the lambda to replace any `return` with a - * store to the result and `goto` the continuation block - */ - for (const [id, block] of body.loweredFunc.func.body.blocks) { - block.preds.clear(); - rewriteBlock(fn.env, block, continuationBlockId, result); - fn.body.blocks.set(id, block); - } + /* + * Rewrite blocks from the lambda to replace any `return` with a + * store to the result and `goto` the continuation block + */ + for (const [id, block] of body.loweredFunc.func.body.blocks) { + block.preds.clear(); + rewriteBlock(fn.env, block, continuationBlockId, result); + fn.body.blocks.set(id, block); + } + } - /* - * Ensure we visit the continuation block, since there may have been - * sequential IIFEs that need to be visited. - */ - queue.push(continuationBlock); - continue queue; - } - default: { - for (const place of eachInstructionValueOperand(instr.value)) { - // Any other use of a function expression means it isn't an IIFE - functions.delete(place.identifier.id); + /* + * Ensure we visit the continuation block, since there may have been + * sequential IIFEs that need to be visited. + */ + queue.push(continuationBlock); + continue queue; + } + default: { + for (const place of eachInstructionValueOperand(instr.value)) { + // Any other use of a function expression means it isn't an IIFE + functions.delete(place.identifier.id); + } } } } @@ -192,7 +243,7 @@ export function inlineImmediatelyInvokedFunctionExpressions( if (inlinedFunctions.size !== 0) { // Remove instructions that define lambdas which we inlined - for (const [, block] of fn.body.blocks) { + for (const block of fn.body.blocks.values()) { retainWhere( block.instructions, instr => !inlinedFunctions.has(instr.lvalue.identifier.id), @@ -206,7 +257,23 @@ export function inlineImmediatelyInvokedFunctionExpressions( reversePostorderBlocks(fn.body); markInstructionIds(fn.body); markPredecessors(fn.body); + mergeConsecutiveBlocks(fn); + } +} + +/** + * Returns true if the function has a single exit terminal (throw/return) which is a return + */ +function hasSingleExitReturnTerminal(fn: HIRFunction): boolean { + let hasReturn = false; + let exitCount = 0; + for (const [, block] of fn.body.blocks) { + if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') { + hasReturn ||= block.terminal.kind === 'return'; + exitCount++; + } } + return exitCount === 1 && hasReturn; } /* @@ -235,6 +302,7 @@ function rewriteBlock( type: null, loc: terminal.loc, }, + effects: null, }); block.terminal = { kind: 'goto', @@ -263,5 +331,6 @@ function declareTemporary( type: null, loc: result.loc, }, + effects: null, }); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md new file mode 100644 index 0000000000000..60ef16a6245f3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md @@ -0,0 +1,544 @@ +# The Mutability & Aliasing Model + +This document describes the new (as of June 2025) mutability and aliasing model powering React Compiler. The mutability and aliasing system is a conceptual subcomponent whose primary role is to determine minimal sets of values that mutate together, and the range of instructions over which those mutations occur. These minimal sets of values that mutate together, and the corresponding instructions doing those mutations, are ultimately grouped into reactive scopes, which then translate into memoization blocks in the output (after substantial additional processing described in the comments of those passes). + +To build an intuition, consider the following example: + +```js +function Component() { + // a is created and mutated over the course of these two instructions: + const a = {}; + mutate(a); + + // b and c are created and mutated together — mutate might modify b via c + const b = {}; + const c = {b}; + mutate(c); + + // does not modify a/b/c + return +} +``` + +The goal of mutability and aliasing inference is to understand the set of instructions that create/modify a, b, and c. + +In code, the mutability and aliasing model is compromised of the following phases: + +* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. +* `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values. +* `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values. + +Finally, `AnalyzeFunctions` needs to understand the mutation and aliasing semantics of nested FunctionExpression and ObjectMethod values. `AnalyzeFunctions` calls `InferFunctionExpressionAliasingEffectsSignature` to determine the publicly observable set of mutation/aliasing effects for nested functions. + +## Mutation and Aliasing Effects + +The inference model is based on a set of "effects" that describe subtle aspects of mutation, aliasing, and other changes to the state of values over time + +### Creation Effects + +#### Create + +```js +{ + kind: 'Create'; + into: Place; + value: ValueKind; + reason: ValueReason; +} +``` + +Describes the creation of a new value with the given kind, and reason for having that kind. For example, `x = 10` might have an effect like `Create x = ValueKind.Primitive [ValueReason.Other]`. + +#### CreateFunction + +```js +{ + kind: 'CreateFunction'; + captures: Array; + function: FunctionExpression | ObjectMethod; + into: Place; +} +``` + +Describes the creation of new function value, capturing the given set of mutable values. CreateFunction is used to specifically track function types so that we can precisely model calls to those functions with `Apply`. + +#### Apply + +```js +{ + kind: 'Apply'; + receiver: Place; + function: Place; // same as receiver for function calls + mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default + args: Array; + into: Place; // where result is stored + signature: FunctionSignature | null; +} +``` + +Describes the potential creation of a value by calling a function. This models `new`, function calls, and method calls. The inference algorithm uses the most precise signature it can determine: + +* If the function is a locally created function expression, we use a signature inferred from the behavior of that function to interpret the effects of calling it with the given arguments. +* Else if the function has a known aliasing signature (new style precise effects signature), we apply the arguments to that signature to get a precise set of effects. +* Else if the function has a legacy style signature (with per-param effects) we convert the legacy per-Place effects into aliasing effects (described in this doc) and apply those. +* Else fall back to inferring a generic set of effects. + +The generic fallback is to assume: +- The return value may alias any of the arguments (Alias param -> return) +- Any arguments *may* be transitively mutated (MutateTransitiveConditionally param) +- Any argument may be captured into any other argument (Capture paramN -> paramM for all N,M where N != M) + +### Aliasing Effects + +These effects describe data-flow only, separately from mutation or other state-changing semantics. + +#### Assign + +```js +{ + kind: 'Assign'; + from: Place; + into: Place; +} +``` + +Describes an `x = y` assignment, where the receiving (into) value is overwritten with a new (from) value. After this effect, any previous assignments/aliases to the receiving value are dropped. Note that `Alias` initializes the receiving value. + +> TODO: InferMutationAliasingRanges may not fully reset aliases on encountering this effect + +#### Alias + +```js +{ + kind: 'Alias'; + from: Place; + into: Place; +} +``` + +Describes that an assignment _may_ occur, but that the possible assignment is non-exclusive. The canonical use-case for `Alias` is a function that may return more than one of its arguments, such as `(x, y, z) => x ? y : z`. Here, the result of this function may be `y` or `z`, but neither one overwrites the other. Note that `Alias` does _not_ initialize the receiving value: it should always be paired with an effect to create the receiving value. + +#### Capture + +```js +{ + kind: 'Capture'; + from: Place; + into: Place; +} +``` + +Describes that a reference to one variable (from) is stored within another value (into). Examples include: +- An array expression captures the items of the array (`array = [capturedValue]`) +- Array.prototype.push captures the pushed values into the array (`array.push(capturedValue)`) +- Property assignment captures the value onto the object (`object.property = capturedValue`) + +#### CreateFrom + +```js +{ + kind: 'CreateFrom'; + from: Place; + into: Place; +} +``` + +This is somewhat the inverse of `Capture`. The `CreateFrom` effect describes that a variable is initialized by extracting _part_ of another value, without taking a direct alias to the full other value. Examples include: + +- Indexing into an array (`createdFrom = array[0]`) +- Reading an object property (`createdFrom = object.property`) +- Getting a Map key (`createdFrom = map.get(key)`) + +#### ImmutableCapture + +Describes immutable data flow from one value to another. This is not currently used for anything, but is intended to eventually power a more sophisticated escape analysis. + +### State-Changing Effects + +The following effects describe state changes to specific values, not data flow. In many cases, JavaScript semantics will involve a combination of both data-flow effects *and* state-change effects. For example, `object.property = value` has data flow (`Capture object <- value`) and mutation (`Mutate object`). + +#### Freeze + +```js +{ + kind: 'Freeze', + // The reference being frozen + value: Place; + // The reason the value is frozen (passed to a hook, passed to jsx, etc) + reason: ValueReason; +} +``` + +Once a reference to a value has been passed to React, that value is generally not safe to mutate further. This is not a strictly required property of React, but is a natural consequence of making components and hooks composable without leaking implementation details. Concretely, once a value has been passed as a JSX prop, passed as argument to a hook, or returned from a hook, it must be assumed that the other "side" — receiver of the prop/argument/return value — will use that value as an input to an effect or memoization unit. Mutating that value (instead of creating a new value) will fail to cause the consuming computation to update: + +```js +// INVALID DO NOT DO THIS +function Component(props) { + const array = useArray(props.value); + // OOPS! this value is memoized, the array won't get re-created + // when `props.value` changes, so we might just keep pushing new + // values to the same array on every render! + array.push(props.otherValue); +} + +function useArray(a) { + return useMemo(() => [a], [a]); +} +``` + +The **Freeze** effect accepts a variable reference and a reason that the value is being frozen. Note: _freeze only applies to the reference, not the underlying value_. Our inference is conservative, and assumes that there may still be other references to the same underlying value which are mutated later. For example: + +```js +const x = {}; +const y = []; +x.y = y; +freeze(y); // y _reference_ is frozen +x.y.push(props.value); // but y is still considered mutable bc of this +``` + +#### Mutate (and MutateConditionally) + +```js +{ + kind: 'Mutate'; + value: Place; +} +``` + +Mutate indicates that a value is mutated, without modifying any of the values that it may transitively have captured. Canonical examples include: + +- Pushing an item onto an array modifies the array, but does not modify any items stored _within_ the array (unless the array has a reference to itself!) +- Assigning a value to an object property modifies the object, but not any values stored in the object's other properties. + +This helps explain the distinction between Assign/Alias and Capture: Mutate only affects assign/alias but not captures. + +`MutateConditionally` is an alternative in which the mutation _may_ happen depending on the type of the value. The conditional variant is not generally used and included for completeness. + + + +#### MutateTransitiveConditionally (and MutateTransitive) + +`MutateTransitiveConditionally` represents an operation that may mutate _any_ aspect of a value, including reaching arbitrarily deep into nested values to mutate them. This is the default semantic for unknown functions — we have no idea what they do, so we assume that they are idempotent but may mutate any aspect of the mutable values that are passed to them. + +There is also `MutateTransitive` for completeness, but this is not generally used. + +### Side Effects + +Finally, there are a few effects that describe error, or potential error, conditions: + +- `MutateFrozen` is always an error, because it indicates known mutation of a value that should not be mutated. +- `MutateGlobal` indicates known mutation of a global value, which is not safe during render. This effect is an error if reachable during render, but allowed if only reachable via an event handler or useEffect. +- `Impure` indicates calling some other logic that is impure/side-effecting. This is an error if reachable during render, but allowed if only reachable via an event handler or useEffect. + - TODO: we could probably merge this and MutateGlobal +- `Render` indicates a value that is not mutated, but is known to be called during render. It's used for a few particular places like JSX tags and JSX children, which we assume are accessed during render (while other props may be event handlers etc). This helps to detect more MutateGlobal/Impure effects and reject more invalid programs. + + +## Rules + +### Mutation of Alias Mutates the Source Value + +``` +Alias a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = maybeIdentity(b); // Alias a <- b +a.property = value; // a could be b, so this mutates b +``` + +### Mutation of Assignment Mutates the Source Value + +``` +Assign a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = b; +a.property = value // a _is_ b, this mutates b +``` + +### Mutation of CreateFrom Mutates the Source Value + +``` +CreateFrom a <- b +Mutate a +=> +Mutate b +``` + +Example: + +```js +const a = b[index]; +a.property = value // the contents of b are transitively mutated +``` + + +### Mutation of Capture Does *Not* Mutate the Source Value + +``` +Capture a <- b +Mutate a +!=> +~Mutate b~ +``` + +Example: + +```js +const a = {}; +a.b = b; +a.property = value; // mutates a, not b +``` + +### Mutation of Source Affects Alias, Assignment, CreateFrom, and Capture + +``` +Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b +Mutate b +=> +Mutate a +``` + +A derived value changes when it's source value is mutated. + +Example: + +```js +const x = {}; +const y = [x]; +x.y = true; // this changes the value within `y` ie mutates y +``` + + +### TransitiveMutation of Alias, Assignment, CreateFrom, or Capture Mutates the Source + +``` +Alias a <- b OR Assign a <- b OR CreateFrom a <- b OR Capture a <- b +MutateTransitive a +=> +MutateTransitive b +``` + +Remember, the intuition for a transitive mutation is that it's something that could traverse arbitrarily deep into an object and mutate whatever it finds. Imagine something that recurses into every nested object/array and sets `.field = value`. Given a function `mutate()` that does this, then: + +```js +const a = b; // assign +mutate(a); // clearly can transitively mutate b + +const a = maybeIdentity(b); // alias +mutate(a); // clearly can transitively mutate b + +const a = b[index]; // createfrom +mutate(a); // clearly can transitively mutate b + +const a = {}; +a.b = b; // capture +mutate(a); // can transitively mutate b +``` + +### Freeze Does Not Freeze the Value + +Freeze does not freeze the value itself: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +!=> +~Freeze x~ +``` + +This means that subsequent mutations of the original value are valid: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +Mutate x +=> +Mutate x (mutation is ok) +``` + +As well as mutations through other assignments/aliases/captures/createfroms of the original value: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +Alias z <- x OR Capture z <- x OR CreateFrom z <- x OR Assign z <- x +Mutate z +=> +Mutate x (mutation is ok) +``` + +### Freeze Freezes The Reference + +Although freeze doesn't freeze the value, it does affect the reference. The reference cannot be used to mutate. + +Conditional mutations of the reference are no-ops: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +MutateConditional y +=> +(no mutation) +``` + +And known mutations of the reference are errors: + +``` +Create x +Assign y <- x OR Alias y <- x OR CreateFrom y <- x OR Capture y <- x +Freeze y +MutateConditional y +=> +MutateFrozen y error=... +``` + +### Corollary: Transitivity of Assign/Alias/CreateFrom/Capture + +A key part of the inference model is inferring a signature for function expressions. The signature is a minimal set of effects that describes the publicly observable behavior of the function. This can include "global" effects like side effects (MutateGlobal/Impure) as well as mutations/aliasing of parameters and free variables. + +In order to determine the aliasing of params and free variables into each other and/or the return value, we may encounter chains of assign, alias, createfrom, and capture effects. For example: + +```js +const f = (x) => { + const y = [x]; // capture y <- x + const z = y[0]; // createfrom z <- y + return z; // assign return <- z +} +// return <- x +``` + +In this example we can see that there should be some effect on `f` that tracks the flow of data from `x` into the return value. The key constraint is preserving the semantics around how local/transitive mutations of the destination would affect the source. + +#### Each of the effects is transitive with itself + +``` +Assign b <- a +Assign c <- b +=> +Assign c <- a +``` + +``` +Alias b <- a +Alias c <- b +=> +Alias c <- a +``` + +``` +CreateFrom b <- a +CreateFrom c <- b +=> +CreateFrom c <- a +``` + +``` +Capture b <- a +Capture c <- b +=> +Capture c <- a +``` + +#### Alias > Assign + +``` +Assign b <- a +Alias c <- b +=> +Alias c <- a +``` + +``` +Alias b <- a +Assign c <- b +=> +Alias c <- a +``` + +### CreateFrom > Assign/Alias + +Intuition: + +``` +CreateFrom b <- a +Alias c <- b OR Assign c <- b +=> +CreateFrom c <- a +``` + +``` +Alias b <- a OR Assign b <- a +CreateFrom c <- b +=> +CreateFrom c <- a +``` + +### Capture > Assign/Alias + +Intuition: capturing means that a local mutation of the destination will not affect the source, so we preserve the capture. + +``` +Capture b <- a +Alias c <- b OR Assign c <- b +=> +Capture c <- a +``` + +``` +Alias b <- a OR Assign b <- a +Capture c <- b +=> +Capture c <- a +``` + +### Capture And CreateFrom + +Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations: + +Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. + +```js +const b = [a]; // capture +const c = b[0]; // createfrom +mutate(c); // this clearly can mutate a, so the result must be one of Assign/Alias/CreateFrom +``` + +We use Alias as the return type because the mutability kind of the result is not derived from the source value (there's a fresh object in between due to the capture), so the full set of effects in practice would be a Create+Alias. + +``` +Capture b <- a +CreateFrom c <- b +=> +Alias c <- a +``` + +Meanwhile the opposite direction preserves the capture, because the result is not the same as the source: + +```js +const b = a[0]; // createfrom +const c = [b]; // capture +mutate(c); // does not mutate a, so the result must be Capture +``` + +``` +CreateFrom b <- a +Capture c <- b +=> +Capture c <- a +``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts index 29c59c7b3644a..91e2ce0692576 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts @@ -151,6 +151,7 @@ export function inlineJsxTransform( type: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; currentBlockInstructions.push(varInstruction); @@ -167,6 +168,7 @@ export function inlineJsxTransform( }, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; currentBlockInstructions.push(devGlobalInstruction); @@ -220,6 +222,7 @@ export function inlineJsxTransform( type: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; thenBlockInstructions.push(reassignElseInstruction); @@ -292,6 +295,7 @@ export function inlineJsxTransform( ], loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; elseBlockInstructions.push(reactElementInstruction); @@ -309,6 +313,7 @@ export function inlineJsxTransform( type: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; elseBlockInstructions.push(reassignConditionalInstruction); @@ -436,6 +441,7 @@ function createSymbolProperty( binding: {kind: 'Global', name: 'Symbol'}, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(symbolInstruction); @@ -450,6 +456,7 @@ function createSymbolProperty( property: makePropertyLiteral('for'), loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(symbolForInstruction); @@ -463,6 +470,7 @@ function createSymbolProperty( value: symbolName, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(symbolValueInstruction); @@ -478,6 +486,7 @@ function createSymbolProperty( args: [symbolValueInstruction.lvalue], loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; const $$typeofProperty: ObjectProperty = { @@ -508,6 +517,7 @@ function createTagProperty( value: componentTag.name, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; tagProperty = { @@ -634,6 +644,7 @@ function createPropsProperties( elements: [...children], loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; nextInstructions.push(childrenPropInstruction); @@ -657,6 +668,7 @@ function createPropsProperties( value: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; refProperty = { @@ -678,6 +690,7 @@ function createPropsProperties( value: null, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; keyProperty = { @@ -711,6 +724,7 @@ function createPropsProperties( properties: props, loc: instr.value.loc, }, + effects: null, loc: instr.loc, }; propsProperty = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts index 834f60195af29..921ec59ecd2a1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -25,7 +25,6 @@ import { makeBlockId, makeInstructionId, makePropertyLiteral, - makeType, markInstructionIds, promoteTemporary, reversePostorderBlocks, @@ -146,6 +145,7 @@ function emitLoadLoweredContextCallee( id: makeInstructionId(0), loc: GeneratedSource, lvalue: createTemporaryPlace(env, GeneratedSource), + effects: null, value: loadGlobal, }; } @@ -192,6 +192,7 @@ function emitPropertyLoad( lvalue: object, value: loadObj, id: makeInstructionId(0), + effects: null, loc: GeneratedSource, }; @@ -206,6 +207,7 @@ function emitPropertyLoad( lvalue: element, value: loadProp, id: makeInstructionId(0), + effects: null, loc: GeneratedSource, }; return { @@ -237,6 +239,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { kind: 'return', loc: GeneratedSource, value: arrayInstr.lvalue, + effects: null, }, preds: new Set(), phis: new Set(), @@ -249,7 +252,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { env, params: [obj], returnTypeAnnotation: null, - returnType: makeType(), + returns: createTemporaryPlace(env, GeneratedSource), context: [], effects: null, body: { @@ -278,6 +281,7 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { loc: GeneratedSource, }, lvalue: createTemporaryPlace(env, GeneratedSource), + effects: null, loc: GeneratedSource, }; return fnInstr; @@ -294,6 +298,7 @@ function emitArrayInstr(elements: Array, env: Environment): Instruction { id: makeInstructionId(0), value: array, lvalue: arrayLvalue, + effects: null, loc: GeneratedSource, }; return arrayInstr; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts index d35c4d77362db..b7590a570197a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts @@ -21,7 +21,6 @@ import { makeBlockId, makeIdentifierName, makeInstructionId, - makeType, ObjectProperty, Place, promoteTemporary, @@ -297,6 +296,7 @@ function emitOutlinedJsx( }, loc: GeneratedSource, }, + effects: null, }; promoteTemporaryJsxTag(loadJsx.lvalue.identifier); const jsxExpr: Instruction = { @@ -312,6 +312,7 @@ function emitOutlinedJsx( openingLoc: GeneratedSource, closingLoc: GeneratedSource, }, + effects: null, }; return [loadJsx, jsxExpr]; @@ -353,6 +354,7 @@ function emitOutlinedFn( kind: 'return', loc: GeneratedSource, value: instructions.at(-1)!.lvalue, + effects: null, }, preds: new Set(), phis: new Set(), @@ -365,7 +367,7 @@ function emitOutlinedFn( env, params: [propsObj], returnTypeAnnotation: null, - returnType: makeType(), + returns: createTemporaryPlace(env, GeneratedSource), context: [], effects: null, body: { @@ -517,6 +519,7 @@ function emitDestructureProps( loc: GeneratedSource, value: propsObj, }, + effects: null, }; return destructurePropsInstr; } 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 17c62c02a6ee8..f7da5229548ab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -44,7 +44,7 @@ import { getHookKind, makeIdentifierName, } from '../HIR/HIR'; -import {printIdentifier, printPlace} from '../HIR/PrintHIR'; +import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR'; import {eachPatternOperand} from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; import {GuardKind} from '../Utils/RuntimeDiagnosticConstants'; @@ -349,11 +349,9 @@ function codegenReactiveFunction( fn: ReactiveFunction, ): Result { for (const param of fn.params) { - if (param.kind === 'Identifier') { - cx.temp.set(param.identifier.declarationId, null); - } else { - cx.temp.set(param.place.identifier.declarationId, null); - } + const place = param.kind === 'Identifier' ? param : param.place; + cx.temp.set(place.identifier.declarationId, null); + cx.declare(place.identifier); } const params = fn.params.map(param => convertParameter(param)); @@ -1183,7 +1181,7 @@ function codegenTerminal( ? codegenPlaceToExpression(cx, case_.test) : null; const block = codegenBlock(cx, case_.block!); - return t.switchCase(test, [block]); + return t.switchCase(test, block.body.length === 0 ? [] : [block]); }), ); } @@ -1310,7 +1308,7 @@ function codegenInstructionNullable( }); CompilerError.invariant(value?.type === 'FunctionExpression', { reason: 'Expected a function as a function declaration value', - description: null, + description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`, loc: instr.value.loc, suggestions: null, }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts index eb2caa424e417..642b89747e6ea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/ExtractScopeDeclarationsFromDestructuring.ts @@ -79,6 +79,10 @@ export function extractScopeDeclarationsFromDestructuring( fn: ReactiveFunction, ): void { const state = new State(fn.env); + for (const param of fn.params) { + const place = param.kind === 'Identifier' ? param : param.place; + state.declared.add(place.identifier.declarationId); + } visitReactiveFunction(fn, new Visitor(), state); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts index 5ae4c7dfc72f9..52a0312dcd851 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -1064,12 +1064,29 @@ class PruneScopesTransform extends ReactiveFunctionTransform< const value = instruction.value; if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') { + // Complex cases of useMemo inlining result in a temporary that is reassigned const ids = getOrInsertDefault( this.reassignments, value.lvalue.place.identifier.declarationId, new Set(), ); ids.add(value.value.identifier); + } else if ( + value.kind === 'LoadLocal' && + value.place.identifier.scope != null && + instruction.lvalue != null && + instruction.lvalue.identifier.scope == null + ) { + /* + * Simpler cases result in a direct assignment to the original lvalue, with a + * LoadLocal + */ + const ids = getOrInsertDefault( + this.reassignments, + instruction.lvalue.identifier.declarationId, + new Set(), + ); + ids.add(value.place.identifier); } else if (value.kind === 'FinishMemoize') { let decls; if (value.decl.identifier.scope == null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts index b033af6750c37..f88c85f2f0f39 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -436,6 +436,7 @@ function makeLoadUseFireInstruction( value: instrValue, lvalue: {...useFirePlace}, loc: GeneratedSource, + effects: null, }; } @@ -460,6 +461,7 @@ function makeLoadFireCalleeInstruction( }, lvalue: {...loadedFireCallee}, loc: GeneratedSource, + effects: null, }; } @@ -483,6 +485,7 @@ function makeCallUseFireInstruction( value: useFireCall, lvalue: {...useFireCallResultPlace}, loc: GeneratedSource, + effects: null, }; } @@ -511,6 +514,7 @@ function makeStoreUseFireInstruction( }, lvalue: fireFunctionBindingLValuePlace, loc: GeneratedSource, + effects: null, }; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 69812fc130ded..859c871c263ad 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -90,7 +90,8 @@ function apply(func: HIRFunction, unifier: Unifier): void { } } } - func.returnType = unifier.get(func.returnType); + const returns = func.returns.identifier; + returns.type = unifier.get(returns.type); } type TypeEquation = { @@ -143,12 +144,12 @@ function* generate( } } if (returnTypes.length > 1) { - yield equation(func.returnType, { + yield equation(func.returns.identifier.type, { kind: 'Phi', operands: returnTypes, }); } else if (returnTypes.length === 1) { - yield equation(func.returnType, returnTypes[0]!); + yield equation(func.returns.identifier.type, returnTypes[0]!); } } @@ -407,7 +408,7 @@ function* generateInstructionTypes( yield equation(left, { kind: 'Function', shapeId: BuiltInFunctionId, - return: value.loweredFunc.func.returnType, + return: value.loweredFunc.func.returns.identifier.type, isConstructor: false, }); break; 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 aa91c48b1b0db..e5fbacfc772df 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -121,6 +121,21 @@ export function Set_intersect(sets: Array>): Set { return result; } +/** + * @returns `true` if `a` is a superset of `b`. + */ +export function Set_isSuperset( + a: ReadonlySet, + b: ReadonlySet, +): boolean { + for (const v of b) { + if (!a.has(v)) { + return false; + } + } + return true; +} + export function Iterable_some( iter: Iterable, pred: (item: T) => boolean, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts index 81612a7441728..573db2f6b7d00 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts @@ -58,8 +58,7 @@ export function validateNoFreezingKnownMutableFunctions( const effect = contextMutationEffects.get(operand.identifier.id); if (effect != null) { errors.push({ - reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`, - description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`, + reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`, loc: operand.loc, severity: ErrorSeverity.InvalidReact, }); @@ -112,6 +111,55 @@ export function validateNoFreezingKnownMutableFunctions( ); if (knownMutation && knownMutation.kind === 'ContextMutation') { contextMutationEffects.set(lvalue.identifier.id, knownMutation); + } else if ( + fn.env.config.enableNewMutationAliasingModel && + value.loweredFunc.func.aliasingEffects != null + ) { + const context = new Set( + value.loweredFunc.func.context.map(p => p.identifier.id), + ); + effects: for (const effect of value.loweredFunc.func + .aliasingEffects) { + switch (effect.kind) { + case 'Mutate': + case 'MutateTransitive': { + const knownMutation = contextMutationEffects.get( + effect.value.identifier.id, + ); + if (knownMutation != null) { + contextMutationEffects.set( + lvalue.identifier.id, + knownMutation, + ); + } else if ( + context.has(effect.value.identifier.id) && + !isRefOrRefLikeMutableType(effect.value.identifier.type) + ) { + contextMutationEffects.set(lvalue.identifier.id, { + kind: 'ContextMutation', + effect: Effect.Mutate, + loc: effect.value.loc, + places: new Set([effect.value]), + }); + break effects; + } + break; + } + case 'MutateConditionally': + case 'MutateTransitiveConditionally': { + const knownMutation = contextMutationEffects.get( + effect.value.identifier.id, + ); + if (knownMutation != null) { + contextMutationEffects.set( + lvalue.identifier.id, + knownMutation, + ); + } + break; + } + } + } } break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts index 1829d77822998..c150c1fbd7486 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -445,11 +445,13 @@ class Visitor extends ReactiveFunctionVisitor { */ this.recordTemporaries(instruction, state); const value = instruction.value; + // Track reassignments from inlining of manual memo if ( value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign' && state.manualMemoState != null ) { + // Complex cases of inlining end up with a temporary that is reassigned const ids = getOrInsertDefault( state.manualMemoState.reassignments, value.lvalue.place.identifier.declarationId, @@ -457,6 +459,21 @@ class Visitor extends ReactiveFunctionVisitor { ); ids.add(value.value.identifier); } + if ( + value.kind === 'LoadLocal' && + value.place.identifier.scope != null && + instruction.lvalue != null && + instruction.lvalue.identifier.scope == null && + state.manualMemoState != null + ) { + // Simpler cases of inlining assign to the original IIFE lvalue + const ids = getOrInsertDefault( + state.manualMemoState.reassignments, + instruction.lvalue.identifier.declarationId, + new Set(), + ); + ids.add(value.place.identifier); + } if (value.kind === 'StartMemoize') { let depsFromSource: Array | null = null; if (value.deps != null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md index 933fafff5f1ba..12c7b4d5eab93 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md @@ -175,21 +175,14 @@ import { * and mutability. */ function Component(t0) { - const $ = _c(4); + const $ = _c(2); const { prop } = t0; let t1; if ($[0] !== prop) { const obj = shallowCopy(prop); const aliasedObj = identity(obj); - let t2; - if ($[2] !== obj) { - t2 = [obj.id]; - $[2] = obj; - $[3] = t2; - } else { - t2 = $[3]; - } - const id = t2; + + const id = [obj.id]; mutate(aliasedObj); setPropertyByKey(aliasedObj, "id", prop.id + 1); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md index 8d7b62fe8340f..62ea047e2541f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md @@ -26,20 +26,16 @@ import { c as _c } from "react/compiler-runtime"; import { getNull } from "shared-runtime"; function Component(props) { - const $ = _c(3); - let t0; + const $ = _c(2); let items; if ($[0] !== props.a) { - t0 = getNull() ?? []; - items = t0; + items = getNull() ?? []; items.push(props.a); $[0] = props.a; $[1] = items; - $[2] = t0; } else { items = $[1]; - t0 = $[2]; } return items; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md index a8d767831a9fe..44d4974f6f88d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-modify-global-in-callback-jsx.expect.md @@ -52,15 +52,13 @@ function Component(t0) { } const onClick = t1; let t2; - let t3; if ($[2] !== onClick) { - t3 =
{someGlobal.value}
; + t2 =
{someGlobal.value}
; $[2] = onClick; - $[3] = t3; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - t2 = t3; return t2; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md index 6ffd7fa1cdbc5..8b48145d91302 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-import.expect.md @@ -30,50 +30,46 @@ function Component(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } function Component2(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md index ec5d7ae51da4f..745da40e7c22e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-kitchensink-import.expect.md @@ -32,50 +32,46 @@ function Component(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } function Component2(props) { const $ = _c(4); const [x] = useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md index 161b3707f5c46..297b01229c20a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-namespace-import.expect.md @@ -30,25 +30,23 @@ function Component(props) { const $ = _c(4); const [x] = React.useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 =
{expensiveNumber}
; + t1 =
{expensiveNumber}
; $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md index 5bb87a2b032c2..ac4e7dc3a8c4d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/babel-existing-react-runtime-import.expect.md @@ -36,30 +36,28 @@ function Component(props) { const $ = _c(4); const [x] = React.useState(0); let t0; - let t1; if ($[0] !== x) { - t1 = calculateExpensiveNumber(x); + t0 = calculateExpensiveNumber(x); $[0] = x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const expensiveNumber = t0; - let t2; + let t1; if ($[2] !== expensiveNumber) { - t2 = ( + t1 = (
{expensiveNumber} {`${someImport}`}
); $[2] = expensiveNumber; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md index d5bf094ef4ff9..ce8e7b422329f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/block-scoping-switch-variable-scoping.expect.md @@ -36,26 +36,22 @@ import { useMemo } from "react"; function Component(props) { const $ = _c(2); let t0; - let t1; if ($[0] !== props.value) { - t1 = { value: props.value }; + t0 = { value: props.value }; $[0] = props.value; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - const handlers = t1; + const handlers = t0; bb0: switch (props.test) { case true: { console.log(handlers.value); break bb0; } - default: { - } + default: } - - t0 = handlers; - const outerHandlers = t0; + const outerHandlers = handlers; return outerHandlers; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md index d0ad9e2f9dbe5..7d14f2a5dc8e0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @flow @enableTransitivelyFreezeFunctionExpressions:false +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js index c46ecd6250b42..911c06e644661 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js @@ -1,4 +1,4 @@ -// @flow @enableTransitivelyFreezeFunctionExpressions:false +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md index c35efe6a16bb5..698562dad18d2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md @@ -2,7 +2,7 @@ ## Input ```javascript -// @flow @enableTransitivelyFreezeFunctionExpressions:false +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false import {setPropertyByKey, Stringify} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js index a7e57672665f6..1311a9dcfa69d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js @@ -1,4 +1,4 @@ -// @flow @enableTransitivelyFreezeFunctionExpressions:false +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false import {setPropertyByKey, Stringify} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md index b8c7f8d4225f7..ea33e361e3ba2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel:false import {makeArray, mutate} from 'shared-runtime'; /** @@ -56,7 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false import { makeArray, mutate } from "shared-runtime"; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts index ca7076fda4019..62d891febf41a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts @@ -1,3 +1,4 @@ +// @enableNewMutationAliasingModel:false import {makeArray, mutate} from 'shared-runtime'; /** 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 09d2d8800b789..9c874fa68ebc4 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,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel:false import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime'; /** @@ -38,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime"; /** 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 a1a78bfa7e6b1..1a7c996a9e292 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,3 +1,4 @@ +// @enableNewMutationAliasingModel:false import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md index 4ffe0fcb6a541..93098b916d720 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel:false import {identity, mutate} from 'shared-runtime'; /** @@ -39,7 +40,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false import { identity, mutate } from "shared-runtime"; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js index 94befbdd17b77..620f5eeb17af8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js @@ -1,3 +1,4 @@ +// @enableNewMutationAliasingModel:false import {identity, mutate} from 'shared-runtime'; /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md new file mode 100644 index 0000000000000..ccfc451750004 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.expect.md @@ -0,0 +1,132 @@ + +## Input + +```javascript +import {useRef, useEffect} from 'react'; + +/** + * The postfix increment operator should return the value before incrementing. + * ```js + * const id = count.current; // 0 + * count.current = count.current + 1; // 1 + * return id; + * ``` + * The bug is that we currently increment the value before the expression is evaluated. + * This bug does not trigger when the incremented value is a plain primitive. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 0','count = 1'] + * Forget: + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 1','count = 1'] + */ +function useFoo() { + const count = useRef(0); + const updateCountPostfix = () => { + const id = count.current++; + return id; + }; + const updateCountPrefix = () => { + const id = ++count.current; + return id; + }; + useEffect(() => { + const id = updateCountPostfix(); + console.log(`id = ${id}`); + console.log(`count = ${count.current}`); + }, []); + return {count, updateCountPostfix, updateCountPrefix}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef, useEffect } from "react"; + +/** + * The postfix increment operator should return the value before incrementing. + * ```js + * const id = count.current; // 0 + * count.current = count.current + 1; // 1 + * return id; + * ``` + * The bug is that we currently increment the value before the expression is evaluated. + * This bug does not trigger when the incremented value is a plain primitive. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 0','count = 1'] + * Forget: + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 1','count = 1'] + */ +function useFoo() { + const $ = _c(5); + const count = useRef(0); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + count.current = count.current + 1; + const id = count.current; + return id; + }; + $[0] = t0; + } else { + t0 = $[0]; + } + const updateCountPostfix = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + const id_0 = (count.current = count.current + 1); + return id_0; + }; + $[1] = t1; + } else { + t1 = $[1]; + } + const updateCountPrefix = t1; + let t2; + let t3; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t2 = () => { + const id_1 = updateCountPostfix(); + console.log(`id = ${id_1}`); + console.log(`count = ${count.current}`); + }; + t3 = []; + $[2] = t2; + $[3] = t3; + } else { + t2 = $[2]; + t3 = $[3]; + } + useEffect(t2, t3); + let t4; + if ($[4] === Symbol.for("react.memo_cache_sentinel")) { + t4 = { count, updateCountPostfix, updateCountPrefix }; + $[4] = t4; + } else { + t4 = $[4]; + } + return t4; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js new file mode 100644 index 0000000000000..a7c1fad8bf231 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-ref-prefix-postfix-operator.js @@ -0,0 +1,42 @@ +import {useRef, useEffect} from 'react'; + +/** + * The postfix increment operator should return the value before incrementing. + * ```js + * const id = count.current; // 0 + * count.current = count.current + 1; // 1 + * return id; + * ``` + * The bug is that we currently increment the value before the expression is evaluated. + * This bug does not trigger when the incremented value is a plain primitive. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 0','count = 1'] + * Forget: + * (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"} + * logs: ['id = 1','count = 1'] + */ +function useFoo() { + const count = useRef(0); + const updateCountPostfix = () => { + const id = count.current++; + return id; + }; + const updateCountPrefix = () => { + const id = ++count.current; + return id; + }; + useEffect(() => { + const id = updateCountPostfix(); + console.log(`id = ${id}`); + console.log(`count = ${count.current}`); + }, []); + return {count, updateCountPostfix, updateCountPrefix}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md new file mode 100644 index 0000000000000..7767989574c73 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md @@ -0,0 +1,138 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:false +import {ValidateMemoization} from 'shared-runtime'; + +const Codes = { + en: {name: 'English'}, + ja: {name: 'Japanese'}, + ko: {name: 'Korean'}, + zh: {name: 'Chinese'}, +}; + +function Component(a) { + let keys; + if (a) { + keys = Object.keys(Codes); + } else { + return null; + } + const options = keys.map(code => { + const country = Codes[code]; + return { + name: country.name, + code, + }; + }); + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: false}], + sequentialRenders: [ + {a: false}, + {a: true}, + {a: true}, + {a: false}, + {a: true}, + {a: false}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false +import { ValidateMemoization } from "shared-runtime"; + +const Codes = { + en: { name: "English" }, + ja: { name: "Japanese" }, + ko: { name: "Korean" }, + zh: { name: "Chinese" }, +}; + +function Component(a) { + const $ = _c(4); + let keys; + if (a) { + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = Object.keys(Codes); + $[0] = t0; + } else { + t0 = $[0]; + } + keys = t0; + } else { + return null; + } + let t0; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t0 = keys.map(_temp); + $[1] = t0; + } else { + t0 = $[1]; + } + const options = t0; + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ( + + ); + $[2] = t1; + } else { + t1 = $[2]; + } + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 = ( + <> + {t1} + + + ); + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp(code) { + const country = Codes[code]; + return { name: country.name, code }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: false }], + sequentialRenders: [ + { a: false }, + { a: true }, + { a: true }, + { a: false }, + { a: true }, + { a: false }, + ], +}; + +``` + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js new file mode 100644 index 0000000000000..c28ee705d1667 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js @@ -0,0 +1,48 @@ +// @enableNewMutationAliasingModel:false +import {ValidateMemoization} from 'shared-runtime'; + +const Codes = { + en: {name: 'English'}, + ja: {name: 'Japanese'}, + ko: {name: 'Korean'}, + zh: {name: 'Chinese'}, +}; + +function Component(a) { + let keys; + if (a) { + keys = Object.keys(Codes); + } else { + return null; + } + const options = keys.map(code => { + const country = Codes[code]; + return { + name: country.name, + code, + }; + }); + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: false}], + sequentialRenders: [ + {a: false}, + {a: true}, + {a: true}, + {a: false}, + {a: true}, + {a: false}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md index 2afc5fd25dbac..50480f1b2515e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-2-iife.expect.md @@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a) { - const $ = _c(2); - let y; + const $ = _c(4); + let t0; if ($[0] !== a) { - const x = [a]; + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0][1]) { y = {}; y = x[0][1]; - $[0] = a; - $[1] = y; + $[2] = x[0][1]; + $[3] = y; } else { - y = $[1]; + y = $[3]; } return y; } 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 f0267c3309f5b..9678918b3d27f 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 @@ -29,20 +29,29 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a, b) { - const $ = _c(3); - let y; + const $ = _c(6); + let t0; if ($[0] !== a || $[1] !== b) { - const x = [a, b]; + t0 = [a, b]; + $[0] = a; + $[1] = b; + $[2] = t0; + } else { + t0 = $[2]; + } + const x = t0; + let y; + if ($[3] !== x[0][1] || $[4] !== x[1][0]) { y = {}; let t = {}; y = x[0][1]; t = x[1][0]; - $[0] = a; - $[1] = b; - $[2] = y; + $[3] = x[0][1]; + $[4] = x[1][0]; + $[5] = y; } else { - y = $[2]; + y = $[5]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md index 22728aaf4323d..edddf3715a453 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-4-iife.expect.md @@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a) { - const $ = _c(2); - let y; + const $ = _c(4); + let t0; if ($[0] !== a) { - const x = [a]; + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0].a[1]) { y = {}; y = x[0].a[1]; - $[0] = a; - $[1] = y; + $[2] = x[0].a[1]; + $[3] = y; } else { - y = $[1]; + y = $[3]; } return y; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md index 60f829cdc4d66..c9ce6dda9f627 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-iife.expect.md @@ -24,17 +24,25 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function bar(a) { - const $ = _c(2); - let y; + const $ = _c(4); + let t0; if ($[0] !== a) { - const x = [a]; + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0]) { y = {}; y = x[0]; - $[0] = a; - $[1] = y; + $[2] = x[0]; + $[3] = y; } else { - y = $[1]; + y = $[3]; } return y; } 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 9c8fc0f1c5b67..b7288a854ffe3 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 @@ -37,11 +37,9 @@ function useTest() { const t1 = (w = 42); const t2 = w; - let t3; w = 999; - t3 = 2; - t0 = makeArray(t1, t2, t3); + t0 = makeArray(t1, t2, 2); $[0] = t0; } else { t0 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md index 58c54ddaab891..85a66bb204cbb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md @@ -37,11 +37,9 @@ function useTest() { const t1 = (w.x = 42); const t2 = w.x; - let t3; w.x = 999; - t3 = 2; - t0 = makeArray(t1, t2, t3); + t0 = makeArray(t1, t2, 2); $[0] = t0; } else { t0 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md index 25a08bc3329cb..70b23c70c093d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md @@ -32,11 +32,9 @@ function useTest() { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { const t1 = print(1); - let t2; print(2); - t2 = 2; - t0 = makeArray(t1, t2); + t0 = makeArray(t1, 2); $[0] = t0; } else { t0 = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md index 534093bdde10b..b09c51a8768d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/consecutive-use-memo.expect.md @@ -29,37 +29,33 @@ function useHook(t0) { const $ = _c(7); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = identity({ a }); + t1 = identity({ a }); $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const valA = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = identity([b]); + t2 = identity([b]); $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const valB = t3; - let t5; + const valB = t2; + let t3; if ($[4] !== valA || $[5] !== valB) { - t5 = [valA, valB]; + t3 = [valA, valB]; $[4] = valA; $[5] = valB; - $[6] = t5; + $[6] = t3; } else { - t5 = $[6]; + t3 = $[6]; } - return t5; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md index da3bb94ed5ed4..5b8824eca6b6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-as-jsx-element-tag.expect.md @@ -34,10 +34,8 @@ function Component(props) { let Component; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { Component = Stringify; - let t0; - t0 = Component; - Component = t0; + Component = Component; $[0] = Component; } else { Component = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md index 880c158b721d0..7d0a1ffed1fac 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/deeply-nested-function-expressions-with-params.expect.md @@ -28,20 +28,18 @@ import { c as _c } from "react/compiler-runtime"; function Foo() { const $ = _c(1); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = function a(t2) { - const x_0 = t2 === undefined ? _temp : t2; - return (function b(t3) { - const y_0 = t3 === undefined ? [] : t3; + t0 = function a(t1) { + const x_0 = t1 === undefined ? _temp : t1; + return (function b(t2) { + const y_0 = t2 === undefined ? [] : t2; return [x_0, y_0]; })(); }; - $[0] = t1; + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; return t0; } function _temp() {} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md index e878d4fb7f825..508a7b62581d7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/dominator.expect.md @@ -67,8 +67,7 @@ function Component(props) { case "b": { break bb1; } - case "c": { - } + case "c": default: { x = 6; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md index 89041ed0b1711..29d581df9c335 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/drop-methodcall-usememo.expect.md @@ -28,7 +28,6 @@ import * as React from "react"; function Component(props) { const $ = _c(2); - let t0; let x; if ($[0] !== props.value) { x = []; @@ -38,8 +37,7 @@ function Component(props) { } else { x = $[1]; } - t0 = x; - const x_0 = t0; + const x_0 = x; return x_0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md index 3861b16e90dcf..3f0b5530ee2d2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel:false function Component() { const foo = () => { someGlobal = true; @@ -15,13 +16,13 @@ function Component() { ## Error ``` - 1 | function Component() { - 2 | const foo = () => { -> 3 | someGlobal = true; - | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3) - 4 | }; - 5 | return
; - 6 | } + 2 | function Component() { + 3 | const foo = () => { +> 4 | someGlobal = true; + | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4) + 5 | }; + 6 | return
; + 7 | } ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js index 1eea9267b5098..e749f10f78fbd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js @@ -1,3 +1,4 @@ +// @enableNewMutationAliasingModel:false function Component() { const foo = () => { someGlobal = true; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md new file mode 100644 index 0000000000000..e1cebb00df56f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false + +import {useCallback, useEffect, useRef} from 'react'; +import {useHook} from 'shared-runtime'; + +function Component() { + const params = useHook(); + const update = useCallback( + partialParams => { + const nextParams = { + ...params, + ...partialParams, + }; + nextParams.param = 'value'; + console.log(nextParams); + }, + [params] + ); + const ref = useRef(null); + useEffect(() => { + if (ref.current === null) { + update(); + } + }, [update]); + + return 'ok'; +} + +``` + + +## Error + +``` + 18 | ); + 19 | const ref = useRef(null); +> 20 | useEffect(() => { + | ^^^^^^^ +> 21 | if (ref.current === null) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 22 | update(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 23 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 24 | }, [update]); + | ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24) + +InvalidReact: The function modifies a local variable here (14:14) + 25 | + 26 | return 'ok'; + 27 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js new file mode 100644 index 0000000000000..b5d70dbd81611 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js @@ -0,0 +1,27 @@ +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false + +import {useCallback, useEffect, useRef} from 'react'; +import {useHook} from 'shared-runtime'; + +function Component() { + const params = useHook(); + const update = useCallback( + partialParams => { + const nextParams = { + ...params, + ...partialParams, + }; + nextParams.param = 'value'; + console.log(nextParams); + }, + [params] + ); + const ref = useRef(null); + useEffect(() => { + if (ref.current === null) { + update(); + } + }, [update]); + + return 'ok'; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md index 7babe57b000e2..5e0a9886272bc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md @@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = { 11 | }); 12 | > 13 | x.value += count; - | ^ InvalidReact: This mutates a variable that React considers immutable (13:13) + | ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13) 14 | return ; 15 | } 16 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md index fcc47ddc2b14f..c5af59d642426 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md @@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = { 11 | }); 12 | > 13 | x.value += count; - | ^ InvalidReact: This mutates a variable that React considers immutable (13:13) + | ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (13:13) 14 | return ; 15 | } 16 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md similarity index 56% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md index 483d9b1a8e2da..1be37ef830989 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel import {useEffect, useState} from 'react'; import {Stringify} from 'shared-runtime'; @@ -33,45 +34,19 @@ export const FIXTURE_ENTRYPOINT = { ``` -## Code -```javascript -import { c as _c } from "react/compiler-runtime"; -import { useEffect, useState } from "react"; -import { Stringify } from "shared-runtime"; - -function Foo() { - const $ = _c(3); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = []; - $[0] = t0; - } else { - t0 = $[0]; - } - useEffect(() => setState(2), t0); - - const [state, t1] = useState(0); - const setState = t1; - let t2; - if ($[1] !== state) { - t2 = ; - $[1] = state; - $[2] = t2; - } else { - t2 = $[2]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{}], - sequentialRenders: [{}, {}], -}; +## Error ``` - -### Eval output -(kind: ok)
{"state":2}
-
{"state":2}
\ No newline at end of file + 17 | * $2 = Function context=setState + 18 | */ +> 19 | useEffect(() => setState(2), []); + | ^^^^^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `setState` is accessed before it is declared (19:19) + +InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `setState` is accessed before it is declared (21:21) + 20 | + 21 | const [state, setState] = useState(0); + 22 | return ; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.js similarity index 96% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.js index 7b26c8d086491..f3b4167772e9d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.js @@ -1,3 +1,4 @@ +// @enableNewMutationAliasingModel import {useEffect, useState} from 'react'; import {Stringify} from 'shared-runtime'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md index 86a9e14d80e8e..340c9570bb6cc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md @@ -24,7 +24,7 @@ function useFoo() { > 6 | cache.set('key', 'value'); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 7 | }); - | ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7) + | ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (5:7) InvalidReact: The function modifies a local variable here (6:6) 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md index a67d467df8cfd..0fb17a8f6eab4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-impure-functions-in-render.expect.md @@ -20,7 +20,7 @@ function Component() { 2 | 3 | function Component() { > 4 | const date = Date.now(); - | ^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4) + | ^^^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4) InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-captures-context-variable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-captures-context-variable.expect.md new file mode 100644 index 0000000000000..461b2b9e4511f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-captures-context-variable.expect.md @@ -0,0 +1,62 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +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'}, + ], +}; + +``` + + +## Error + +``` + 20 | /> + 21 | ); +> 22 | i = i + 1; + | ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. Found mutation of `i` (22:22) + 23 | items.push( + 24 | 11 | onPress={() => (sharedVal.value = Math.random())} - | ^^^^^^^^^ InvalidReact: Mutating a value returned from a function whose return value should not be mutated. Found mutation of `sharedVal` (11:11) + | ^^^^^^^^^ InvalidReact: Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed. Found mutation of `sharedVal` (11:11) 12 | title="Randomize" 13 | /> 14 | ); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md index 0d4742f26c604..85905b48dfa66 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md @@ -20,7 +20,7 @@ function Component() { 5 | cache.set('key', 'value'); 6 | }; > 7 | return ; - | ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7) + | ^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:7) InvalidReact: The function modifies a local variable here (5:5) 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md index 63a09bedaa0dd..d60433a315803 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md @@ -26,7 +26,7 @@ function useFoo() { > 8 | cache.set('key', 'value'); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 9 | }; - | ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9) + | ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:9) InvalidReact: The function modifies a local variable here (8:8) 10 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md new file mode 100644 index 0000000000000..734ba6f172841 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md @@ -0,0 +1,92 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel +/** + * This hook returns a function that when called with an input object, + * will return the result of mapping that input with the supplied map + * function. Results are cached, so if the same input is passed again, + * the same output object will be returned. + * + * Note that this technically violates the rules of React and is unsafe: + * hooks must return immutable objects and be pure, and a function which + * captures and mutates a value when called is inherently not pure. + * + * However, in this case it is technically safe _if_ the mapping function + * is pure *and* the resulting objects are never modified. This is because + * the function only caches: the result of `returnedFunction(someInput)` + * strictly depends on `returnedFunction` and `someInput`, and cannot + * otherwise change over time. + */ +hook useMemoMap( + map: TInput => TOutput +): TInput => TOutput { + return useMemo(() => { + // The original issue is that `cache` was not memoized together with the returned + // function. This was because neither appears to ever be mutated — the function + // is known to mutate `cache` but the function isn't called. + // + // The fix is to detect cases like this — functions that are mutable but not called - + // and ensure that their mutable captures are aliased together into the same scope. + const cache = new WeakMap(); + return input => { + let output = cache.get(input); + if (output == null) { + output = map(input); + cache.set(input, output); + } + return output; + }; + }, [map]); +} + +``` + + +## Error + +``` + 19 | map: TInput => TOutput + 20 | ): TInput => TOutput { +> 21 | return useMemo(() => { + | ^^^^^^^^^^^^^^^ +> 22 | // The original issue is that `cache` was not memoized together with the returned + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 23 | // function. This was because neither appears to ever be mutated — the function + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 24 | // is known to mutate `cache` but the function isn't called. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 25 | // + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 26 | // The fix is to detect cases like this — functions that are mutable but not called - + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 27 | // and ensure that their mutable captures are aliased together into the same scope. + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 28 | const cache = new WeakMap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 29 | return input => { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 30 | let output = cache.get(input); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 31 | if (output == null) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 32 | output = map(input); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 33 | cache.set(input, output); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 34 | } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 35 | return output; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 36 | }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 37 | }, [map]); + | ^^^^^^^^^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (21:37) + +InvalidReact: The function modifies a local variable here (33:33) + 38 | } + 39 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js similarity index 97% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js index bce92823e3385..accabed80fc4f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js @@ -1,4 +1,4 @@ -// @flow +// @flow @enableNewMutationAliasingModel /** * This hook returns a function that when called with an input object, * will return the result of mapping that input with the supplied map diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.expect.md index cdcd6b3ffad99..a6f2a2719fb1f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.expect.md @@ -18,7 +18,7 @@ function Component(props) { a.property = true; b.push(false); }; - return
; + return
; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.js index b975527138f3b..ac7299181ed5f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.js @@ -14,7 +14,7 @@ function Component(props) { a.property = true; b.push(false); }; - return
; + return
; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutate-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutate-hook-argument.expect.md index 665fc7053b788..dd98f22530713 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutate-hook-argument.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutate-hook-argument.expect.md @@ -16,6 +16,8 @@ function useHook(a, b) { 1 | function useHook(a, b) { > 2 | b.test = 1; | ^ InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (2:2) + +InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (3:3) 3 | a.test = 2; 4 | } 5 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.not-useEffect-external-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.not-useEffect-external-mutate.expect.md index 7d829fe9b013f..8c08efee8d894 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.not-useEffect-external-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.not-useEffect-external-mutate.expect.md @@ -21,6 +21,8 @@ function Component(props) { 4 | foo(() => { > 5 | x.a = 10; | ^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (5:5) + +InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (6:6) 6 | x.a = 20; 7 | }); 8 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md index 1ab2a46afe893..65292c65e9930 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +// @enableNewMutationAliasingModel:false function Foo() { const x = () => { window.href = 'foo'; @@ -21,13 +22,13 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` - 1 | function Foo() { - 2 | const x = () => { -> 3 | window.href = 'foo'; - | ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (3:3) - 4 | }; - 5 | const y = {x}; - 6 | return ; + 2 | function Foo() { + 3 | const x = () => { +> 4 | window.href = 'foo'; + | ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (4:4) + 5 | }; + 6 | const y = {x}; + 7 | return ; ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js index b3c936a2a284d..d95a0a6265cc8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js @@ -1,3 +1,4 @@ +// @enableNewMutationAliasingModel:false function Foo() { const x = () => { window.href = 'foo'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global-indirect.expect.md index e4073947f7e15..503ca7df7be46 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global-indirect.expect.md @@ -21,6 +21,8 @@ function Component() { 3 | // Cannot assign to globals > 4 | someUnknownGlobal = true; | ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4) + +InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (5:5) 5 | moduleLocal = true; 6 | }; 7 | foo(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global.expect.md index 4619cd27cb24d..ec764b7ff27e3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.reassignment-to-global.expect.md @@ -18,6 +18,8 @@ function Component() { 2 | // Cannot assign to globals > 3 | someUnknownGlobal = true; | ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3) + +InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4) 4 | moduleLocal = true; 5 | } 6 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md index f66b970f00dd4..8dd58594ecc64 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md @@ -22,7 +22,7 @@ function Component(props) { 7 | return hasErrors; 8 | } > 9 | return hasErrors(); - | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9) + | ^^^^^^^^^ Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized. hasErrors_0$15:TFunction (9:9) 10 | } 11 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md deleted file mode 100644 index b24f1f18f0d30..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.expect.md +++ /dev/null @@ -1,32 +0,0 @@ - -## Input - -```javascript -function Component(props) { - return ( - useMemo(() => { - return [props.value]; - }) || [] - ); -} - -``` - - -## Error - -``` - 1 | function Component(props) { - 2 | return ( -> 3 | useMemo(() => { - | ^^^^^^^^^^^^^^^ -> 4 | return [props.value]; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -> 5 | }) || [] - | ^^^^^^^^^^^^^ Todo: Support labeled statements combined with value blocks (conditional, logical, optional chaining, etc) (3:5) - 6 | ); - 7 | } - 8 | -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js deleted file mode 100644 index 533e2c370fe34..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-useMemo-with-optional.js +++ /dev/null @@ -1,7 +0,0 @@ -function Component(props) { - return ( - useMemo(() => { - return [props.value]; - }) || [] - ); -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-valid-functiondecl-hoisting.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-valid-functiondecl-hoisting.expect.md index fb8988c8f878d..e057305d05e70 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-valid-functiondecl-hoisting.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-valid-functiondecl-hoisting.expect.md @@ -34,13 +34,13 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` - 13 | return bar(); + 11 | + 12 | function foo() { +> 13 | return bar(); + | ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (13:13) 14 | } -> 15 | function bar() { - | ^^^ Todo: [PruneHoistedContexts] Rewrite hoisted function references (15:15) + 15 | function bar() { 16 | return 42; - 17 | } - 18 | ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/existing-variables-with-c-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/existing-variables-with-c-name.expect.md index 5cde3bde23cf1..9f340f19ab963 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/existing-variables-with-c-name.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/existing-variables-with-c-name.expect.md @@ -42,34 +42,32 @@ function Component(props) { const c1 = __c; const $c = c1; let t0; - let t1; if ($[0] !== $c) { - t1 = [$c]; + t0 = [$c]; $[0] = $c; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const array = t0; - let t2; + let t1; if ($[2] !== state) { - t2 = [state]; + t1 = [state]; $[2] = state; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== array || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== array || $[5] !== t1) { + t2 = ; $[4] = array; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md index df2f4b01a9dc7..3c3b3e32d05ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-dont-refresh-const-changes-prod.expect.md @@ -63,23 +63,21 @@ function Component() { unsafeUpdateConst(); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = [{ pretendConst }]; - $[0] = t1; + t0 = [{ pretendConst }]; + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; const value = t0; - let t2; + let t1; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t2 = ; - $[1] = t2; + t1 = ; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - return t2; + return t1; } function _temp() { unsafeResetConst(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md index 56a7289ae8d13..1524de192d79a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-refresh-on-const-changes-dev.expect.md @@ -74,23 +74,21 @@ function Component() { unsafeUpdateConst(); let t0; - let t1; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = [{ pretendConst }]; - $[1] = t1; + t0 = [{ pretendConst }]; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const value = t0; - let t2; + let t1; if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t2 = ; - $[2] = t2; + t1 = ; + $[2] = t1; } else { - t2 = $[2]; + t1 = $[2]; } - return t2; + return t1; } function _temp() { unsafeResetConst(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.expect.md index 4175d23fdab57..136c19e62d0c7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fast-refresh-reloading.expect.md @@ -38,36 +38,34 @@ function Component(props) { $[0] = "20945b0193e529df490847c66111b38d7b02485d5b53d0829ff3b23af87b105c"; } const [state] = useState(0); - let t0; - const t1 = state * 2; - let t2; - if ($[1] !== t1) { - t2 = [t1]; - $[1] = t1; - $[2] = t2; + const t0 = state * 2; + let t1; + if ($[1] !== t0) { + t1 = [t0]; + $[1] = t0; + $[2] = t1; } else { - t2 = $[2]; + t1 = $[2]; } - t0 = t2; - const doubled = t0; - let t3; + const doubled = t1; + let t2; if ($[3] !== state) { - t3 = [state]; + t2 = [state]; $[3] = state; - $[4] = t3; + $[4] = t2; } else { - t3 = $[4]; + t2 = $[4]; } - let t4; - if ($[5] !== doubled || $[6] !== t3) { - t4 = ; + let t3; + if ($[5] !== doubled || $[6] !== t2) { + t3 = ; $[5] = doubled; - $[6] = t3; - $[7] = t4; + $[6] = t2; + $[7] = t3; } else { - t4 = $[7]; + t3 = $[7]; } - return t4; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-repro-invalid-mutable-range-destructured-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-repro-invalid-mutable-range-destructured-prop.expect.md index 9bb651aa676e6..4f81700bbb005 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-repro-invalid-mutable-range-destructured-prop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/fbt/fbt-repro-invalid-mutable-range-destructured-prop.expect.md @@ -40,36 +40,34 @@ function Component(t0) { const $ = _c(7); const { data } = t0; let t1; - let t2; if ($[0] !== data.name) { - t2 = fbt._("{name}", [fbt._param("name", data.name ?? "")], { + t1 = fbt._("{name}", [fbt._param("name", data.name ?? "")], { hk: "csQUH", }); $[0] = data.name; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const el = t1; - let t3; + let t2; if ($[2] !== data.name) { - t3 = [data.name]; + t2 = [data.name]; $[2] = data.name; - $[3] = t3; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - let t4; - if ($[4] !== el || $[5] !== t3) { - t4 = ; + let t3; + if ($[4] !== el || $[5] !== t2) { + t3 = ; $[4] = el; - $[5] = t3; - $[6] = t4; + $[5] = t2; + $[6] = t3; } else { - t4 = $[6]; + t3 = $[6]; } - return t4; + return t3; } const props1 = { data: { name: "Mike" } }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-of-nonmutating-loop-local-collection.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-of-nonmutating-loop-local-collection.expect.md index 4abe630044084..6d05e5e5ae30b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-of-nonmutating-loop-local-collection.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-of-nonmutating-loop-local-collection.expect.md @@ -47,17 +47,14 @@ function Component(t0) { const $ = _c(19); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = [a]; + t1 = [a]; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const x = t1; - let t3; let items; if ($[2] !== b || $[3] !== x) { items = [b]; @@ -70,59 +67,57 @@ function Component(t0) { } else { items = $[4]; } - - t3 = items; - const y = t3; - let t4; + const y = items; + let t2; if ($[5] !== a) { - t4 = [a]; + t2 = [a]; $[5] = a; - $[6] = t4; + $[6] = t2; } else { - t4 = $[6]; + t2 = $[6]; } - let t5; - if ($[7] !== t4 || $[8] !== x) { - t5 = ; - $[7] = t4; + let t3; + if ($[7] !== t2 || $[8] !== x) { + t3 = ; + $[7] = t2; $[8] = x; - $[9] = t5; + $[9] = t3; } else { - t5 = $[9]; + t3 = $[9]; } - let t6; + let t4; if ($[10] !== b || $[11] !== x) { - t6 = [x, b]; + t4 = [x, b]; $[10] = b; $[11] = x; - $[12] = t6; + $[12] = t4; } else { - t6 = $[12]; + t4 = $[12]; } - let t7; - if ($[13] !== t6 || $[14] !== y) { - t7 = ; - $[13] = t6; + let t5; + if ($[13] !== t4 || $[14] !== y) { + t5 = ; + $[13] = t4; $[14] = y; - $[15] = t7; + $[15] = t5; } else { - t7 = $[15]; + t5 = $[15]; } - let t8; - if ($[16] !== t5 || $[17] !== t7) { - t8 = ( + let t6; + if ($[16] !== t3 || $[17] !== t5) { + t6 = ( <> + {t3} {t5} - {t7} ); - $[16] = t5; - $[17] = t7; - $[18] = t8; + $[16] = t3; + $[17] = t5; + $[18] = t6; } else { - t8 = $[18]; + t6 = $[18]; } - return t8; + return t6; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/function-expression-prototype-call-mutating.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/function-expression-prototype-call-mutating.expect.md index 9888e96222b3d..4c03adc0e38b0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/function-expression-prototype-call-mutating.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/function-expression-prototype-call-mutating.expect.md @@ -34,7 +34,6 @@ import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(7); - let t0; let a; if ($[0] !== props.name) { a = []; @@ -48,26 +47,25 @@ function Component(props) { } else { a = $[1]; } - t0 = a; - const a_0 = t0; - let t1; + const a_0 = a; + let t0; if ($[2] !== props.name) { - t1 = [props.name]; + t0 = [props.name]; $[2] = props.name; - $[3] = t1; + $[3] = t0; } else { - t1 = $[3]; + t0 = $[3]; } - let t2; - if ($[4] !== a_0 || $[5] !== t1) { - t2 = ; + let t1; + if ($[4] !== a_0 || $[5] !== t0) { + t1 = ; $[4] = a_0; - $[5] = t1; - $[6] = t2; + $[5] = t0; + $[6] = t1; } else { - t2 = $[6]; + t1 = $[6]; } - return t2; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hooks-with-prefix.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hooks-with-prefix.expect.md index 085df625f5544..6b95dda4739eb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hooks-with-prefix.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hooks-with-prefix.expect.md @@ -46,7 +46,7 @@ const React$useMemo = React.useMemo; const Internal$Reassigned$useHook = useHook; function Component() { - const $ = _c(8); + const $ = _c(7); const [state] = React$useState(0); const object = Internal$Reassigned$useHook(); let t0; @@ -59,34 +59,30 @@ function Component() { } const json = t0; let t1; - let t2; if ($[2] !== state) { - t1 = makeArray(state); - const doubledArray = t1; + const doubledArray = makeArray(state); - t2 = doubledArray.join(""); + t1 = doubledArray.join(""); $[2] = state; - $[3] = t2; - $[4] = t1; + $[3] = t1; } else { - t2 = $[3]; - t1 = $[4]; + t1 = $[3]; } - let t3; - if ($[5] !== json || $[6] !== t2) { - t3 = ( + let t2; + if ($[4] !== json || $[5] !== t1) { + t2 = (
- {t2} + {t1} {json}
); - $[5] = json; + $[4] = json; + $[5] = t1; $[6] = t2; - $[7] = t3; } else { - t3 = $[7]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md new file mode 100644 index 0000000000000..bf4dddc6fc3fb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +function Component(props) { + const x = props.foo + ? 1 + : (() => { + throw new Error('Did not receive 1'); + })(); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: true}], +}; + +``` + +## Code + +```javascript +function Component(props) { + props.foo ? 1 : _temp(); + return items; +} +function _temp() { + throw new Error("Did not receive 1"); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: true }], +}; + +``` + +### Eval output +(kind: exception) items is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js new file mode 100644 index 0000000000000..56d20f7f9ff97 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-inline-ternary.js @@ -0,0 +1,13 @@ +function Component(props) { + const x = props.foo + ? 1 + : (() => { + throw new Error('Did not receive 1'); + })(); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: true}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later.expect.md index 8420b02d7b55e..76fc6e86aba85 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later.expect.md @@ -22,20 +22,16 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(3); - let t0; + const $ = _c(2); let items; if ($[0] !== props.a) { - t0 = []; - items = t0; + items = []; items.push(props.a); $[0] = props.a; $[1] = items; - $[2] = t0; } else { items = $[1]; - t0 = $[2]; } return items; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-optional-chain.expect.md index e4560848dd5f2..b9aafe41df26e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-optional-chain.expect.md @@ -48,7 +48,7 @@ export const FIXTURE_ENTRYPOINT = { ## Logs ``` -{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}} +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}}} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":304},"end":{"line":9,"column":39,"index":341},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":326},"end":{"line":9,"column":27,"index":329},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]} {"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md index 5e6f19dd83e65..0d159211a9082 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md @@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = { ## Logs ``` -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"},"suggestions":null,"severity":"InvalidReact"}} +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"}}} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":227},"end":{"line":8,"column":40,"index":265},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":249},"end":{"line":8,"column":30,"index":255},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]} {"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect.expect.md index 3b61fbf834246..d48d98a1e67f7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect.expect.md @@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = { ## Logs ``` -{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}} +{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"}}} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":149},"end":{"line":8,"column":4,"index":190},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":180},"end":{"line":7,"column":16,"index":183},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]} {"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md index bd70c0138d7e2..3e559339d0632 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/no-emit/retry-no-emit.expect.md @@ -52,7 +52,7 @@ export const FIXTURE_ENTRYPOINT = { ## Logs ``` -{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"},"suggestions":null,"severity":"InvalidReact"}} +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"}}} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":216},"end":{"line":7,"column":36,"index":250},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":245},"end":{"line":7,"column":34,"index":248},"filename":"retry-no-emit.ts","identifierName":"arr"}]} {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":274},"end":{"line":10,"column":44,"index":316},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":307},"end":{"line":10,"column":42,"index":314},"filename":"retry-no-emit.ts","identifierName":"propVal"}]} {"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-effect-event.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-effect-event.expect.md new file mode 100644 index 0000000000000..56c5f6f6f8807 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-effect-event.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @inferEffectDependencies +import {useEffect, useEffectEvent} from 'react'; +import {print} from 'shared-runtime'; + +/** + * We do not include effect events in dep arrays. + */ +function NonReactiveEffectEvent() { + const fn = useEffectEvent(() => print('hello world')); + useEffect(() => fn()); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies +import { useEffect, useEffectEvent } from "react"; +import { print } from "shared-runtime"; + +/** + * We do not include effect events in dep arrays. + */ +function NonReactiveEffectEvent() { + const $ = _c(2); + const fn = useEffectEvent(_temp); + let t0; + if ($[0] !== fn) { + t0 = () => fn(); + $[0] = fn; + $[1] = t0; + } else { + t0 = $[1]; + } + useEffect(t0, []); +} +function _temp() { + return print("hello world"); +} + +``` + +### 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/infer-effect-dependencies/nonreactive-effect-event.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-effect-event.js new file mode 100644 index 0000000000000..02706c6b23735 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-effect-event.js @@ -0,0 +1,11 @@ +// @inferEffectDependencies +import {useEffect, useEffectEvent} from 'react'; +import {print} from 'shared-runtime'; + +/** + * We do not include effect events in dep arrays. + */ +function NonReactiveEffectEvent() { + const fn = useEffectEvent(() => print('hello world')); + useEffect(() => fn()); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-setState.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-setState.expect.md index 3af2b9b8b1c89..2329aaed21ad9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-setState.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/reactive-setState.expect.md @@ -34,22 +34,28 @@ import { print } from "shared-runtime"; * setState types are not enough to determine to omit from deps. Must also take reactivity into account. */ function ReactiveRefInEffect(props) { - const $ = _c(2); + const $ = _c(4); const [, setState1] = useRef("initial value"); const [, setState2] = useRef("initial value"); let setState; - if (props.foo) { - setState = setState1; + if ($[0] !== props.foo) { + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + $[0] = props.foo; + $[1] = setState; } else { - setState = setState2; + setState = $[1]; } let t0; - if ($[0] !== setState) { + if ($[2] !== setState) { t0 = () => print(setState); - $[0] = setState; - $[1] = t0; + $[2] = setState; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } useEffect(t0, [setState]); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/use-memo-returned.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/use-memo-returned.expect.md index e750b8ab8415e..32d454712f39b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/use-memo-returned.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/inner-function/nullable-objects/assume-invoked/use-memo-returned.expect.md @@ -50,19 +50,17 @@ function useMakeCallback(t0) { const [, setState] = useState(0); let t1; - let t2; if ($[0] !== obj.value) { - t2 = () => { + t1 = () => { if (obj.value !== 0) { setState(obj.value); } }; $[0] = obj.value; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const cb = t1; useIdentity(null); 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 deleted file mode 100644 index c1a9ad205c87e..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md +++ /dev/null @@ -1,129 +0,0 @@ - -## 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/nested-function-with-param-as-captured-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-with-param-as-captured-dep.expect.md index 7b18dd5e76bbf..2ab19c3f2c53e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-with-param-as-captured-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-with-param-as-captured-dep.expect.md @@ -26,17 +26,15 @@ import { c as _c } from "react/compiler-runtime"; function Foo() { const $ = _c(1); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = function a(t2) { - const x_0 = t2 === undefined ? _temp : t2; + t0 = function a(t1) { + const x_0 = t1 === undefined ? _temp : t1; return x_0; }; - $[0] = t1; + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; return t0; } function _temp() {} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.expect.md new file mode 100644 index 0000000000000..8024676c65a32 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.expect.md @@ -0,0 +1,215 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import { + Stringify, + mutate, + identity, + shallowCopy, + setPropertyByKey, +} from 'shared-runtime'; + +/** + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on + * dependency extraction. + * + * NOTE: this fixture is currently valid, but will break with optimizations: + * - Scope and mutable-range based reordering may move the array creation + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate + * reassigns inner properties. + * - RecycleInto or other deeper-equality optimizations may produce invalid + * output -- it may compare the array's contents / dependencies too early. + * - Runtime validation for immutable values will break if `mutate` does + * interior mutation of the value captured into the array. + * + * Before scope block creation, HIR looks like this: + * // + * // $1 is unscoped as obj's mutable range will be + * // extended in a later pass + * // + * $1 = LoadLocal obj@0[0:12] + * $2 = PropertyLoad $1.id + * // + * // $3 gets assigned a scope as Array is an allocating + * // instruction, but this does *not* get extended or + * // merged into the later mutation site. + * // (explained in `bug-aliased-capture-aliased-mutate`) + * // + * $3@1 = Array[$2] + * ... + * $10@0 = LoadLocal shallowCopy@0[0, 12] + * $11 = LoadGlobal mutate + * $12 = $11($10@0[0, 12]) + * + * When filling in scope dependencies, we find that it's incorrect to depend on + * PropertyLoads from obj as it hasn't completed its mutable range. Following + * the immutable / mutable-new typing system, we check the identity of obj to + * detect whether it was newly created (and thus mutable) in this render pass. + * + * HIR with scopes looks like this. + * bb0: + * $1 = LoadLocal obj@0[0:12] + * $2 = PropertyLoad $1.id + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2 + * bb1: + * $3@1 = Array[$2] + * goto bb2 + * bb2: + * ... + * + * This is surprising as deps now is entirely decoupled from temporaries used + * by the block itself. scope @1's instructions now reference a value (1) + * produced outside its scope range and (2) not represented in its dependencies + * + * The right thing to do is to ensure that all Loads from a value get assigned + * the value's reactive scope. This also requires track mutating and aliasing + * separately from scope range. In this example, that would correctly merge + * the scopes of $3 with obj. + * Runtime validation and optimizations such as ReactiveGraph-based reordering + * require this as well. + * + * A tempting fix is to instead extend $3's ReactiveScope range up to include + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering + * and mutability. + */ +function Component({prop}) { + let obj = shallowCopy(prop); + const aliasedObj = identity(obj); + + // [obj.id] currently is assigned its own reactive scope + const id = [obj.id]; + + // Writing to the alias may reassign to previously captured references. + // The compiler currently produces valid output, but this breaks with + // reordering, recycleInto, and other potential optimizations. + mutate(aliasedObj); + setPropertyByKey(aliasedObj, 'id', prop.id + 1); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop: {id: 1}}], + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { + Stringify, + mutate, + identity, + shallowCopy, + setPropertyByKey, +} from "shared-runtime"; + +/** + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on + * dependency extraction. + * + * NOTE: this fixture is currently valid, but will break with optimizations: + * - Scope and mutable-range based reordering may move the array creation + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate + * reassigns inner properties. + * - RecycleInto or other deeper-equality optimizations may produce invalid + * output -- it may compare the array's contents / dependencies too early. + * - Runtime validation for immutable values will break if `mutate` does + * interior mutation of the value captured into the array. + * + * Before scope block creation, HIR looks like this: + * // + * // $1 is unscoped as obj's mutable range will be + * // extended in a later pass + * // + * $1 = LoadLocal obj@0[0:12] + * $2 = PropertyLoad $1.id + * // + * // $3 gets assigned a scope as Array is an allocating + * // instruction, but this does *not* get extended or + * // merged into the later mutation site. + * // (explained in `bug-aliased-capture-aliased-mutate`) + * // + * $3@1 = Array[$2] + * ... + * $10@0 = LoadLocal shallowCopy@0[0, 12] + * $11 = LoadGlobal mutate + * $12 = $11($10@0[0, 12]) + * + * When filling in scope dependencies, we find that it's incorrect to depend on + * PropertyLoads from obj as it hasn't completed its mutable range. Following + * the immutable / mutable-new typing system, we check the identity of obj to + * detect whether it was newly created (and thus mutable) in this render pass. + * + * HIR with scopes looks like this. + * bb0: + * $1 = LoadLocal obj@0[0:12] + * $2 = PropertyLoad $1.id + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2 + * bb1: + * $3@1 = Array[$2] + * goto bb2 + * bb2: + * ... + * + * This is surprising as deps now is entirely decoupled from temporaries used + * by the block itself. scope @1's instructions now reference a value (1) + * produced outside its scope range and (2) not represented in its dependencies + * + * The right thing to do is to ensure that all Loads from a value get assigned + * the value's reactive scope. This also requires track mutating and aliasing + * separately from scope range. In this example, that would correctly merge + * the scopes of $3 with obj. + * Runtime validation and optimizations such as ReactiveGraph-based reordering + * require this as well. + * + * A tempting fix is to instead extend $3's ReactiveScope range up to include + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering + * and mutability. + */ +function Component(t0) { + const $ = _c(2); + const { prop } = t0; + let t1; + if ($[0] !== prop) { + const obj = shallowCopy(prop); + const aliasedObj = identity(obj); + + const id = [obj.id]; + + mutate(aliasedObj); + setPropertyByKey(aliasedObj, "id", prop.id + 1); + + t1 = ; + $[0] = prop; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ prop: { id: 1 } }], + sequentialRenders: [ + { prop: { id: 1 } }, + { prop: { id: 1 } }, + { prop: { id: 2 } }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"id":[1]}
+
{"id":[1]}
+
{"id":[2]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.tsx new file mode 100644 index 0000000000000..ecd5598cb0913 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.tsx @@ -0,0 +1,94 @@ +// @enableNewMutationAliasingModel +import { + Stringify, + mutate, + identity, + shallowCopy, + setPropertyByKey, +} from 'shared-runtime'; + +/** + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on + * dependency extraction. + * + * NOTE: this fixture is currently valid, but will break with optimizations: + * - Scope and mutable-range based reordering may move the array creation + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate + * reassigns inner properties. + * - RecycleInto or other deeper-equality optimizations may produce invalid + * output -- it may compare the array's contents / dependencies too early. + * - Runtime validation for immutable values will break if `mutate` does + * interior mutation of the value captured into the array. + * + * Before scope block creation, HIR looks like this: + * // + * // $1 is unscoped as obj's mutable range will be + * // extended in a later pass + * // + * $1 = LoadLocal obj@0[0:12] + * $2 = PropertyLoad $1.id + * // + * // $3 gets assigned a scope as Array is an allocating + * // instruction, but this does *not* get extended or + * // merged into the later mutation site. + * // (explained in `bug-aliased-capture-aliased-mutate`) + * // + * $3@1 = Array[$2] + * ... + * $10@0 = LoadLocal shallowCopy@0[0, 12] + * $11 = LoadGlobal mutate + * $12 = $11($10@0[0, 12]) + * + * When filling in scope dependencies, we find that it's incorrect to depend on + * PropertyLoads from obj as it hasn't completed its mutable range. Following + * the immutable / mutable-new typing system, we check the identity of obj to + * detect whether it was newly created (and thus mutable) in this render pass. + * + * HIR with scopes looks like this. + * bb0: + * $1 = LoadLocal obj@0[0:12] + * $2 = PropertyLoad $1.id + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2 + * bb1: + * $3@1 = Array[$2] + * goto bb2 + * bb2: + * ... + * + * This is surprising as deps now is entirely decoupled from temporaries used + * by the block itself. scope @1's instructions now reference a value (1) + * produced outside its scope range and (2) not represented in its dependencies + * + * The right thing to do is to ensure that all Loads from a value get assigned + * the value's reactive scope. This also requires track mutating and aliasing + * separately from scope range. In this example, that would correctly merge + * the scopes of $3 with obj. + * Runtime validation and optimizations such as ReactiveGraph-based reordering + * require this as well. + * + * A tempting fix is to instead extend $3's ReactiveScope range up to include + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering + * and mutability. + */ +function Component({prop}) { + let obj = shallowCopy(prop); + const aliasedObj = identity(obj); + + // [obj.id] currently is assigned its own reactive scope + const id = [obj.id]; + + // Writing to the alias may reassign to previously captured references. + // The compiler currently produces valid output, but this breaks with + // reordering, recycleInto, and other potential optimizations. + mutate(aliasedObj); + setPropertyByKey(aliasedObj, 'id', prop.id + 1); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop: {id: 1}}], + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.expect.md new file mode 100644 index 0000000000000..b3531c225d9ea --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.expect.md @@ -0,0 +1,93 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.filter(Boolean); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(13); + const { value } = t0; + let t1; + let t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = { value: "foo" }; + t2 = { value: "bar" }; + $[0] = t1; + $[1] = t2; + } else { + t1 = $[0]; + t2 = $[1]; + } + let t3; + if ($[2] !== value) { + t3 = [t1, t2, { value }]; + $[2] = value; + $[3] = t3; + } else { + t3 = $[3]; + } + const arr = t3; + useIdentity(null); + let t4; + if ($[4] !== arr) { + t4 = arr.filter(Boolean); + $[4] = arr; + $[5] = t4; + } else { + t4 = $[5]; + } + const derived = t4; + let t5; + if ($[6] !== derived) { + t5 = derived.at(0); + $[6] = derived; + $[7] = t5; + } else { + t5 = $[7]; + } + let t6; + if ($[8] !== derived) { + t6 = derived.at(-1); + $[8] = derived; + $[9] = t6; + } else { + t6 = $[9]; + } + let t7; + if ($[10] !== t5 || $[11] !== t6) { + t7 = ( + + {t5} + {t6} + + ); + $[10] = t5; + $[11] = t6; + $[12] = t7; + } else { + t7 = $[12]; + } + return t7; +} + +``` + +### 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/new-mutability/array-filter.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.js new file mode 100644 index 0000000000000..3229088e1da90 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.js @@ -0,0 +1,12 @@ +// @enableNewMutationAliasingModel +function Component({value}) { + const arr = [{value: 'foo'}, {value: 'bar'}, {value}]; + useIdentity(null); + const derived = arr.filter(Boolean); + return ( + + {derived.at(0)} + {derived.at(-1)} + + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.expect.md new file mode 100644 index 0000000000000..e687c995d077f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component(props) { + // This item is part of the receiver, should be memoized + const item = {a: props.a}; + const items = [item]; + const mapped = items.map(item => item); + // mapped[0].a = null; + return mapped; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: {id: 42}}], + isComponent: false, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(props) { + const $ = _c(6); + let t0; + if ($[0] !== props.a) { + t0 = { a: props.a }; + $[0] = props.a; + $[1] = t0; + } else { + t0 = $[1]; + } + const item = t0; + let t1; + if ($[2] !== item) { + t1 = [item]; + $[2] = item; + $[3] = t1; + } else { + t1 = $[3]; + } + const items = t1; + let t2; + if ($[4] !== items) { + t2 = items.map(_temp); + $[4] = items; + $[5] = t2; + } else { + t2 = $[5]; + } + const mapped = t2; + return mapped; +} +function _temp(item_0) { + return item_0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: { id: 42 } }], + isComponent: false, +}; + +``` + +### Eval output +(kind: ok) [{"a":{"id":42}}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.js new file mode 100644 index 0000000000000..42e32b3e38b3f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.js @@ -0,0 +1,15 @@ +// @enableNewMutationAliasingModel +function Component(props) { + // This item is part of the receiver, should be memoized + const item = {a: props.a}; + const items = [item]; + const mapped = items.map(item => item); + // mapped[0].a = null; + return mapped; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: {id: 42}}], + isComponent: false, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md new file mode 100644 index 0000000000000..7bc2c193cf7cc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md @@ -0,0 +1,134 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +/** + * Forked from array-map-simple.js + * + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and + * used in a different lambda (getArrMap1). + * + * Here, we should try to determine if cb1 is actually called. In this case: + * - getArrMap1 is assumed to be called as it's passed to JSX + * - cb1 is not assumed to be called since it's only used as a call operand + */ +function useFoo({arr1, arr2}) { + const cb1 = e => arr1[0].value + e.value; + const getArrMap1 = () => arr1.map(cb1); + const cb2 = e => arr2[0].value + e.value; + const getArrMap2 = () => arr1.map(cb2); + return ( + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{arr1: [], arr2: []}], + sequentialRenders: [ + {arr1: [], arr2: []}, + {arr1: [], arr2: null}, + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +/** + * Forked from array-map-simple.js + * + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and + * used in a different lambda (getArrMap1). + * + * Here, we should try to determine if cb1 is actually called. In this case: + * - getArrMap1 is assumed to be called as it's passed to JSX + * - cb1 is not assumed to be called since it's only used as a call operand + */ +function useFoo(t0) { + const $ = _c(13); + const { arr1, arr2 } = t0; + let t1; + if ($[0] !== arr1[0]) { + t1 = (e) => arr1[0].value + e.value; + $[0] = arr1[0]; + $[1] = t1; + } else { + t1 = $[1]; + } + const cb1 = t1; + let t2; + if ($[2] !== arr1 || $[3] !== cb1) { + t2 = () => arr1.map(cb1); + $[2] = arr1; + $[3] = cb1; + $[4] = t2; + } else { + t2 = $[4]; + } + const getArrMap1 = t2; + let t3; + if ($[5] !== arr2) { + t3 = (e_0) => arr2[0].value + e_0.value; + $[5] = arr2; + $[6] = t3; + } else { + t3 = $[6]; + } + const cb2 = t3; + let t4; + if ($[7] !== arr1 || $[8] !== cb2) { + t4 = () => arr1.map(cb2); + $[7] = arr1; + $[8] = cb2; + $[9] = t4; + } else { + t4 = $[9]; + } + const getArrMap2 = t4; + let t5; + if ($[10] !== getArrMap1 || $[11] !== getArrMap2) { + t5 = ( + + ); + $[10] = getArrMap1; + $[11] = getArrMap2; + $[12] = t5; + } else { + t5 = $[12]; + } + return t5; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ arr1: [], arr2: [] }], + sequentialRenders: [ + { arr1: [], arr2: [] }, + { arr1: [], arr2: null }, + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}
+
{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}
+
{"getArrMap1":{"kind":"Function","result":[2,3]},"getArrMap2":{"kind":"Function","result":[0,1]},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.js new file mode 100644 index 0000000000000..faa34747da188 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.js @@ -0,0 +1,36 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +/** + * Forked from array-map-simple.js + * + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and + * used in a different lambda (getArrMap1). + * + * Here, we should try to determine if cb1 is actually called. In this case: + * - getArrMap1 is assumed to be called as it's passed to JSX + * - cb1 is not assumed to be called since it's only used as a call operand + */ +function useFoo({arr1, arr2}) { + const cb1 = e => arr1[0].value + e.value; + const getArrMap1 = () => arr1.map(cb1); + const cb2 = e => arr2[0].value + e.value; + const getArrMap2 = () => arr1.map(cb2); + return ( + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{arr1: [], arr2: []}], + sequentialRenders: [ + {arr1: [], arr2: []}, + {arr1: [], arr2: null}, + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.expect.md new file mode 100644 index 0000000000000..b2564a7a90d1b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b, c}) { + const x = []; + x.push(a); + const merged = {b}; // could be mutated by mutate(x) below + x.push(merged); + mutate(x); + const independent = {c}; // can't be later mutated + x.push(independent); + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(6); + const { a, b, c } = t0; + let t1; + if ($[0] !== a || $[1] !== b || $[2] !== c) { + const x = []; + x.push(a); + const merged = { b }; + x.push(merged); + mutate(x); + let t2; + if ($[4] !== c) { + t2 = { c }; + $[4] = c; + $[5] = t2; + } else { + t2 = $[5]; + } + const independent = t2; + x.push(independent); + t1 = ; + $[0] = a; + $[1] = b; + $[2] = c; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +``` + +### 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/new-mutability/array-push.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.js new file mode 100644 index 0000000000000..eb7f31bff631e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.js @@ -0,0 +1,11 @@ +// @enableNewMutationAliasingModel +function Component({a, b, c}) { + const x = []; + x.push(a); + const merged = {b}; // could be mutated by mutate(x) below + x.push(merged); + mutate(x); + const independent = {c}; // can't be later mutated + x.push(independent); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.expect.md new file mode 100644 index 0000000000000..8b767931a8949 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + const y = [b]; + const f = () => { + y.x = x; + mutate(y); + }; + f(); + return
{x}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a }; + const y = [b]; + const f = () => { + y.x = x; + mutate(y); + }; + + f(); + t1 =
{x}
; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### 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/new-mutability/basic-mutation-via-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.js new file mode 100644 index 0000000000000..8d4bb23742b2b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.js @@ -0,0 +1,11 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + const y = [b]; + const f = () => { + y.x = x; + mutate(y); + }; + f(); + return
{x}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.expect.md new file mode 100644 index 0000000000000..0753f007b7b32 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + const y = [b]; + y.x = x; + mutate(y); + return
{x}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a }; + const y = [b]; + y.x = x; + mutate(y); + t1 =
{x}
; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### 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/new-mutability/basic-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.js new file mode 100644 index 0000000000000..480221fef4dab --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.js @@ -0,0 +1,8 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + const y = [b]; + y.x = x; + mutate(y); + return
{x}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.expect.md new file mode 100644 index 0000000000000..df9b5e58f8b0a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {arrayPush, Stringify} from 'shared-runtime'; + +function Component({prop1, prop2}) { + 'use memo'; + + let x = [{value: prop1}]; + let z; + while (x.length < 2) { + // there's a phi here for x (value before the loop and the reassignment later) + + // this mutation occurs before the reassigned value + arrayPush(x, {value: prop2}); + + if (x[0].value === prop1) { + x = [{value: prop2}]; + const y = x; + z = y[0]; + } + } + z.other = true; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop1: 0, prop2: 'a'}], + sequentialRenders: [ + {prop1: 0, prop2: 'a'}, + {prop1: 1, prop2: 'a'}, + {prop1: 1, prop2: 'b'}, + {prop1: 0, prop2: 'b'}, + {prop1: 0, prop2: 'a'}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { arrayPush, Stringify } from "shared-runtime"; + +function Component(t0) { + "use memo"; + const $ = _c(5); + const { prop1, prop2 } = t0; + let z; + if ($[0] !== prop1 || $[1] !== prop2) { + let x = [{ value: prop1 }]; + while (x.length < 2) { + arrayPush(x, { value: prop2 }); + if (x[0].value === prop1) { + x = [{ value: prop2 }]; + const y = x; + z = y[0]; + } + } + + z.other = true; + $[0] = prop1; + $[1] = prop2; + $[2] = z; + } else { + z = $[2]; + } + let t1; + if ($[3] !== z) { + t1 = ; + $[3] = z; + $[4] = t1; + } else { + t1 = $[4]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ prop1: 0, prop2: "a" }], + sequentialRenders: [ + { prop1: 0, prop2: "a" }, + { prop1: 1, prop2: "a" }, + { prop1: 1, prop2: "b" }, + { prop1: 0, prop2: "b" }, + { prop1: 0, prop2: "a" }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"z":{"value":"a","other":true}}
+
{"z":{"value":"a","other":true}}
+
{"z":{"value":"b","other":true}}
+
{"z":{"value":"b","other":true}}
+
{"z":{"value":"a","other":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.js new file mode 100644 index 0000000000000..042cae823f7f3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.js @@ -0,0 +1,35 @@ +// @enableNewMutationAliasingModel +import {arrayPush, Stringify} from 'shared-runtime'; + +function Component({prop1, prop2}) { + 'use memo'; + + let x = [{value: prop1}]; + let z; + while (x.length < 2) { + // there's a phi here for x (value before the loop and the reassignment later) + + // this mutation occurs before the reassigned value + arrayPush(x, {value: prop2}); + + if (x[0].value === prop1) { + x = [{value: prop2}]; + const y = x; + z = y[0]; + } + } + z.other = true; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop1: 0, prop2: 'a'}], + sequentialRenders: [ + {prop1: 0, prop2: 'a'}, + {prop1: 1, prop2: 'a'}, + {prop1: 1, prop2: 'b'}, + {prop1: 0, prop2: 'b'}, + {prop1: 0, prop2: 'a'}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md new file mode 100644 index 0000000000000..326cd9f7e0d90 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +import {Stringify, mutate} from 'shared-runtime'; + +function Component({foo, bar}) { + let x = {foo}; + let y = {bar}; + const f0 = function () { + let a = {y}; + let b = {x}; + a.y.x = b; + }; + f0(); + mutate(y); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 2, bar: 3}], + sequentialRenders: [ + {foo: 2, bar: 3}, + {foo: 2, bar: 3}, + {foo: 2, bar: 4}, + {foo: 3, bar: 4}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, mutate } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { foo, bar } = t0; + let t1; + if ($[0] !== bar || $[1] !== foo) { + const x = { foo }; + const y = { bar }; + const f0 = function () { + const a = { y }; + const b = { x }; + a.y.x = b; + }; + + f0(); + mutate(y); + t1 = ; + $[0] = bar; + $[1] = foo; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 2, bar: 3 }], + sequentialRenders: [ + { foo: 2, bar: 3 }, + { foo: 2, bar: 3 }, + { foo: 2, bar: 4 }, + { foo: 3, bar: 4 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}
+
{"x":{"bar":3,"x":{"x":{"foo":2}},"wat0":"joe"}}
+
{"x":{"bar":4,"x":{"x":{"foo":2}},"wat0":"joe"}}
+
{"x":{"bar":4,"x":{"x":{"foo":3}},"wat0":"joe"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js new file mode 100644 index 0000000000000..5aa39d3ffb36b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-in-function-expression-indirect.js @@ -0,0 +1,25 @@ +import {Stringify, mutate} from 'shared-runtime'; + +function Component({foo, bar}) { + let x = {foo}; + let y = {bar}; + const f0 = function () { + let a = {y}; + let b = {x}; + a.y.x = b; + }; + f0(); + mutate(y); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 2, bar: 3}], + sequentialRenders: [ + {foo: 2, bar: 3}, + {foo: 2, bar: 3}, + {foo: 2, bar: 4}, + {foo: 3, bar: 4}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.expect.md new file mode 100644 index 0000000000000..d1434e95b827a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function bar(a) { + let x = [a]; + let y = {}; + (function () { + y = x[0][1]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [['val1', 'val2']], + isComponent: false, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function bar(a) { + const $ = _c(4); + let t0; + if ($[0] !== a) { + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0][1]) { + y = {}; + + y = x[0][1]; + $[2] = x[0][1]; + $[3] = y; + } else { + y = $[3]; + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [["val1", "val2"]], + isComponent: false, +}; + +``` + +### Eval output +(kind: ok) "val2" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.js new file mode 100644 index 0000000000000..a77287910a419 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.js @@ -0,0 +1,16 @@ +// @enableNewMutationAliasingModel +function bar(a) { + let x = [a]; + let y = {}; + (function () { + y = x[0][1]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [['val1', 'val2']], + isComponent: false, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.expect.md new file mode 100644 index 0000000000000..80bb009ba25d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function bar(a, b) { + let x = [a, b]; + let y = {}; + let t = {}; + (function () { + y = x[0][1]; + t = x[1][0]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [ + [1, 2], + [2, 3], + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function bar(a, b) { + const $ = _c(6); + let t0; + if ($[0] !== a || $[1] !== b) { + t0 = [a, b]; + $[0] = a; + $[1] = b; + $[2] = t0; + } else { + t0 = $[2]; + } + const x = t0; + let y; + if ($[3] !== x[0][1] || $[4] !== x[1][0]) { + y = {}; + let t = {}; + + y = x[0][1]; + t = x[1][0]; + $[3] = x[0][1]; + $[4] = x[1][0]; + $[5] = y; + } else { + y = $[5]; + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [ + [1, 2], + [2, 3], + ], +}; + +``` + +### Eval output +(kind: ok) 2 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.js new file mode 100644 index 0000000000000..9afe5994b210b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.js @@ -0,0 +1,20 @@ +// @enableNewMutationAliasingModel +function bar(a, b) { + let x = [a, b]; + let y = {}; + let t = {}; + (function () { + y = x[0][1]; + t = x[1][0]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [ + [1, 2], + [2, 3], + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.expect.md new file mode 100644 index 0000000000000..663d1f3d567b3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function bar(a) { + let x = [a]; + let y = {}; + (function () { + y = x[0].a[1]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [{a: ['val1', 'val2']}], + isComponent: false, +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function bar(a) { + const $ = _c(4); + let t0; + if ($[0] !== a) { + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0].a[1]) { + y = {}; + + y = x[0].a[1]; + $[2] = x[0].a[1]; + $[3] = y; + } else { + y = $[3]; + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [{ a: ["val1", "val2"] }], + isComponent: false, +}; + +``` + +### Eval output +(kind: ok) "val2" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.js new file mode 100644 index 0000000000000..5a3cb878485d4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.js @@ -0,0 +1,16 @@ +// @enableNewMutationAliasingModel +function bar(a) { + let x = [a]; + let y = {}; + (function () { + y = x[0].a[1]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: [{a: ['val1', 'val2']}], + isComponent: false, +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.expect.md new file mode 100644 index 0000000000000..58694faf57d33 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.expect.md @@ -0,0 +1,59 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function bar(a) { + let x = [a]; + let y = {}; + (function () { + y = x[0]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: ['TodoAdd'], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function bar(a) { + const $ = _c(4); + let t0; + if ($[0] !== a) { + t0 = [a]; + $[0] = a; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== x[0]) { + y = {}; + + y = x[0]; + $[2] = x[0]; + $[3] = y; + } else { + y = $[3]; + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: ["TodoAdd"], +}; + +``` + +### Eval output +(kind: ok) "TodoAdd" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.js new file mode 100644 index 0000000000000..0b95fc02a2b58 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.js @@ -0,0 +1,15 @@ +// @enableNewMutationAliasingModel +function bar(a) { + let x = [a]; + let y = {}; + (function () { + y = x[0]; + })(); + + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: bar, + params: ['TodoAdd'], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md new file mode 100644 index 0000000000000..73dd12670f159 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md @@ -0,0 +1,33 @@ + +## Input + +```javascript +// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel + +function Component() { + const date = Date.now(); + const now = performance.now(); + const rand = Math.random(); + return ; +} + +``` + + +## Error + +``` + 2 | + 3 | function Component() { +> 4 | const date = Date.now(); + | ^^^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4) + +InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5) + +InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Math.random` is an impure function whose results may change on every call (6:6) + 5 | const now = performance.now(); + 6 | const rand = Math.random(); + 7 | return ; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.js new file mode 100644 index 0000000000000..83cf3e04f2b6a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.js @@ -0,0 +1,8 @@ +// @validateNoImpureFunctionsInRender @enableNewMutationAliasingModel + +function Component() { + const date = Date.now(); + const now = performance.now(); + const rand = Math.random(); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.expect.md new file mode 100644 index 0000000000000..0461bb4b7b4a4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component() { + let local; + + const reassignLocal = newValue => { + local = newValue; + }; + + const onClick = newValue => { + reassignLocal('hello'); + + if (local === newValue) { + // Without React Compiler, `reassignLocal` is freshly created + // on each render, capturing a binding to the latest `local`, + // such that invoking reassignLocal will reassign the same + // binding that we are observing in the if condition, and + // we reach this branch + console.log('`local` was updated!'); + } else { + // With React Compiler enabled, `reassignLocal` is only created + // once, capturing a binding to `local` in that render pass. + // Therefore, calling `reassignLocal` will reassign the wrong + // version of `local`, and not update the binding we are checking + // in the if condition. + // + // To protect against this, we disallow reassigning locals from + // functions that escape + throw new Error('`local` not updated!'); + } + }; + + return ; +} + +``` + + +## Error + +``` + 4 | + 5 | const reassignLocal = newValue => { +> 6 | local = newValue; + | ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (6:6) + 7 | }; + 8 | + 9 | const onClick = newValue => { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js new file mode 100644 index 0000000000000..2cfb336bcf5e3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js @@ -0,0 +1,33 @@ +// @enableNewMutationAliasingModel +function Component() { + let local; + + const reassignLocal = newValue => { + local = newValue; + }; + + const onClick = newValue => { + reassignLocal('hello'); + + if (local === newValue) { + // Without React Compiler, `reassignLocal` is freshly created + // on each render, capturing a binding to the latest `local`, + // such that invoking reassignLocal will reassign the same + // binding that we are observing in the if condition, and + // we reach this branch + console.log('`local` was updated!'); + } else { + // With React Compiler enabled, `reassignLocal` is only created + // once, capturing a binding to `local` in that render pass. + // Therefore, calling `reassignLocal` will reassign the wrong + // version of `local`, and not update the binding we are checking + // in the if condition. + // + // To protect against this, we disallow reassigning locals from + // functions that escape + throw new Error('`local` not updated!'); + } + }; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md new file mode 100644 index 0000000000000..a95ace1df5dd4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel + +import {useCallback} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function Component({content, refetch}) { + // This callback function accesses a hoisted const as a dependency, + // but it cannot reference it as a dependency since that would be a + // TDZ violation! + const onRefetch = useCallback(() => { + refetch(data); + }, [refetch]); + + // The context variable gets frozen here since it's passed to a hook + const onSubmit = useIdentity(onRefetch); + + // This has to error: onRefetch needs to memoize with `content` as a + // dependency, but the dependency comes later + const {data = null} = content; + + return ; +} + +``` + + +## Error + +``` + 9 | // TDZ violation! + 10 | const onRefetch = useCallback(() => { +> 11 | refetch(data); + | ^^^^ InvalidReact: This variable is accessed before it is declared, which may prevent it from updating as the assigned value changes over time. Variable `data` is accessed before it is declared (11:11) + +InvalidReact: This variable is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. Variable `data` is accessed before it is declared (19:19) + 12 | }, [refetch]); + 13 | + 14 | // The context variable gets frozen here since it's passed to a hook +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.js new file mode 100644 index 0000000000000..30d1e0e35e3ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-referencing-frozen-hoisted-storecontext-const.js @@ -0,0 +1,22 @@ +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel + +import {useCallback} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function Component({content, refetch}) { + // This callback function accesses a hoisted const as a dependency, + // but it cannot reference it as a dependency since that would be a + // TDZ violation! + const onRefetch = useCallback(() => { + refetch(data); + }, [refetch]); + + // The context variable gets frozen here since it's passed to a hook + const onSubmit = useIdentity(onRefetch); + + // This has to error: onRefetch needs to memoize with `content` as a + // dependency, but the dependency comes later + const {data = null} = content; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md new file mode 100644 index 0000000000000..498f3d8a0766f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {makeArray} from 'shared-runtime'; + +// This case is already unsound in source, so we can safely bailout +function Foo(props) { + let x = []; + x.push(props); + + // makeArray() is captured, but depsList contains [props] + const cb = useCallback(() => [x], [x]); + + x = makeArray(); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + + +## Error + +``` + 9 | + 10 | // makeArray() is captured, but depsList contains [props] +> 11 | const cb = useCallback(() => [x], [x]); + | ^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (11:11) + +CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (11:11) + 12 | + 13 | x = makeArray(); + 14 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js new file mode 100644 index 0000000000000..b9b914d30ec90 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js @@ -0,0 +1,20 @@ +// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {makeArray} from 'shared-runtime'; + +// This case is already unsound in source, so we can safely bailout +function Foo(props) { + let x = []; + x.push(props); + + // makeArray() is captured, but depsList contains [props] + const cb = useCallback(() => [x], [x]); + + x = makeArray(); + + return cb; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md new file mode 100644 index 0000000000000..d54d2b35e241e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md @@ -0,0 +1,28 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + useFreeze(x); + x.y = true; + return
error
; +} + +``` + + +## Error + +``` + 3 | const x = {a}; + 4 | useFreeze(x); +> 5 | x.y = true; + | ^ InvalidReact: Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook (5:5) + 6 | return
error
; + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js new file mode 100644 index 0000000000000..4964f2304912a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js @@ -0,0 +1,7 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {a}; + useFreeze(x); + x.y = true; + return
error
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md new file mode 100644 index 0000000000000..a26381d1d301c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md @@ -0,0 +1,28 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function useHook(a, b) { + b.test = 1; + a.test = 2; +} + +``` + + +## Error + +``` + 1 | // @enableNewMutationAliasingModel + 2 | function useHook(a, b) { +> 3 | b.test = 1; + | ^ InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (3:3) + +InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (4:4) + 4 | a.test = 2; + 5 | } + 6 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js new file mode 100644 index 0000000000000..41c5b99132460 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js @@ -0,0 +1,5 @@ +// @enableNewMutationAliasingModel +function useHook(a, b) { + b.test = 1; + a.test = 2; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md new file mode 100644 index 0000000000000..6f7d6b24831a8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +let x = {a: 42}; + +function Component(props) { + foo(() => { + x.a = 10; + x.a = 20; + }); +} + +``` + + +## Error + +``` + 4 | function Component(props) { + 5 | foo(() => { +> 6 | x.a = 10; + | ^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (6:6) + +InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (7:7) + 7 | x.a = 20; + 8 | }); + 9 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js new file mode 100644 index 0000000000000..ed51080726b5a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js @@ -0,0 +1,9 @@ +// @enableNewMutationAliasingModel +let x = {a: 42}; + +function Component(props) { + foo(() => { + x.a = 10; + x.a = 20; + }); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md new file mode 100644 index 0000000000000..b6f01488fc755 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component() { + const foo = () => { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; + }; + foo(); +} + +``` + + +## Error + +``` + 3 | const foo = () => { + 4 | // Cannot assign to globals +> 5 | someUnknownGlobal = true; + | ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (5:5) + +InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (6:6) + 6 | moduleLocal = true; + 7 | }; + 8 | foo(); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js new file mode 100644 index 0000000000000..6d6681e60ad34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js @@ -0,0 +1,9 @@ +// @enableNewMutationAliasingModel +function Component() { + const foo = () => { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; + }; + foo(); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md new file mode 100644 index 0000000000000..a75aa397eceec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md @@ -0,0 +1,29 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component() { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; +} + +``` + + +## Error + +``` + 2 | function Component() { + 3 | // Cannot assign to globals +> 4 | someUnknownGlobal = true; + | ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4) + +InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (5:5) + 5 | moduleLocal = true; + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js new file mode 100644 index 0000000000000..41b706866bf7c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js @@ -0,0 +1,6 @@ +// @enableNewMutationAliasingModel +function Component() { + // Cannot assign to globals + someUnknownGlobal = true; + moduleLocal = true; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md new file mode 100644 index 0000000000000..3d9d0b5613857 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component(props) { + function hasErrors() { + let hasErrors = false; + if (props.items == null) { + hasErrors = true; + } + return hasErrors; + } + return hasErrors(); +} + +``` + + +## Error + +``` + 8 | return hasErrors; + 9 | } +> 10 | return hasErrors(); + | ^^^^^^^^^ Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized. hasErrors_0$15:TFunction (10:10) + 11 | } + 12 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js new file mode 100644 index 0000000000000..b58c0aea7daf7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js @@ -0,0 +1,11 @@ +// @enableNewMutationAliasingModel +function Component(props) { + function hasErrors() { + let hasErrors = false; + if (props.items == null) { + hasErrors = true; + } + return hasErrors; + } + return hasErrors(); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md new file mode 100644 index 0000000000000..22f967883b09a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +function Component(props) { + const items = (() => { + if (props.cond) { + return []; + } else { + return null; + } + })(); + items?.push(props.a); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: {}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(props) { + const $ = _c(3); + let items; + if ($[0] !== props.a || $[1] !== props.cond) { + let t0; + if (props.cond) { + t0 = []; + } else { + t0 = null; + } + items = t0; + + items?.push(props.a); + $[0] = props.a; + $[1] = props.cond; + $[2] = items; + } else { + items = $[2]; + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: {} }], +}; + +``` + +### Eval output +(kind: ok) null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js new file mode 100644 index 0000000000000..f4f953d294e6f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js @@ -0,0 +1,16 @@ +function Component(props) { + const items = (() => { + if (props.cond) { + return []; + } else { + return null; + } + })(); + items?.push(props.a); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: {}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md new file mode 100644 index 0000000000000..8dec2e3ebe94b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect} from 'react'; +import {print} from 'shared-runtime'; + +function Component({foo}) { + const arr = []; + // Taking either arr[0].value or arr as a dependency is reasonable + // as long as developers know what to expect. + useEffect(() => print(arr[0]?.value)); + arr.push({value: foo}); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import { useEffect } from "react"; +import { print } from "shared-runtime"; + +function Component(t0) { + const { foo } = t0; + const arr = []; + + useEffect(() => print(arr[0]?.value), [arr[0]?.value]); + arr.push({ value: foo }); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 1 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":171},"end":{"line":12,"column":1,"index":416},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":10,"column":2,"index":377},"end":{"line":10,"column":5,"index":380},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":336},"end":{"line":9,"column":39,"index":373},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":358},"end":{"line":9,"column":27,"index":361},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":171},"end":{"line":12,"column":1,"index":416},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) [{"value":1}] +logs: [1] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js new file mode 100644 index 0000000000000..dd8d6669885d2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js @@ -0,0 +1,17 @@ +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect} from 'react'; +import {print} from 'shared-runtime'; + +function Component({foo}) { + const arr = []; + // Taking either arr[0].value or arr as a dependency is reasonable + // as long as developers know what to expect. + useEffect(() => print(arr[0]?.value)); + arr.push({value: foo}); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md new file mode 100644 index 0000000000000..167c23c3476b5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md @@ -0,0 +1,57 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel + +import {useEffect, useRef} from 'react'; +import {print} from 'shared-runtime'; + +function Component({arrRef}) { + // Avoid taking arr.current as a dependency + useEffect(() => print(arrRef.current)); + arrRef.current.val = 2; + return arrRef; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{arrRef: {current: {val: 'initial ref value'}}}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel + +import { useEffect, useRef } from "react"; +import { print } from "shared-runtime"; + +function Component(t0) { + const { arrRef } = t0; + + useEffect(() => print(arrRef.current), [arrRef]); + arrRef.current.val = 2; + return arrRef; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ arrRef: { current: { val: "initial ref value" } } }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":180},"end":{"line":11,"column":1,"index":343},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":2,"index":301},"end":{"line":9,"column":16,"index":315},"filename":"mutate-after-useeffect-ref-access.ts"}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":259},"end":{"line":8,"column":40,"index":297},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":281},"end":{"line":8,"column":30,"index":287},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":180},"end":{"line":11,"column":1,"index":343},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) {"current":{"val":2}} +logs: [{ val: 2 }] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js new file mode 100644 index 0000000000000..f91bd14deb14c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js @@ -0,0 +1,16 @@ +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel + +import {useEffect, useRef} from 'react'; +import {print} from 'shared-runtime'; + +function Component({arrRef}) { + // Avoid taking arr.current as a dependency + useEffect(() => print(arrRef.current)); + arrRef.current.val = 2; + return arrRef; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{arrRef: {current: {val: 'initial ref value'}}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md new file mode 100644 index 0000000000000..47a0124baa856 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect} from 'react'; + +function Component({foo}) { + const arr = []; + useEffect(() => { + arr.push(foo); + }); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import { useEffect } from "react"; + +function Component(t0) { + const { foo } = t0; + const arr = []; + useEffect(() => { + arr.push(foo); + }, [arr, foo]); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 1 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":133},"end":{"line":11,"column":1,"index":254},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":9,"column":2,"index":226},"end":{"line":9,"column":5,"index":229},"filename":"mutate-after-useeffect.ts","identifierName":"arr"}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":181},"end":{"line":8,"column":4,"index":222},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":203},"end":{"line":7,"column":7,"index":206},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":203},"end":{"line":7,"column":7,"index":206},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":212},"end":{"line":7,"column":16,"index":215},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":133},"end":{"line":11,"column":1,"index":254},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) [2] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js new file mode 100644 index 0000000000000..6f237c89b4d4f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js @@ -0,0 +1,16 @@ +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {useEffect} from 'react'; + +function Component({foo}) { + const arr = []; + useEffect(() => { + arr.push(foo); + }); + arr.push(2); + return arr; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md new file mode 100644 index 0000000000000..013da083261e5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const f = () => { + const y = [x]; + return y[0]; + }; + const x0 = f(); + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a, b }; + const f = () => { + const y = [x]; + return y[0]; + }; + + const x0 = f(); + const z = [x0]; + const x1 = z[0]; + x1.key = "value"; + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"a":0,"b":1,"key":"value"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js new file mode 100644 index 0000000000000..6a981e840891c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js @@ -0,0 +1,20 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const f = () => { + const y = [x]; + return y[0]; + }; + const x0 = f(); + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md new file mode 100644 index 0000000000000..f8ceba27158bd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const f = () => { + const x0 = y[0]; + return [x0]; + }; + const z = f(); + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a, b }; + const y = [x]; + const f = () => { + const x0 = y[0]; + return [x0]; + }; + + const z = f(); + const x1 = z[0]; + x1.key = "value"; + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"a":0,"b":1,"key":"value"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js new file mode 100644 index 0000000000000..aecd27a093094 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js @@ -0,0 +1,20 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const f = () => { + const x0 = y[0]; + return [x0]; + }; + const z = f(); + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md new file mode 100644 index 0000000000000..5f14dd1fe0770 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const x0 = y[0]; + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = { a, b }; + const y = [x]; + const x0 = y[0]; + const z = [x0]; + const x1 = z[0]; + x1.key = "value"; + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"x":{"a":0,"b":1,"key":"value"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js new file mode 100644 index 0000000000000..ba8808eedfe8c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js @@ -0,0 +1,17 @@ +// @enableNewMutationAliasingModel +import {Stringify} from 'shared-runtime'; + +function Component({a, b}) { + const x = {a, b}; + const y = [x]; + const x0 = y[0]; + const z = [x0]; + const x1 = z[0]; + x1.key = 'value'; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md new file mode 100644 index 0000000000000..83e593dbd4149 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.expect.md @@ -0,0 +1,93 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + return identity(x); + }; + const x2 = f(); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { identity, ValidateMemoization } from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const f = () => identity(x); + + const x2 = f(); + x2.b = b; + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":0}}
+
{"inputs":[0,1],"output":{"a":0,"b":1}}
+
{"inputs":[1,1],"output":{"a":1,"b":1}}
+
{"inputs":[0,0],"output":{"a":0,"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js new file mode 100644 index 0000000000000..c7770ffcdce2b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity-function-expression.js @@ -0,0 +1,24 @@ +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + return identity(x); + }; + const x2 = f(); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md new file mode 100644 index 0000000000000..78cb6697fc7ac --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.expect.md @@ -0,0 +1,88 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const x2 = identity(x); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { identity, ValidateMemoization } from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const x2 = identity(x); + x2.b = b; + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":0}}
+
{"inputs":[0,1],"output":{"a":0,"b":1}}
+
{"inputs":[1,1],"output":{"a":1,"b":1}}
+
{"inputs":[0,0],"output":{"a":0,"b":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js new file mode 100644 index 0000000000000..bd928634a29bf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-identity.js @@ -0,0 +1,21 @@ +import {useMemo} from 'react'; +import {identity, ValidateMemoization} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => ({a}), [a, b]); + const x2 = identity(x); + x2.b = b; + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md new file mode 100644 index 0000000000000..34345951ed7fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {}; + const y = {x}; + const z = y.x; + z.true = false; + return
{z}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(1); + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const x = {}; + const y = { x }; + const z = y.x; + z.true = false; + t1 =
{z}
; + $[0] = t1; + } else { + t1 = $[0]; + } + return t1; +} + +``` + +### 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/new-mutability/mutate-through-propertyload.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.js new file mode 100644 index 0000000000000..bff1ea4c35046 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.js @@ -0,0 +1,8 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = {}; + const y = {x}; + const z = y.x; + z.true = false; + return
{z}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md new file mode 100644 index 0000000000000..5033da8eac440 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md @@ -0,0 +1,75 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useState} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function useMakeCallback({obj}: {obj: {value: number}}) { + const [state, setState] = useState(0); + const cb = () => { + if (obj.value !== state) setState(obj.value); + }; + useIdentity(); + cb(); + return [cb]; +} +export const FIXTURE_ENTRYPOINT = { + fn: useMakeCallback, + params: [{obj: {value: 1}}], + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useState } from "react"; +import { useIdentity } from "shared-runtime"; + +function useMakeCallback(t0) { + const $ = _c(5); + const { obj } = t0; + const [state, setState] = useState(0); + let t1; + if ($[0] !== obj.value || $[1] !== state) { + t1 = () => { + if (obj.value !== state) { + setState(obj.value); + } + }; + $[0] = obj.value; + $[1] = state; + $[2] = t1; + } else { + t1 = $[2]; + } + const cb = t1; + + useIdentity(); + cb(); + let t2; + if ($[3] !== cb) { + t2 = [cb]; + $[3] = cb; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useMakeCallback, + params: [{ obj: { value: 1 } }], + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }], +}; + +``` + +### Eval output +(kind: ok) ["[[ function params=0 ]]"] +["[[ function params=0 ]]"] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js new file mode 100644 index 0000000000000..1f2d69d93193f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js @@ -0,0 +1,18 @@ +// @enableNewMutationAliasingModel +import {useState} from 'react'; +import {useIdentity} from 'shared-runtime'; + +function useMakeCallback({obj}: {obj: {value: number}}) { + const [state, setState] = useState(0); + const cb = () => { + if (obj.value !== state) setState(obj.value); + }; + useIdentity(); + cb(); + return [cb]; +} +export const FIXTURE_ENTRYPOINT = { + fn: useMakeCallback, + params: [{obj: {value: 1}}], + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md new file mode 100644 index 0000000000000..5c73ce6d77adf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {identity, mutate} from 'shared-runtime'; + +function Component(props) { + const key = {}; + const context = { + [key]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { identity, mutate } from "shared-runtime"; + +function Component(props) { + const $ = _c(2); + let context; + if ($[0] !== props.value) { + const key = {}; + context = { [key]: identity([props.value]) }; + + mutate(key); + $[0] = props.value; + $[1] = context; + } else { + context = $[1]; + } + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) {"[object Object]":[42]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js new file mode 100644 index 0000000000000..923733b9c238d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js @@ -0,0 +1,16 @@ +// @enableNewMutationAliasingModel +import {identity, mutate} from 'shared-runtime'; + +function Component(props) { + const key = {}; + const context = { + [key]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md new file mode 100644 index 0000000000000..1ef3ed157f9fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {identity, mutate, mutateAndReturn} from 'shared-runtime'; + +function Component(props) { + const key = {a: 'key'}; + const context = { + [key.a]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { identity, mutate, mutateAndReturn } from "shared-runtime"; + +function Component(props) { + const $ = _c(4); + let context; + if ($[0] !== props.value) { + const key = { a: "key" }; + + const t0 = key.a; + const t1 = identity([props.value]); + let t2; + if ($[2] !== t1) { + t2 = { [t0]: t1 }; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } + context = t2; + + mutate(key); + $[0] = props.value; + $[1] = context; + } else { + context = $[1]; + } + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) {"key":[42]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js new file mode 100644 index 0000000000000..516fdc1dbcf41 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js @@ -0,0 +1,16 @@ +// @enableNewMutationAliasingModel +import {identity, mutate, mutateAndReturn} from 'shared-runtime'; + +function Component(props) { + const key = {a: 'key'}; + const context = { + [key.a]: identity([props.value]), + }; + mutate(key); + return context; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md new file mode 100644 index 0000000000000..a5cfc790ebc06 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b, c}) { + const x = [a, b]; + const f = () => { + maybeMutate(x); + // different dependency to force this not to merge with x's scope + console.log(c); + }; + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(9); + const { a, b, c } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + t1 = [a, b]; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + const x = t1; + let t2; + if ($[3] !== c || $[4] !== x) { + t2 = () => { + maybeMutate(x); + + console.log(c); + }; + $[3] = c; + $[4] = x; + $[5] = t2; + } else { + t2 = $[5]; + } + const f = t2; + let t3; + if ($[6] !== f || $[7] !== x) { + t3 = ; + $[6] = f; + $[7] = x; + $[8] = t3; + } else { + t3 = $[8]; + } + return t3; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.js new file mode 100644 index 0000000000000..096f4f17ea545 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.js @@ -0,0 +1,10 @@ +// @enableNewMutationAliasingModel +function Component({a, b, c}) { + const x = [a, b]; + const f = () => { + maybeMutate(x); + // different dependency to force this not to merge with x's scope + console.log(c); + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md new file mode 100644 index 0000000000000..26757db1a3c28 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function ReactiveRefInEffect(props) { + const ref1 = useRef('initial value'); + const ref2 = useRef('initial value'); + let ref; + if (props.foo) { + ref = ref1; + } else { + ref = ref2; + } + useEffect(() => print(ref)); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function ReactiveRefInEffect(props) { + const $ = _c(4); + const ref1 = useRef("initial value"); + const ref2 = useRef("initial value"); + let ref; + if ($[0] !== props.foo) { + if (props.foo) { + ref = ref1; + } else { + ref = ref2; + } + $[0] = props.foo; + $[1] = ref; + } else { + ref = $[1]; + } + let t0; + if ($[2] !== ref) { + t0 = () => print(ref); + $[2] = ref; + $[3] = t0; + } else { + t0 = $[3]; + } + useEffect(t0); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.js new file mode 100644 index 0000000000000..3ae653c962034 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.js @@ -0,0 +1,12 @@ +// @enableNewMutationAliasingModel +function ReactiveRefInEffect(props) { + const ref1 = useRef('initial value'); + const ref2 = useRef('initial value'); + let ref; + if (props.foo) { + ref = ref1; + } else { + ref = ref2; + } + useEffect(() => print(ref)); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md new file mode 100644 index 0000000000000..de7fc2903ebd2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +// @inferEffectDependencies @enableNewMutationAliasingModel +import {useEffect, useState} from 'react'; +import {print} from 'shared-runtime'; + +/* + * setState types are not enough to determine to omit from deps. Must also take reactivity into account. + */ +function ReactiveRefInEffect(props) { + const [_state1, setState1] = useRef('initial value'); + const [_state2, setState2] = useRef('initial value'); + let setState; + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + useEffect(() => print(setState)); +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies @enableNewMutationAliasingModel +import { useEffect, useState } from "react"; +import { print } from "shared-runtime"; + +/* + * setState types are not enough to determine to omit from deps. Must also take reactivity into account. + */ +function ReactiveRefInEffect(props) { + const $ = _c(4); + const [, setState1] = useRef("initial value"); + const [, setState2] = useRef("initial value"); + let setState; + if ($[0] !== props.foo) { + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + $[0] = props.foo; + $[1] = setState; + } else { + setState = $[1]; + } + let t0; + if ($[2] !== setState) { + t0 = () => print(setState); + $[2] = setState; + $[3] = t0; + } else { + t0 = $[3]; + } + useEffect(t0, [setState]); +} + +``` + +### 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/new-mutability/reactive-setState.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.js new file mode 100644 index 0000000000000..158881eb0204d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.js @@ -0,0 +1,18 @@ +// @inferEffectDependencies @enableNewMutationAliasingModel +import {useEffect, useState} from 'react'; +import {print} from 'shared-runtime'; + +/* + * setState types are not enough to determine to omit from deps. Must also take reactivity into account. + */ +function ReactiveRefInEffect(props) { + const [_state1, setState1] = useRef('initial value'); + const [_state2, setState2] = useRef('initial value'); + let setState; + if (props.foo) { + setState = setState1; + } else { + setState = setState2; + } + useEffect(() => print(setState)); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md new file mode 100644 index 0000000000000..dfc4ed988309a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel + +import fbt from 'fbt'; + +component Component() { + const sections = Object.keys(items); + + for (let i = 0; i < sections.length; i += 3) { + chunks.push( + sections.slice(i, i + 3).map(section => { + return ; + }) + ); + } + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import fbt from "fbt"; + +function Component() { + const $ = _c(1); + const sections = Object.keys(items); + for (let i = 0; i < sections.length; i = i + 3, i) { + chunks.push(sections.slice(i, i + 3).map(_temp)); + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp(section) { + return ; +} + +``` + +### 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/new-mutability/repro-compiler-infinite-loop.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js new file mode 100644 index 0000000000000..d03a44618ea01 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-compiler-infinite-loop.js @@ -0,0 +1,17 @@ +// @flow @enableNewMutationAliasingModel + +import fbt from 'fbt'; + +component Component() { + const sections = Object.keys(items); + + for (let i = 0; i < sections.length; i += 3) { + chunks.push( + sections.slice(i, i + 3).map(section => { + return ; + }) + ); + } + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md new file mode 100644 index 0000000000000..9d168c9e5c1ea --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +function Component() { + const x = {}; + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = {}; + $[0] = t0; + } else { + t0 = $[0]; + } + const x = t0; + let t1; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + + t1 = ; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +``` + +### 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/new-mutability/repro-function-expression-effects-stack-overflow.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js new file mode 100644 index 0000000000000..6e67ed7bab695 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-function-expression-effects-stack-overflow.js @@ -0,0 +1,14 @@ +function Component() { + const x = {}; + const fn = () => { + new Object() + .build(x) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}) + .build({}); + }; + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.expect.md new file mode 100644 index 0000000000000..9a0c82a3ccca0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +component Component( + onAsyncSubmit?: (() => void) => void, + onClose: (isConfirmed: boolean) => void +) { + // When running inferReactiveScopeVariables, + // onAsyncSubmit and onClose update to share + // a mutableRange instance. + const onSubmit = useCallback(() => { + if (onAsyncSubmit) { + onAsyncSubmit(() => { + onClose(true); + }); + return; + } + }, [onAsyncSubmit, onClose]); + // When running inferReactiveScopeVariables here, + // first the existing range gets updated (affecting + // onAsyncSubmit) and then onClose gets assigned a + // different mutable range instance, which is the + // one reset after AnalyzeFunctions. + // The fix is to fully reset mutable ranges *instances* + // after AnalyzeFunctions visit a function expression + return onClose(false)} />; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(8); + const { onAsyncSubmit, onClose } = t0; + let t1; + if ($[0] !== onAsyncSubmit || $[1] !== onClose) { + t1 = () => { + if (onAsyncSubmit) { + onAsyncSubmit(() => { + onClose(true); + }); + return; + } + }; + $[0] = onAsyncSubmit; + $[1] = onClose; + $[2] = t1; + } else { + t1 = $[2]; + } + const onSubmit = t1; + let t2; + if ($[3] !== onClose) { + t2 = () => onClose(false); + $[3] = onClose; + $[4] = t2; + } else { + t2 = $[4]; + } + let t3; + if ($[5] !== onSubmit || $[6] !== t2) { + t3 = ; + $[5] = onSubmit; + $[6] = t2; + $[7] = t3; + } else { + t3 = $[7]; + } + return t3; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.js new file mode 100644 index 0000000000000..20cad06e97e2e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-internal-compiler-shared-mutablerange-bug.js @@ -0,0 +1,25 @@ +//@flow @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel +component Component( + onAsyncSubmit?: (() => void) => void, + onClose: (isConfirmed: boolean) => void +) { + // When running inferReactiveScopeVariables, + // onAsyncSubmit and onClose update to share + // a mutableRange instance. + const onSubmit = useCallback(() => { + if (onAsyncSubmit) { + onAsyncSubmit(() => { + onClose(true); + }); + return; + } + }, [onAsyncSubmit, onClose]); + // When running inferReactiveScopeVariables here, + // first the existing range gets updated (affecting + // onAsyncSubmit) and then onClose gets assigned a + // different mutable range instance, which is the + // one reset after AnalyzeFunctions. + // The fix is to fully reset mutable ranges *instances* + // after AnalyzeFunctions visit a function expression + return onClose(false)} />; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md new file mode 100644 index 0000000000000..73cf419be14d3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.expect.md @@ -0,0 +1,60 @@ + +## Input + +```javascript +function Component({a, b}) { + const y = {a}; + const x = {b}; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + // z is a phi with a backedge, and we don't realize it could be x, + // and therefore fail to record a Capture x <- y effect for this + // function expression + z.y = y; + }; + f(); + mutate(x); + return
{x}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const y = { a }; + const x = { b }; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + + z.y = y; + }; + + f(); + mutate(x); + t1 =
{x}
; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### 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/new-mutability/repro-invalid-function-expression-effects-phi.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js new file mode 100644 index 0000000000000..31a51b45aa384 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-invalid-function-expression-effects-phi.js @@ -0,0 +1,17 @@ +function Component({a, b}) { + const y = {a}; + const x = {b}; + const f = () => { + let z = null; + while (z == null) { + z = x; + } + // z is a phi with a backedge, and we don't realize it could be x, + // and therefore fail to record a Capture x <- y effect for this + // function expression + z.y = y; + }; + f(); + mutate(x); + return
{x}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md new file mode 100644 index 0000000000000..109219e03ada0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @flow @enableNewMutationAliasingModel + +import {identity, Stringify, useFragment} from 'shared-runtime'; + +component Example() { + const data = useFragment(); + + const {a, b} = identity(data); + + const el = ; + + identity(a.at(0)); + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; + +import { identity, Stringify, useFragment } from "shared-runtime"; + +function Example() { + const $ = _c(2); + const data = useFragment(); + let t0; + if ($[0] !== data) { + const { a, b } = identity(data); + + const el = ; + + identity(a.at(0)); + + t0 = ; + $[0] = data; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js new file mode 100644 index 0000000000000..7ab6dbc30ab2c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-jsx-captures-value-mutated-later.js @@ -0,0 +1,15 @@ +// @flow @enableNewMutationAliasingModel + +import {identity, Stringify, useFragment} from 'shared-runtime'; + +component Example() { + const data = useFragment(); + + const {a, b} = identity(data); + + const el = ; + + identity(a.at(0)); + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md new file mode 100644 index 0000000000000..28fc8b601f7ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel:true + +export const App = () => { + const [selected, setSelected] = useState(new Set()); + const onSelectedChange = (value: string) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + // This should not count as a mutation of `selected` + newSelected.delete(value); + } else { + // This should not count as a mutation of `selected` + newSelected.add(value); + } + setSelected(newSelected); + }; + + return ; +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:true + +export const App = () => { + const $ = _c(6); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = new Set(); + $[0] = t0; + } else { + t0 = $[0]; + } + const [selected, setSelected] = useState(t0); + let t1; + if ($[1] !== selected) { + t1 = (value) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + newSelected.delete(value); + } else { + newSelected.add(value); + } + + setSelected(newSelected); + }; + $[1] = selected; + $[2] = t1; + } else { + t1 = $[2]; + } + const onSelectedChange = t1; + let t2; + if ($[3] !== onSelectedChange || $[4] !== selected) { + t2 = ; + $[3] = onSelectedChange; + $[4] = selected; + $[5] = t2; + } else { + t2 = $[5]; + } + 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/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js new file mode 100644 index 0000000000000..c5a404a66c371 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/repro-mutate-new-set-of-frozen-items-in-callback.js @@ -0,0 +1,18 @@ +// @enableNewMutationAliasingModel:true + +export const App = () => { + const [selected, setSelected] = useState(new Set()); + const onSelectedChange = (value: string) => { + const newSelected = new Set(selected); + if (newSelected.has(value)) { + // This should not count as a mutation of `selected` + newSelected.delete(value); + } else { + // This should not count as a mutation of `selected` + newSelected.add(value); + } + setSelected(newSelected); + }; + + return ; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md new file mode 100644 index 0000000000000..2e678796cd0fb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; + +function Foo({propVal}) { + const arr = [propVal]; + useEffectWrapper(() => print(arr)); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal)); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; + +``` + +## Code + +```javascript +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import { print } from "shared-runtime"; +import useEffectWrapper from "useEffectWrapper"; + +function Foo({ propVal }) { + const arr = [propVal]; + useEffectWrapper(() => print(arr)); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal)); + arr2.push(2); + return { arr, arr2 }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ propVal: 1 }], + sequentialRenders: [{ propVal: 1 }, { propVal: 2 }], +}; + +``` + +## Logs + +``` +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":195},"end":{"line":13,"column":1,"index":389},"filename":"retry-no-emit.ts"},"detail":{"reason":"Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook","description":null,"severity":"InvalidReact","suggestions":null,"loc":{"start":{"line":11,"column":2,"index":352},"end":{"line":11,"column":6,"index":356},"filename":"retry-no-emit.ts","identifierName":"arr2"}}} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":248},"end":{"line":7,"column":36,"index":282},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":277},"end":{"line":7,"column":34,"index":280},"filename":"retry-no-emit.ts","identifierName":"arr"}]} +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":306},"end":{"line":10,"column":44,"index":348},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":329},"end":{"line":10,"column":29,"index":333},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":329},"end":{"line":10,"column":29,"index":333},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":339},"end":{"line":10,"column":42,"index":346},"filename":"retry-no-emit.ts","identifierName":"propVal"}]} +{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":195},"end":{"line":13,"column":1,"index":389},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0} +``` + +### Eval output +(kind: ok) {"arr":[1],"arr2":[2]} +{"arr":[2],"arr2":[2]} +logs: [[ 1 ],[ 2 ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js new file mode 100644 index 0000000000000..c15f400d3114d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js @@ -0,0 +1,19 @@ +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly @enableNewMutationAliasingModel +import {print} from 'shared-runtime'; +import useEffectWrapper from 'useEffectWrapper'; + +function Foo({propVal}) { + const arr = [propVal]; + useEffectWrapper(() => print(arr)); + + const arr2 = []; + useEffectWrapper(() => arr2.push(propVal)); + arr2.push(2); + return {arr, arr2}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{propVal: 1}], + sequentialRenders: [{propVal: 1}, {propVal: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md new file mode 100644 index 0000000000000..955c4e0705797 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md @@ -0,0 +1,54 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function useHook({el1, el2}) { + const s = new Set(); + const arr = makeArray(el1); + s.add(arr); + // Mutate after store + arr.push(el2); + + s.add(makeArray(el2)); + return s.size; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function useHook(t0) { + const $ = _c(5); + const { el1, el2 } = t0; + let s; + if ($[0] !== el1 || $[1] !== el2) { + s = new Set(); + const arr = makeArray(el1); + s.add(arr); + + arr.push(el2); + let t1; + if ($[3] !== el2) { + t1 = makeArray(el2); + $[3] = el2; + $[4] = t1; + } else { + t1 = $[4]; + } + s.add(t1); + $[0] = el1; + $[1] = el2; + $[2] = s; + } else { + s = $[2]; + } + return s.size; +} + +``` + +### 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/new-mutability/set-add-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.js new file mode 100644 index 0000000000000..3afbd93f84b17 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.js @@ -0,0 +1,11 @@ +// @enableNewMutationAliasingModel +function useHook({el1, el2}) { + const s = new Set(); + const arr = makeArray(el1); + s.add(arr); + // Mutate after store + arr.push(el2); + + s.add(makeArray(el2)); + return s.size; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md new file mode 100644 index 0000000000000..39bd61aaf34a7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +// @enableFire @enableNewMutationAliasingModel +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} + +``` + +## Code + +```javascript +import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire @enableNewMutationAliasingModel +import { fire } from "react"; + +function Component(t0) { + const $ = _c(9); + const { bar, baz } = t0; + let t1; + if ($[0] !== bar) { + t1 = () => { + console.log(bar); + }; + $[0] = bar; + $[1] = t1; + } else { + t1 = $[1]; + } + const foo = t1; + const t2 = useFire(foo); + const t3 = useFire(baz); + let t4; + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) { + t4 = () => { + t2(bar); + t3(bar); + }; + $[2] = bar; + $[3] = t2; + $[4] = t3; + $[5] = t4; + } else { + t4 = $[5]; + } + useEffect(t4); + let t5; + if ($[6] !== bar || $[7] !== t2) { + t5 = () => { + t2(bar); + }; + $[6] = bar; + $[7] = t2; + $[8] = t5; + } else { + t5 = $[8]; + } + useEffect(t5); + return null; +} + +``` + +### 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/new-mutability/shared-hook-calls.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.js new file mode 100644 index 0000000000000..54d4cf83fe310 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.js @@ -0,0 +1,18 @@ +// @enableFire @enableNewMutationAliasingModel +import {fire} from 'react'; + +function Component({bar, baz}) { + const foo = () => { + console.log(bar); + }; + useEffect(() => { + fire(foo(bar)); + fire(baz(bar)); + }); + + useEffect(() => { + fire(foo(bar)); + }); + + return null; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md new file mode 100644 index 0000000000000..4c04ae197292f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md @@ -0,0 +1,70 @@ + +## Input + +```javascript +// @enablePropagateDepsInHIR @enableNewMutationAliasingModel +function useFoo(props) { + let x = []; + x.push(props.bar); + // todo: the below should memoize separately from the above + // my guess is that the phi causes the different `x` identifiers + // to get added to an alias group. this is where we need to track + // the actual state of the alias groups at the time of the mutation + props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null; + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{cond: false, foo: 2, bar: 55}], + sequentialRenders: [ + {cond: false, foo: 2, bar: 55}, + {cond: false, foo: 3, bar: 55}, + {cond: true, foo: 3, bar: 55}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR @enableNewMutationAliasingModel +function useFoo(props) { + const $ = _c(5); + let x; + if ($[0] !== props.bar) { + x = []; + x.push(props.bar); + $[0] = props.bar; + $[1] = x; + } else { + x = $[1]; + } + if ($[2] !== props.cond || $[3] !== props.foo) { + props.cond ? (([x] = [[]]), x.push(props.foo)) : null; + $[2] = props.cond; + $[3] = props.foo; + $[4] = x; + } else { + x = $[4]; + } + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond: false, foo: 2, bar: 55 }], + sequentialRenders: [ + { cond: false, foo: 2, bar: 55 }, + { cond: false, foo: 3, bar: 55 }, + { cond: true, foo: 3, bar: 55 }, + ], +}; + +``` + +### Eval output +(kind: ok) [55] +[55] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js new file mode 100644 index 0000000000000..923d0b59bb810 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js @@ -0,0 +1,21 @@ +// @enablePropagateDepsInHIR @enableNewMutationAliasingModel +function useFoo(props) { + let x = []; + x.push(props.bar); + // todo: the below should memoize separately from the above + // my guess is that the phi causes the different `x` identifiers + // to get added to an alias group. this is where we need to track + // the actual state of the alias groups at the time of the mutation + props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null; + return x; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{cond: false, foo: 2, bar: 55}], + sequentialRenders: [ + {cond: false, foo: 2, bar: 55}, + {cond: false, foo: 3, bar: 55}, + {cond: true, foo: 3, bar: 55}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md new file mode 100644 index 0000000000000..0a31e02ae25e7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.expect.md @@ -0,0 +1,157 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b, c}: {a: number; b: number; c: number}) { + const x = useMemo(() => [{value: a}], [a, b, c]); + if (b === 0) { + // This object should only depend on c, it cannot be affected by the later mutation + x.push({value: c}); + } else { + // This mutation shouldn't affect the object in the consequent + mutate(x); + } + + return ( + <> + ; + {/* TODO: should only depend on c */} + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0, c: 0}], + sequentialRenders: [ + {a: 0, b: 0, c: 0}, + {a: 0, b: 1, c: 0}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 1, c: 1}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 0, c: 0}, + {a: 0, b: 0, c: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(21); + const { a, b, c } = t0; + let x; + if ($[0] !== a || $[1] !== b || $[2] !== c) { + x = [{ value: a }]; + if (b === 0) { + x.push({ value: c }); + } else { + mutate(x); + } + $[0] = a; + $[1] = b; + $[2] = c; + $[3] = x; + } else { + x = $[3]; + } + let t1; + if ($[4] !== a || $[5] !== b || $[6] !== c) { + t1 = [a, b, c]; + $[4] = a; + $[5] = b; + $[6] = c; + $[7] = t1; + } else { + t1 = $[7]; + } + let t2; + if ($[8] !== t1 || $[9] !== x) { + t2 = ; + $[8] = t1; + $[9] = x; + $[10] = t2; + } else { + t2 = $[10]; + } + let t3; + if ($[11] !== a || $[12] !== b || $[13] !== c) { + t3 = [a, b, c]; + $[11] = a; + $[12] = b; + $[13] = c; + $[14] = t3; + } else { + t3 = $[14]; + } + let t4; + if ($[15] !== t3 || $[16] !== x[0]) { + t4 = ; + $[15] = t3; + $[16] = x[0]; + $[17] = t4; + } else { + t4 = $[17]; + } + let t5; + if ($[18] !== t2 || $[19] !== t4) { + t5 = ( + <> + {t2};{t4}; + + ); + $[18] = t2; + $[19] = t4; + $[20] = t5; + } else { + t5 = $[20]; + } + return t5; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0, c: 0 }], + sequentialRenders: [ + { a: 0, b: 0, c: 0 }, + { a: 0, b: 1, c: 0 }, + { a: 1, b: 1, c: 0 }, + { a: 1, b: 1, c: 1 }, + { a: 1, b: 1, c: 0 }, + { a: 1, b: 0, c: 0 }, + { a: 0, b: 0, c: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}
;
{"inputs":[0,0,0],"output":{"value":0}}
; +
{"inputs":[0,1,0],"output":[{"value":0},"joe"]}
;
{"inputs":[0,1,0],"output":{"value":0}}
; +
{"inputs":[1,1,0],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,0],"output":{"value":1}}
; +
{"inputs":[1,1,1],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,1],"output":{"value":1}}
; +
{"inputs":[1,1,0],"output":[{"value":1},"joe"]}
;
{"inputs":[1,1,0],"output":{"value":1}}
; +
{"inputs":[1,0,0],"output":[{"value":1},{"value":0}]}
;
{"inputs":[1,0,0],"output":{"value":1}}
; +
{"inputs":[0,0,0],"output":[{"value":0},{"value":0}]}
;
{"inputs":[0,0,0],"output":{"value":0}}
; \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx new file mode 100644 index 0000000000000..61f8c47e453d4 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-control-flow-sensitive-mutation.tsx @@ -0,0 +1,41 @@ +import {useMemo} from 'react'; +import { + mutate, + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b, c}: {a: number; b: number; c: number}) { + const x = useMemo(() => [{value: a}], [a, b, c]); + if (b === 0) { + // This object should only depend on c, it cannot be affected by the later mutation + x.push({value: c}); + } else { + // This mutation shouldn't affect the object in the consequent + mutate(x); + } + + return ( + <> + ; + {/* TODO: should only depend on c */} + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0, c: 0}], + sequentialRenders: [ + {a: 0, b: 0, c: 0}, + {a: 0, b: 1, c: 0}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 1, c: 1}, + {a: 1, b: 1, c: 0}, + {a: 1, b: 0, c: 0}, + {a: 0, b: 0, c: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md new file mode 100644 index 0000000000000..c985809353e45 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.expect.md @@ -0,0 +1,112 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + const z = f(); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + // TODO: this *should* only depend on `a` + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = [{ a }]; + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + + const z_0 = f(); + + typedMutate(z_0, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":[{"a":0}]}
+
{"inputs":[0,1],"output":[{"a":0}]}
+
{"inputs":[1,1],"output":[{"a":1}]}
+
{"inputs":[0,0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx new file mode 100644 index 0000000000000..d6bd1690f6557 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/todo-transitivity-createfrom-capture-lambda.tsx @@ -0,0 +1,33 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const f = () => { + const y = typedCreateFrom(x); + const z = typedCapture(y); + return z; + }; + const z = f(); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + // TODO: this *should* only depend on `a` + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md new file mode 100644 index 0000000000000..09c4e3eaf3332 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = [a]; + const y = {b}; + mutate(y); + y.x = x; + return
{y}
; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +function Component(t0) { + const $ = _c(5); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = [a]; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let t2; + if ($[2] !== b || $[3] !== x) { + const y = { b }; + mutate(y); + y.x = x; + t2 =
{y}
; + $[2] = b; + $[3] = x; + $[4] = t2; + } else { + t2 = $[4]; + } + 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/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js new file mode 100644 index 0000000000000..e6e2e17bc0b96 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js @@ -0,0 +1,8 @@ +// @enableNewMutationAliasingModel +function Component({a, b}) { + const x = [a]; + const y = {b}; + mutate(y); + y.x = x; + return
{y}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md new file mode 100644 index 0000000000000..4f665646241fa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.expect.md @@ -0,0 +1,147 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const o: any = useMemo(() => ({a}), [a]); + const x: Array = useMemo(() => [o], [o, b]); + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + + return ( + <> + ; + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(19); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = { a }; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const o = t1; + let x; + if ($[2] !== b || $[3] !== o) { + x = [o]; + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + $[2] = b; + $[3] = o; + $[4] = x; + } else { + x = $[4]; + } + let t2; + if ($[5] !== a) { + t2 = [a]; + $[5] = a; + $[6] = t2; + } else { + t2 = $[6]; + } + let t3; + if ($[7] !== o || $[8] !== t2) { + t3 = ; + $[7] = o; + $[8] = t2; + $[9] = t3; + } else { + t3 = $[9]; + } + let t4; + if ($[10] !== a || $[11] !== b) { + t4 = [a, b]; + $[10] = a; + $[11] = b; + $[12] = t4; + } else { + t4 = $[12]; + } + let t5; + if ($[13] !== t4 || $[14] !== x) { + t5 = ; + $[13] = t4; + $[14] = x; + $[15] = t5; + } else { + t5 = $[15]; + } + let t6; + if ($[16] !== t3 || $[17] !== t5) { + t6 = ( + <> + {t3};{t5}; + + ); + $[16] = t3; + $[17] = t5; + $[18] = t6; + } else { + t6 = $[18]; + } + return t6; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}
; +
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,1],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],1]}
; +
{"inputs":[1],"output":{"a":1}}
;
{"inputs":[1,1],"output":[{"a":1},[["[[ cyclic ref *2 ]]"]],1]}
; +
{"inputs":[0],"output":{"a":0}}
;
{"inputs":[0,0],"output":[{"a":0},[["[[ cyclic ref *2 ]]"]],0]}
; \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx new file mode 100644 index 0000000000000..d81c069e336ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-add-captured-array-to-itself.tsx @@ -0,0 +1,34 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const o: any = useMemo(() => ({a}), [a]); + const x: Array = useMemo(() => [o], [o, b]); + const y = typedCapture(x); + const z = typedCapture(y); + x.push(z); + x.push(b); + + return ( + <> + ; + ; + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md new file mode 100644 index 0000000000000..2cffd06f07b75 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.expect.md @@ -0,0 +1,111 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + const z = f(); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + + const z_0 = f(); + + typedMutate(z_0, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"property":0}}
+
{"inputs":[0,1],"output":{"a":0,"property":1}}
+
{"inputs":[1,1],"output":{"a":1,"property":1}}
+
{"inputs":[0,0],"output":{"a":0,"property":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx new file mode 100644 index 0000000000000..72289eb833571 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom-lambda.tsx @@ -0,0 +1,32 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const f = () => { + const y = typedCapture(x); + const z = typedCreateFrom(y); + return z; + }; + const z = f(); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md new file mode 100644 index 0000000000000..458b75dff94a8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const y = typedCapture(x); + const z = typedCreateFrom(y); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = { a }; + const y = typedCapture(x); + const z = typedCreateFrom(y); + + typedMutate(z, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"property":0}}
+
{"inputs":[0,1],"output":{"a":0,"property":1}}
+
{"inputs":[1,1],"output":{"a":1,"property":1}}
+
{"inputs":[0,0],"output":{"a":0,"property":0}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx new file mode 100644 index 0000000000000..d06ad11eb5753 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-capture-createfrom.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}: {a: number; b: number}) { + const x = useMemo(() => ({a}), [a, b]); + const y = typedCapture(x); + const z = typedCreateFrom(y); + // mutates x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md new file mode 100644 index 0000000000000..42f34bff4144a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.expect.md @@ -0,0 +1,101 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const y = typedCreateFrom(x); + const z = typedCapture(y); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = [{ a }]; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + const y = typedCreateFrom(x); + const z = typedCapture(y); + + typedMutate(z, b); + let t2; + if ($[2] !== a) { + t2 = [a]; + $[2] = a; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== x) { + t3 = ; + $[4] = t2; + $[5] = x; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":[{"a":0}]}
+
{"inputs":[0],"output":[{"a":0}]}
+
{"inputs":[1],"output":[{"a":1}]}
+
{"inputs":[0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx new file mode 100644 index 0000000000000..32d65e61e01ec --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-createfrom-capture.tsx @@ -0,0 +1,28 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a]); + const y = typedCreateFrom(x); + const z = typedCapture(y); + // does not mutate x, so x should not depend on b + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md new file mode 100644 index 0000000000000..0786bac4341f6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.expect.md @@ -0,0 +1,118 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a, b]); + let z: any; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + // could mutate x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(11); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = { a }; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + let x; + if ($[2] !== b || $[3] !== t1) { + x = [t1]; + let z; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + + typedMutate(z, b); + $[2] = b; + $[3] = t1; + $[4] = x; + } else { + x = $[4]; + } + let t2; + if ($[5] !== a || $[6] !== b) { + t2 = [a, b]; + $[5] = a; + $[6] = b; + $[7] = t2; + } else { + t2 = $[7]; + } + let t3; + if ($[8] !== t2 || $[9] !== x) { + t3 = ; + $[8] = t2; + $[9] = x; + $[10] = t3; + } else { + t3 = $[10]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 0, b: 1 }, + { a: 1, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":[{"a":0}]}
+
{"inputs":[0,1],"output":[{"a":0}]}
+
{"inputs":[1,1],"output":[{"a":1}]}
+
{"inputs":[0,0],"output":[{"a":0}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx new file mode 100644 index 0000000000000..90b7597694607 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitivity-phi-assign-or-capture.tsx @@ -0,0 +1,32 @@ +import {useMemo} from 'react'; +import { + typedCapture, + typedCreateFrom, + typedMutate, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + const x = useMemo(() => [{a}], [a, b]); + let z: any; + if (b) { + z = x; + } else { + z = typedCapture(x); + } + // could mutate x + typedMutate(z, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 0, b: 1}, + {a: 1, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md new file mode 100644 index 0000000000000..d3378b4d482bb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.expect.md @@ -0,0 +1,119 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel + +import {useMemo} from 'react'; +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = useMemo(() => makeObject_Primitives(a), [a]); + + // freeze the value + useIdentity(x); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // but x2 is frozen so this downgrades to a read. + // x should *not* take b as a dependency + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel + +import { useMemo } from "react"; +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + const { a, b } = t0; + let t1; + if ($[0] !== a) { + t1 = makeObject_Primitives(a); + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + + useIdentity(x); + + const x2 = typedIdentity(x); + + identity(x2, b); + let t2; + if ($[2] !== a) { + t2 = [a]; + $[2] = a; + $[3] = t2; + } else { + t2 = $[3]; + } + let t3; + if ($[4] !== t2 || $[5] !== x) { + t3 = ; + $[4] = t2; + $[5] = x; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 0, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0],"output":{"a":0,"b":"value1","c":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js new file mode 100644 index 0000000000000..d0f677ee4df17 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-frozen-input.js @@ -0,0 +1,40 @@ +// @enableNewMutationAliasingModel + +import {useMemo} from 'react'; +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = useMemo(() => makeObject_Primitives(a), [a]); + + // freeze the value + useIdentity(x); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // but x2 is frozen so this downgrades to a read. + // x should *not* take b as a dependency + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md new file mode 100644 index 0000000000000..17fed05d93d4d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.expect.md @@ -0,0 +1,112 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // and x is still mutable so + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from "shared-runtime"; + +function Component(t0) { + const $ = _c(9); + const { a, b } = t0; + let x; + if ($[0] !== a || $[1] !== b) { + x = makeObject_Primitives(a); + + const x2 = typedIdentity(x); + + identity(x2, b); + $[0] = a; + $[1] = b; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== a || $[4] !== b) { + t1 = [a, b]; + $[3] = a; + $[4] = b; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1 || $[7] !== x) { + t2 = ; + $[6] = t1; + $[7] = x; + $[8] = t2; + } else { + t2 = $[8]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: 0 }], + sequentialRenders: [ + { a: 0, b: 0 }, + { a: 1, b: 0 }, + { a: 1, b: 1 }, + { a: 0, b: 1 }, + { a: 0, b: 0 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1,0],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[1,1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0,1],"output":{"a":0,"b":"value1","c":true}}
+
{"inputs":[0,0],"output":{"a":0,"b":"value1","c":true}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js new file mode 100644 index 0000000000000..719c89d11de2f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/typed-identity-function-mutable-input.js @@ -0,0 +1,35 @@ +// @enableNewMutationAliasingModel + +import { + identity, + makeObject_Primitives, + typedIdentity, + useIdentity, + ValidateMemoization, +} from 'shared-runtime'; + +function Component({a, b}) { + // create a mutable value with input `a` + const x = makeObject_Primitives(a); + + // known to pass-through via aliasing signature + const x2 = typedIdentity(x); + + // Unknown function so we assume it conditionally mutates, + // and x is still mutable so + identity(x2, b); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 0}], + sequentialRenders: [ + {a: 0, b: 0}, + {a: 1, b: 0}, + {a: 1, b: 1}, + {a: 0, b: 1}, + {a: 0, b: 0}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md new file mode 100644 index 0000000000000..e33f52396d5e5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md @@ -0,0 +1,102 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Foo({arr1, arr2, foo}) { + const x = [arr1]; + + let y = []; + + const getVal1 = useCallback(() => { + return {x: 2}; + }, []); + + const getVal2 = useCallback(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{arr1: [1, 2], arr2: [3, 4], foo: true}], + sequentialRenders: [ + {arr1: [1, 2], arr2: [3, 4], foo: true}, + {arr1: [1, 2], arr2: [3, 4], foo: false}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +function Foo(t0) { + const $ = _c(10); + const { arr1, arr2, foo } = t0; + let t1; + if ($[0] !== arr1) { + t1 = [arr1]; + $[0] = arr1; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let getVal1; + let t2; + if ($[2] !== arr2 || $[3] !== foo || $[4] !== x) { + let y = []; + + getVal1 = _temp; + + t2 = () => [y]; + foo ? (y = x.concat(arr2)) : y; + $[2] = arr2; + $[3] = foo; + $[4] = x; + $[5] = getVal1; + $[6] = t2; + } else { + getVal1 = $[5]; + t2 = $[6]; + } + const getVal2 = t2; + let t3; + if ($[7] !== getVal1 || $[8] !== getVal2) { + t3 = ; + $[7] = getVal1; + $[8] = getVal2; + $[9] = t3; + } else { + t3 = $[9]; + } + return t3; +} +function _temp() { + return { x: 2 }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }], + sequentialRenders: [ + { arr1: [1, 2], arr2: [3, 4], foo: true }, + { arr1: [1, 2], arr2: [3, 4], foo: false }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[[1,2],3,4]]},"shouldInvokeFns":true}
+
{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[]]},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx new file mode 100644 index 0000000000000..08b9e4b2faa6c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx @@ -0,0 +1,28 @@ +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Foo({arr1, arr2, foo}) { + const x = [arr1]; + + let y = []; + + const getVal1 = useCallback(() => { + return {x: 2}; + }, []); + + const getVal2 = useCallback(() => { + return [y]; + }, [foo ? (y = x.concat(arr2)) : y]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{arr1: [1, 2], arr2: [3, 4], foo: true}], + sequentialRenders: [ + {arr1: [1, 2], arr2: [3, 4], foo: true}, + {arr1: [1, 2], arr2: [3, 4], foo: false}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md new file mode 100644 index 0000000000000..d37762bbac530 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md @@ -0,0 +1,85 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + const getVal = useCallback(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useCallback } from "react"; +import { Stringify } from "shared-runtime"; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const $ = _c(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let t1; + if ($[2] !== arr2 || $[3] !== x) { + let y; + t1 = () => ({ y }); + + (y = x.concat(arr2)), y; + $[2] = arr2; + $[3] = x; + $[4] = t1; + } else { + t1 = $[4]; + } + const getVal = t1; + let t2; + if ($[5] !== getVal) { + t2 = ; + $[5] = getVal; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +### Eval output +(kind: ok)
{"getVal":{"kind":"Function","result":{"y":[[1,2],3,4]}},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx new file mode 100644 index 0000000000000..43e2dfbb0504a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx @@ -0,0 +1,23 @@ +// @enableNewMutationAliasingModel +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +// We currently produce invalid output (incorrect scoping for `y` declaration) +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + const getVal = useCallback(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md new file mode 100644 index 0000000000000..926887a7a448b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md @@ -0,0 +1,75 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {useMemo} from 'react'; + +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + return useMemo(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { useMemo } from "react"; + +function useFoo(arr1, arr2) { + const $ = _c(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let y; + if ($[2] !== arr2 || $[3] !== x) { + (y = x.concat(arr2)), y; + $[2] = arr2; + $[3] = x; + $[4] = y; + } else { + y = $[4]; + } + let t1; + if ($[5] !== y) { + t1 = { y }; + $[5] = y; + $[6] = t1; + } else { + t1 = $[6]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; + +``` + +### Eval output +(kind: ok) {"y":[[1,2],3,4]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts new file mode 100644 index 0000000000000..5b7d799d68b13 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts @@ -0,0 +1,19 @@ +// @enableNewMutationAliasingModel +import {useMemo} from 'react'; + +function useFoo(arr1, arr2) { + const x = [arr1]; + + let y; + return useMemo(() => { + return {y}; + }, [((y = x.concat(arr2)), y)]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [ + [1, 2], + [3, 4], + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md new file mode 100644 index 0000000000000..8b4dbc8f863d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +function Component({a, b, c}) { + // This is an object version of array-access-assignment.js + // Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings + // works equivalently to array expressions and property accesses with numeric indices + const x = {zero: a}; + const y = {zero: null, one: b}; + const z = {zero: {}, one: {}, two: {zero: c}}; + x.zero = y.one; + z.zero.zero = x.zero; + return {zero: x, one: z}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1, b: 20, c: 300}], + sequentialRenders: [ + {a: 2, b: 20, c: 300}, + {a: 3, b: 20, c: 300}, + {a: 3, b: 21, c: 300}, + {a: 3, b: 22, c: 300}, + {a: 3, b: 22, c: 301}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component(t0) { + const $ = _c(6); + const { a, b, c } = t0; + let t1; + if ($[0] !== a || $[1] !== b || $[2] !== c) { + const x = { zero: a }; + let t2; + if ($[4] !== b) { + t2 = { zero: null, one: b }; + $[4] = b; + $[5] = t2; + } else { + t2 = $[5]; + } + const y = t2; + const z = { zero: {}, one: {}, two: { zero: c } }; + x.zero = y.one; + z.zero.zero = x.zero; + t1 = { zero: x, one: z }; + $[0] = a; + $[1] = b; + $[2] = c; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 1, b: 20, c: 300 }], + sequentialRenders: [ + { a: 2, b: 20, c: 300 }, + { a: 3, b: 20, c: 300 }, + { a: 3, b: 21, c: 300 }, + { a: 3, b: 22, c: 300 }, + { a: 3, b: 22, c: 301 }, + ], +}; + +``` + +### Eval output +(kind: ok) {"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}} +{"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}} +{"zero":{"zero":21},"one":{"zero":{"zero":21},"one":{},"two":{"zero":300}}} +{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":300}}} +{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":301}}} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js new file mode 100644 index 0000000000000..ef047238e7f84 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js @@ -0,0 +1,23 @@ +function Component({a, b, c}) { + // This is an object version of array-access-assignment.js + // Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings + // works equivalently to array expressions and property accesses with numeric indices + const x = {zero: a}; + const y = {zero: null, one: b}; + const z = {zero: {}, one: {}, two: {zero: c}}; + x.zero = y.one; + z.zero.zero = x.zero; + return {zero: x, one: z}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1, b: 20, c: 300}], + sequentialRenders: [ + {a: 2, b: 20, c: 300}, + {a: 3, b: 20, c: 300}, + {a: 3, b: 21, c: 300}, + {a: 3, b: 22, c: 300}, + {a: 3, b: 22, c: 301}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md index bf0f9da6b1da1..f187c8c79d369 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-key-object-mutated-later.expect.md @@ -27,34 +27,18 @@ import { c as _c } from "react/compiler-runtime"; import { identity, mutate } from "shared-runtime"; function Component(props) { - const $ = _c(5); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = {}; - $[0] = t0; + const $ = _c(2); + let context; + if ($[0] !== props.value) { + const key = {}; + context = { [key]: identity([props.value]) }; + + mutate(key); + $[0] = props.value; + $[1] = context; } else { - t0 = $[0]; + context = $[1]; } - const key = t0; - let t1; - if ($[1] !== props.value) { - t1 = identity([props.value]); - $[1] = props.value; - $[2] = t1; - } else { - t1 = $[2]; - } - let t2; - if ($[3] !== t1) { - t2 = { [key]: t1 }; - $[3] = t1; - $[4] = t2; - } else { - t2 = $[4]; - } - const context = t2; - - mutate(key); return context; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md index 810b03e529e77..01d91de8d4fb7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-expression-computed-member.expect.md @@ -27,11 +27,22 @@ import { c as _c } from "react/compiler-runtime"; import { identity, mutate, mutateAndReturn } from "shared-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(4); let context; if ($[0] !== props.value) { const key = { a: "key" }; - context = { [key.a]: identity([props.value]) }; + + const t0 = key.a; + const t1 = identity([props.value]); + let t2; + if ($[2] !== t1) { + t2 = { [t0]: t1 }; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } + context = t2; mutate(key); $[0] = props.value; 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 3dd8e730325ea..4c2bced216ff4 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 @@ -42,36 +42,34 @@ function Component(t0) { arg?.items.edges?.nodes; let t1; - let t2; if ($[0] !== arg?.items.edges?.nodes) { - t2 = arg?.items.edges?.nodes.map(identity); + t1 = arg?.items.edges?.nodes.map(identity); $[0] = arg?.items.edges?.nodes; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const data = t1; - const t3 = arg?.items.edges?.nodes; - let t4; - if ($[2] !== t3) { - t4 = [t3]; - $[2] = t3; - $[3] = t4; + const t2 = arg?.items.edges?.nodes; + let t3; + if ($[2] !== t2) { + t3 = [t2]; + $[2] = t2; + $[3] = t3; } else { - t4 = $[3]; + t3 = $[3]; } - let t5; - if ($[4] !== data || $[5] !== t4) { - t5 = ; + let t4; + if ($[4] !== data || $[5] !== t3) { + t4 = ; $[4] = data; - $[5] = t4; - $[6] = t5; + $[5] = t3; + $[6] = t4; } else { - t5 = $[6]; + t4 = $[6]; } - return t5; + return t4; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md index 98fcfbe7f0f6f..73c88e2c493c1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-inverted-optionals-parallel-paths.expect.md @@ -23,21 +23,19 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(2); - let t0; const x$0 = []; x$0.push(props?.a.b?.c.d?.e); x$0.push(props.a?.b.c?.d.e); - t0 = x$0; - let t1; + let t0; if ($[0] !== props.a.b.c.d.e) { - t1 = ; + t0 = ; $[0] = props.a.b.c.d.e; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - return t1; + return t0; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md index 3cd9877813c58..95ebf3f95f9e7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single-with-unconditional.expect.md @@ -23,7 +23,6 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(7); - let t0; let x; if ($[0] !== props.items) { x = []; @@ -34,26 +33,25 @@ function Component(props) { } else { x = $[1]; } - t0 = x; - const data = t0; - let t1; + const data = x; + let t0; if ($[2] !== props.items) { - t1 = [props.items]; + t0 = [props.items]; $[2] = props.items; - $[3] = t1; + $[3] = t0; } else { - t1 = $[3]; + t0 = $[3]; } - let t2; - if ($[4] !== data || $[5] !== t1) { - t2 = ; + let t1; + if ($[4] !== data || $[5] !== t0) { + t1 = ; $[4] = data; - $[5] = t1; - $[6] = t2; + $[5] = t0; + $[6] = t1; } else { - t2 = $[6]; + t1 = $[6]; } - return t2; + return t1; } ``` 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 60a6171ab1ea1..43476f1604b49 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 @@ -38,7 +38,6 @@ function Component(t0) { const { arg } = t0; arg?.items; - let t1; let x; if ($[0] !== arg?.items) { x = []; @@ -48,27 +47,26 @@ function Component(t0) { } else { x = $[1]; } - t1 = x; - const data = t1; - const t2 = arg?.items; - let t3; - if ($[2] !== t2) { - t3 = [t2]; - $[2] = t2; - $[3] = t3; + const data = x; + const t1 = arg?.items; + let t2; + if ($[2] !== t1) { + t2 = [t1]; + $[2] = t1; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - let t4; - if ($[4] !== data || $[5] !== t3) { - t4 = ; + let t3; + if ($[4] !== data || $[5] !== t2) { + t3 = ; $[4] = data; - $[5] = t3; - $[6] = t4; + $[5] = t2; + $[6] = t3; } else { - t4 = $[6]; + t3 = $[6]; } - return t4; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md index bbbb0c0cb88e0..b1a65cab02e21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/maybe-invalid-useMemo-no-memoblock-sideeffect.expect.md @@ -36,11 +36,9 @@ import { useMemo } from "react"; // (i.e. inferred non-mutable or non-escaping values don't get memoized) function useFoo(t0) { const { minWidth, styles, setStyles } = t0; - let t1; if (styles.width > minWidth) { setStyles(styles); } - t1 = undefined; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/prune-nonescaping-useMemo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/prune-nonescaping-useMemo.expect.md index 27759f5db30fd..228790c4e274f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/prune-nonescaping-useMemo.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/prune-nonescaping-useMemo.expect.md @@ -34,10 +34,7 @@ import { identity } from "shared-runtime"; * This is technically a false positive, although it makes sense * to bailout as source code might be doing something sketchy. */ -function useFoo(x) { - let t0; - t0 = identity(x); -} +function useFoo(x) {} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md index 2ebfe4e4116fe..cf36d6ed67308 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/todo-ensure-constant-prop-decls-get-removed.expect.md @@ -40,14 +40,12 @@ function useFoo() { const $ = _c(1); const constVal = 0; let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = [0]; - $[0] = t1; + t0 = [0]; + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; return t0; } 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 080cc0a74a609..18e0621d62669 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 @@ -40,39 +40,46 @@ import { useCallback } from "react"; import { Stringify } from "shared-runtime"; function Foo(t0) { - const $ = _c(8); + const $ = _c(10); const { arr1, arr2, foo } = t0; - let getVal1; let t1; - if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) { - const x = [arr1]; - + if ($[0] !== arr1) { + t1 = [arr1]; + $[0] = arr1; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let getVal1; + let t2; + if ($[2] !== arr2 || $[3] !== foo || $[4] !== x) { let y = []; getVal1 = _temp; - t1 = () => [y]; + t2 = () => [y]; foo ? (y = x.concat(arr2)) : y; - $[0] = arr1; - $[1] = arr2; - $[2] = foo; - $[3] = getVal1; - $[4] = t1; + $[2] = arr2; + $[3] = foo; + $[4] = x; + $[5] = getVal1; + $[6] = t2; } else { - getVal1 = $[3]; - t1 = $[4]; + getVal1 = $[5]; + t2 = $[6]; } - const getVal2 = t1; - let t2; - if ($[5] !== getVal1 || $[6] !== getVal2) { - t2 = ; - $[5] = getVal1; - $[6] = getVal2; - $[7] = t2; + const getVal2 = t2; + let t3; + if ($[7] !== getVal1 || $[8] !== getVal2) { + t3 = ; + $[7] = getVal1; + $[8] = getVal2; + $[9] = t3; } else { - t2 = $[7]; + t3 = $[9]; } - return t2; + return t3; } function _temp() { return { x: 2 }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md index 89a6ad80c3945..f7ebdf0ca59dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-depslist-assignment.expect.md @@ -36,31 +36,38 @@ import { Stringify } from "shared-runtime"; // We currently produce invalid output (incorrect scoping for `y` declaration) function useFoo(arr1, arr2) { - const $ = _c(5); + const $ = _c(7); let t0; - if ($[0] !== arr1 || $[1] !== arr2) { - const x = [arr1]; - - let y; - t0 = () => ({ y }); - - (y = x.concat(arr2)), y; + if ($[0] !== arr1) { + t0 = [arr1]; $[0] = arr1; - $[1] = arr2; - $[2] = t0; + $[1] = t0; } else { - t0 = $[2]; + t0 = $[1]; } - const getVal = t0; + const x = t0; let t1; - if ($[3] !== getVal) { - t1 = ; - $[3] = getVal; + if ($[2] !== arr2 || $[3] !== x) { + let y; + t1 = () => ({ y }); + + (y = x.concat(arr2)), y; + $[2] = arr2; + $[3] = x; $[4] = t1; } else { t1 = $[4]; } - return t1; + const getVal = t1; + let t2; + if ($[5] !== getVal) { + t2 = ; + $[5] = getVal; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md index 1745a6b4dbc2a..80e4106303483 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-alias-property-load-dep.expect.md @@ -32,16 +32,14 @@ function Component(t0) { const { propA, propB } = t0; const x = propB.x.y; let t1; - let t2; if ($[0] !== propA.x || $[1] !== x) { - t2 = sum(propA.x, x); + t1 = sum(propA.x, x); $[0] = propA.x; $[1] = x; - $[2] = t2; + $[2] = t1; } else { - t2 = $[2]; + t1 = $[2]; } - t1 = t2; return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md index f7353ddd5e97c..697234a02976a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-alloc.expect.md @@ -32,28 +32,26 @@ import { identity } from "shared-runtime"; function Component(t0) { const $ = _c(5); const { propA, propB } = t0; - let t1; - const t2 = propB?.x.y; - let t3; - if ($[0] !== t2) { - t3 = identity(t2); - $[0] = t2; - $[1] = t3; + const t1 = propB?.x.y; + let t2; + if ($[0] !== t1) { + t2 = identity(t1); + $[0] = t1; + $[1] = t2; } else { - t3 = $[1]; + t2 = $[1]; } - let t4; - if ($[2] !== propA || $[3] !== t3) { - t4 = { value: t3, other: propA }; + let t3; + if ($[2] !== propA || $[3] !== t2) { + t3 = { value: t2, other: propA }; $[2] = propA; - $[3] = t3; - $[4] = t4; + $[3] = t2; + $[4] = t3; } else { - t4 = $[4]; + t3 = $[4]; } - t1 = t4; - return t1; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md index c6ad9bcdac356..3a279219dad7c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-conditional-access-noAlloc.expect.md @@ -30,20 +30,18 @@ import { useMemo } from "react"; function Component(t0) { const $ = _c(3); const { propA, propB } = t0; - let t1; - const t2 = propB?.x.y; - let t3; - if ($[0] !== propA || $[1] !== t2) { - t3 = { value: t2, other: propA }; + const t1 = propB?.x.y; + let t2; + if ($[0] !== propA || $[1] !== t1) { + t2 = { value: t1, other: propA }; $[0] = propA; - $[1] = t2; - $[2] = t3; + $[1] = t1; + $[2] = t2; } else { - t3 = $[2]; + t2 = $[2]; } - t1 = t3; - return t1; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md index 06add73ec849d..918fb3d656886 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-constant-prop.expect.md @@ -37,39 +37,35 @@ function useFoo(cond) { const $ = _c(5); const sourceDep = 0; let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = identity(0); - $[0] = t1; + t0 = identity(0); + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; const derived1 = t0; const derived2 = (cond ?? Math.min(0, 1)) ? 1 : 2; - let t2; - let t3; + let t1; if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t3 = identity(0); - $[1] = t3; + t1 = identity(0); + $[1] = t1; } else { - t3 = $[1]; + t1 = $[1]; } - t2 = t3; - const derived3 = t2; + const derived3 = t1; const derived4 = (Math.min(0, -1) ?? cond) ? 1 : 2; - let t4; + let t2; if ($[2] !== derived2 || $[3] !== derived4) { - t4 = [derived1, derived2, derived3, derived4]; + t2 = [derived1, derived2, derived3, derived4]; $[2] = derived2; $[3] = derived4; - $[4] = t4; + $[4] = t2; } else { - t4 = $[4]; + t2 = $[4]; } - return t4; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.expect.md index dafe2b63e35ae..85a27553d7114 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.expect.md @@ -48,15 +48,13 @@ function Foo(props) { } const x = t0; let t1; - let t2; if ($[2] !== x[0]) { - t2 = [x[0]]; + t1 = [x[0]]; $[2] = x[0]; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - t1 = t2; return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md index 7a27bb8521ed5..a3f36517ffff9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-in-other-reactive-block.expect.md @@ -42,19 +42,17 @@ function useFoo(minWidth, otherProp) { let t0; if ($[0] !== minWidth || $[1] !== otherProp || $[2] !== width) { const x = []; - let t1; - const t2 = Math.max(minWidth, width); - let t3; - if ($[4] !== t2) { - t3 = { width: t2 }; - $[4] = t2; - $[5] = t3; + const t1 = Math.max(minWidth, width); + let t2; + if ($[4] !== t1) { + t2 = { width: t1 }; + $[4] = t1; + $[5] = t2; } else { - t3 = $[5]; + t2 = $[5]; } - t1 = t3; - const style = t1; + const style = t2; arrayPush(x, otherProp); t0 = [style, x]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md index 9bc7606498668..27dbb57698aec 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-fewer-deps.expect.md @@ -29,15 +29,13 @@ import { useMemo } from "react"; function useFoo(a, b) { const $ = _c(2); let t0; - let t1; if ($[0] !== a) { - t1 = [a]; + t0 = [a]; $[0] = a; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-more-specific.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-more-specific.expect.md index 8406f28c0d533..fa68ee4669547 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-more-specific.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-more-specific.expect.md @@ -37,15 +37,13 @@ import { useMemo } from "react"; function useHook(x) { const $ = _c(2); let t0; - let t1; if ($[0] !== x.y.z) { - t1 = [x.y.z]; + t0 = [x.y.z]; $[0] = x.y.z; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md index 22b5cc05befba..ddcb2257dc02d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-nonallocating.expect.md @@ -29,9 +29,7 @@ import { useMemo } from "react"; // It's correct to infer a useMemo value is non-allocating // and not provide it with a reactive scope function useFoo(num1, num2) { - let t0; - t0 = Math.min(num1, num2); - return t0; + return Math.min(num1, num2); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md index f08183d9cab5c..2a805aa77757b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-infer-scope-global.expect.md @@ -31,14 +31,12 @@ import { CONST_STRING0 } from "shared-runtime"; function useFoo() { const $ = _c(1); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = [CONST_STRING0]; - $[0] = t1; + t0 = [CONST_STRING0]; + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md index e541cbe840e19..bd812aa404c65 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-inner-decl.expect.md @@ -30,25 +30,23 @@ import { identity } from "shared-runtime"; function useFoo(data) { const $ = _c(4); let t0; - let t1; if ($[0] !== data.a) { - t1 = identity(data.a); + t0 = identity(data.a); $[0] = data.a; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - const temp = t1; - let t2; + const temp = t0; + let t1; if ($[2] !== temp) { - t2 = { temp }; + t1 = { temp }; $[2] = temp; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - t0 = t2; - return t0; + return t1; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md index 04d6b1f2e91c5..d41c67039175d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-invoke-prop.expect.md @@ -35,15 +35,13 @@ function useFoo(t0) { const $ = _c(2); const { callback } = t0; let t1; - let t2; if ($[0] !== callback) { - t2 = new Array(callback()); + t1 = new Array(callback()); $[0] = callback; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md index 3fffec6a7dc20..c224f1d17ec7d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-assignment.expect.md @@ -30,29 +30,34 @@ import { c as _c } from "react/compiler-runtime"; import { useMemo } from "react"; function useFoo(arr1, arr2) { - const $ = _c(5); + const $ = _c(7); + let t0; + if ($[0] !== arr1) { + t0 = [arr1]; + $[0] = arr1; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; let y; - if ($[0] !== arr1 || $[1] !== arr2) { - const x = [arr1]; - + if ($[2] !== arr2 || $[3] !== x) { (y = x.concat(arr2)), y; - $[0] = arr1; - $[1] = arr2; - $[2] = y; + $[2] = arr2; + $[3] = x; + $[4] = y; } else { - y = $[2]; + y = $[4]; } - let t0; let t1; - if ($[3] !== y) { + if ($[5] !== y) { t1 = { y }; - $[3] = y; - $[4] = t1; + $[5] = y; + $[6] = t1; } else { - t1 = $[4]; + t1 = $[6]; } - t0 = t1; - return t0; + return t1; } export const FIXTURE_ENTRYPOINT = { 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 4c452dfabd1e9..d8a20367c9179 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 @@ -49,14 +49,12 @@ function Foo(t0) { let y = []; let t2; - let t3; if ($[5] === Symbol.for("react.memo_cache_sentinel")) { - t3 = { x: 2 }; - $[5] = t3; + t2 = { x: 2 }; + $[5] = t2; } else { - t3 = $[5]; + t2 = $[5]; } - t2 = t3; val1 = t2; foo ? (y = x.concat(arr2)) : y; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md index 85026f1f58c9c..bb0518c25fdfd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-with-no-depslist.expect.md @@ -33,15 +33,13 @@ function Component(t0) { const $ = _c(2); const { propA } = t0; let t1; - let t2; if ($[0] !== propA) { - t2 = [propA]; + t1 = [propA]; $[0] = propA; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; return t1; } 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 index 28612f2d738dd..f763eea4f105f 100644 --- 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 @@ -42,36 +42,34 @@ function Component(t0) { arg?.items.edges?.nodes; let t1; - let t2; if ($[0] !== arg?.items.edges?.nodes) { - t2 = arg?.items.edges?.nodes.map(identity); + t1 = arg?.items.edges?.nodes.map(identity); $[0] = arg?.items.edges?.nodes; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const data = t1; - const t3 = arg?.items.edges?.nodes; - let t4; - if ($[2] !== t3) { - t4 = [t3]; - $[2] = t3; - $[3] = t4; + const t2 = arg?.items.edges?.nodes; + let t3; + if ($[2] !== t2) { + t3 = [t2]; + $[2] = t2; + $[3] = t3; } else { - t4 = $[3]; + t3 = $[3]; } - let t5; - if ($[4] !== data || $[5] !== t4) { - t5 = ; + let t4; + if ($[4] !== data || $[5] !== t3) { + t4 = ; $[4] = data; - $[5] = t4; - $[6] = t5; + $[5] = t3; + $[6] = t4; } else { - t5 = $[6]; + t4 = $[6]; } - return t5; + return t4; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-inverted-optionals-parallel-paths.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-inverted-optionals-parallel-paths.expect.md index 60ae4e49d328c..f0dbc3448ba37 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-inverted-optionals-parallel-paths.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-inverted-optionals-parallel-paths.expect.md @@ -23,21 +23,19 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(2); - let t0; const x$0 = []; x$0.push(props?.a.b?.c.d?.e); x$0.push(props.a?.b.c?.d.e); - t0 = x$0; - let t1; + let t0; if ($[0] !== props.a.b.c.d.e) { - t1 = ; + t0 = ; $[0] = props.a.b.c.d.e; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - return t1; + return t0; } ``` 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 index 2861ab71c6e97..5600df5683c85 100644 --- 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 @@ -23,7 +23,6 @@ import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMe import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(7); - let t0; let x; if ($[0] !== props.items) { x = []; @@ -34,26 +33,25 @@ function Component(props) { } else { x = $[1]; } - t0 = x; - const data = t0; - let t1; + const data = x; + let t0; if ($[2] !== props.items) { - t1 = [props.items]; + t0 = [props.items]; $[2] = props.items; - $[3] = t1; + $[3] = t0; } else { - t1 = $[3]; + t0 = $[3]; } - let t2; - if ($[4] !== data || $[5] !== t1) { - t2 = ; + let t1; + if ($[4] !== data || $[5] !== t0) { + t1 = ; $[4] = data; - $[5] = t1; - $[6] = t2; + $[5] = t0; + $[6] = t1; } else { - t2 = $[6]; + t1 = $[6]; } - return t2; + return t1; } ``` 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 index b5db44aa2b6c0..8e55da462efa7 100644 --- 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 @@ -38,7 +38,6 @@ function Component(t0) { const { arg } = t0; arg?.items; - let t1; let x; if ($[0] !== arg?.items) { x = []; @@ -48,27 +47,26 @@ function Component(t0) { } else { x = $[1]; } - t1 = x; - const data = t1; - const t2 = arg?.items; - let t3; - if ($[2] !== t2) { - t3 = [t2]; - $[2] = t2; - $[3] = t3; + const data = x; + const t1 = arg?.items; + let t2; + if ($[2] !== t1) { + t2 = [t1]; + $[2] = t1; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - let t4; - if ($[4] !== data || $[5] !== t3) { - t4 = ; + let t3; + if ($[4] !== data || $[5] !== t2) { + t3 = ; $[4] = data; - $[5] = t3; - $[6] = t4; + $[5] = t2; + $[6] = t3; } else { - t4 = $[6]; + t3 = $[6]; } - return t4; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/props-method-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/props-method-dependency.expect.md index 4c5da20170754..b84e6e44eab35 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/props-method-dependency.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/props-method-dependency.expect.md @@ -31,34 +31,32 @@ import { ValidateMemoization } from "shared-runtime"; function Component(props) { const $ = _c(7); let t0; - let t1; if ($[0] !== props.x) { - t1 = props.x(); + t0 = props.x(); $[0] = props.x; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const x = t0; - let t2; + let t1; if ($[2] !== props.x) { - t2 = [props.x]; + t1 = [props.x]; $[2] = props.x; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== t2 || $[5] !== x) { - t3 = ; - $[4] = t2; + let t2; + if ($[4] !== t1 || $[5] !== x) { + t2 = ; + $[4] = t1; $[5] = x; - $[6] = t3; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } const f = () => ["React"]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function.expect.md index 023e129625cf6..3dd1f745c1b04 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/recursive-function.expect.md @@ -23,9 +23,7 @@ function foo(x) { if (x <= 0) { return 0; } - let t0; - t0 = foo(x - 2); - return x + foo(x - 1) + t0; + return x + foo(x - 1) + foo(x - 2); } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reordering-across-blocks.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reordering-across-blocks.expect.md index a2bd99e9a7691..aba6e5dc8aa3c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reordering-across-blocks.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reordering-across-blocks.expect.md @@ -59,48 +59,46 @@ function Component(t0) { const $ = _c(9); const { config } = t0; let t1; - let t2; if ($[0] !== config) { - t2 = (event) => { + t1 = (event) => { config?.onA?.(event); }; $[0] = config; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - const a = t2; - let t3; + const a = t1; + let t2; if ($[2] !== config) { - t3 = (event_0) => { + t2 = (event_0) => { config?.onB?.(event_0); }; $[2] = config; - $[3] = t3; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } - const b = t3; - let t4; + const b = t2; + let t3; if ($[4] !== a || $[5] !== b) { - t4 = { b, a }; + t3 = { b, a }; $[4] = a; $[5] = b; - $[6] = t4; + $[6] = t3; } else { - t4 = $[6]; + t3 = $[6]; } - t1 = t4; - const object = t1; - let t5; + const object = t3; + let t4; if ($[7] !== object) { - t5 = ; + t4 = ; $[7] = object; - $[8] = t5; + $[8] = t4; } else { - t5 = $[8]; + t4 = $[8]; } - return t5; + return t4; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.expect.md new file mode 100644 index 0000000000000..5a866044bde26 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.expect.md @@ -0,0 +1,104 @@ + +## Input + +```javascript +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel +import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime'; + +/** + * Repro of a bug fixed in the new aliasing model. + * + * 1. `InferMutableRanges` derives the mutable range of identifiers and their + * aliases from `LoadLocal`, `PropertyLoad`, etc + * - After this pass, y's mutable range only extends to `arrayPush(x, y)` + * - We avoid assigning mutable ranges to loads after y's mutable range, as + * these are working with an immutable value. As a result, `LoadLocal y` and + * `PropertyLoad y` do not get mutable ranges + * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes, + * as according to the 'co-mutation' of different values + * - Here, we infer that + * - `arrayPush(y, x)` might alias `x` and `y` to each other + * - `setPropertyKey(x, ...)` may mutate both `x` and `y` + * - This pass correctly extends the mutable range of `y` + * - Since we didn't run `InferMutableRange` logic again, the LoadLocal / + * PropertyLoads still don't have a mutable range + * + * Note that the this bug is an edge case. Compiler output is only invalid for: + * - function expressions with + * `enableTransitivelyFreezeFunctionExpressions:false` + * - functions that throw and get retried without clearing the memocache + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}
+ * Forget: + * (kind: ok) + *
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+ */ +function useFoo({a, b}: {a: number, b: number}) { + const x = []; + const y = {value: a}; + + arrayPush(x, y); // x and y co-mutate + const y_alias = y; + const cb = () => y_alias.value; + setPropertyByKey(x[0], 'value', b); // might overwrite y.value + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{a: 2, b: 10}], + sequentialRenders: [ + {a: 2, b: 10}, + {a: 2, b: 11}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime"; + +function useFoo(t0) { + const $ = _c(3); + const { a, b } = t0; + let t1; + if ($[0] !== a || $[1] !== b) { + const x = []; + const y = { value: a }; + + arrayPush(x, y); + const y_alias = y; + const cb = () => y_alias.value; + setPropertyByKey(x[0], "value", b); + t1 = ; + $[0] = a; + $[1] = b; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ a: 2, b: 10 }], + sequentialRenders: [ + { a: 2, b: 10 }, + { a: 2, b: 11 }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+
{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.js new file mode 100644 index 0000000000000..df9e294261e3e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.js @@ -0,0 +1,55 @@ +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel +import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime'; + +/** + * Repro of a bug fixed in the new aliasing model. + * + * 1. `InferMutableRanges` derives the mutable range of identifiers and their + * aliases from `LoadLocal`, `PropertyLoad`, etc + * - After this pass, y's mutable range only extends to `arrayPush(x, y)` + * - We avoid assigning mutable ranges to loads after y's mutable range, as + * these are working with an immutable value. As a result, `LoadLocal y` and + * `PropertyLoad y` do not get mutable ranges + * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes, + * as according to the 'co-mutation' of different values + * - Here, we infer that + * - `arrayPush(y, x)` might alias `x` and `y` to each other + * - `setPropertyKey(x, ...)` may mutate both `x` and `y` + * - This pass correctly extends the mutable range of `y` + * - Since we didn't run `InferMutableRange` logic again, the LoadLocal / + * PropertyLoads still don't have a mutable range + * + * Note that the this bug is an edge case. Compiler output is only invalid for: + * - function expressions with + * `enableTransitivelyFreezeFunctionExpressions:false` + * - functions that throw and get retried without clearing the memocache + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}
+ * Forget: + * (kind: ok) + *
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}
+ */ +function useFoo({a, b}: {a: number, b: number}) { + const x = []; + const y = {value: a}; + + arrayPush(x, y); // x and y co-mutate + const y_alias = y; + const cb = () => y_alias.value; + setPropertyByKey(x[0], 'value', b); // might overwrite y.value + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{a: 2, b: 10}], + sequentialRenders: [ + {a: 2, b: 10}, + {a: 2, b: 11}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.expect.md new file mode 100644 index 0000000000000..1427ec8eb503d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.expect.md @@ -0,0 +1,84 @@ + +## Input + +```javascript +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel +import {setPropertyByKey, Stringify} from 'shared-runtime'; + +/** + * Variation of bug in `bug-aliased-capture-aliased-mutate`. + * Fixed in the new inference model. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}
+ * Forget: + * (kind: ok) + *
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+ */ + +function useFoo({a}: {a: number, b: number}) { + const arr = []; + const obj = {value: a}; + + setPropertyByKey(obj, 'arr', arr); + const obj_alias = obj; + const cb = () => obj_alias.arr.length; + for (let i = 0; i < a; i++) { + arr.push(i); + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 3}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { setPropertyByKey, Stringify } from "shared-runtime"; + +function useFoo(t0) { + const $ = _c(2); + const { a } = t0; + let t1; + if ($[0] !== a) { + const arr = []; + const obj = { value: a }; + + setPropertyByKey(obj, "arr", arr); + const obj_alias = obj; + const cb = () => obj_alias.arr.length; + for (let i = 0; i < a; i++) { + arr.push(i); + } + + t1 = ; + $[0] = a; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 3 }], +}; + +``` + +### Eval output +(kind: ok)
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+
{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.js new file mode 100644 index 0000000000000..2ed6941fa7f39 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.js @@ -0,0 +1,36 @@ +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel +import {setPropertyByKey, Stringify} from 'shared-runtime'; + +/** + * Variation of bug in `bug-aliased-capture-aliased-mutate`. + * Fixed in the new inference model. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) + *
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}
+ * Forget: + * (kind: ok) + *
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+ *
{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}
+ */ + +function useFoo({a}: {a: number, b: number}) { + const arr = []; + const obj = {value: a}; + + setPropertyByKey(obj, 'arr', arr); + const obj_alias = obj; + const cb = () => obj_alias.arr.length; + for (let i = 0; i < a; i++) { + arr.push(i); + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.expect.md new file mode 100644 index 0000000000000..f6b7ef3b43d5a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.expect.md @@ -0,0 +1,111 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {makeArray, mutate} from 'shared-runtime'; + +/** + * Bug repro, fixed in the new mutability/aliasing inference. + * + * Previous issue: + * + * Fork of `capturing-func-alias-captured-mutate`, but instead of directly + * aliasing `y` via `[y]`, we make an opaque call. + * + * Note that the bug here is that we don't infer that `a = makeArray(y)` + * potentially captures a context variable into a local variable. As a result, + * we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're + * currently inferring that this lambda captures `y` (for a potential later + * mutation) and simply reads `x`. + * + * Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not + * used when we analyze CallExpressions. + */ +function Component({foo, bar}: {foo: number; bar: number}) { + let x = {foo}; + let y: {bar: number; x?: {foo: number}} = {bar}; + const f0 = function () { + let a = makeArray(y); // a = [y] + let b = x; + // this writes y.x = x + a[0].x = b; + }; + f0(); + mutate(y.x); + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 3, bar: 4}], + sequentialRenders: [ + {foo: 3, bar: 4}, + {foo: 3, bar: 5}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { makeArray, mutate } from "shared-runtime"; + +/** + * Bug repro, fixed in the new mutability/aliasing inference. + * + * Previous issue: + * + * Fork of `capturing-func-alias-captured-mutate`, but instead of directly + * aliasing `y` via `[y]`, we make an opaque call. + * + * Note that the bug here is that we don't infer that `a = makeArray(y)` + * potentially captures a context variable into a local variable. As a result, + * we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're + * currently inferring that this lambda captures `y` (for a potential later + * mutation) and simply reads `x`. + * + * Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not + * used when we analyze CallExpressions. + */ +function Component(t0) { + const $ = _c(3); + const { foo, bar } = t0; + let y; + if ($[0] !== bar || $[1] !== foo) { + const x = { foo }; + y = { bar }; + const f0 = function () { + const a = makeArray(y); + const b = x; + + a[0].x = b; + }; + + f0(); + mutate(y.x); + $[0] = bar; + $[1] = foo; + $[2] = y; + } else { + y = $[2]; + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ foo: 3, bar: 4 }], + sequentialRenders: [ + { foo: 3, bar: 4 }, + { foo: 3, bar: 5 }, + ], +}; + +``` + +### Eval output +(kind: ok) {"bar":4,"x":{"foo":3,"wat0":"joe"}} +{"bar":5,"x":{"foo":3,"wat0":"joe"}} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.ts new file mode 100644 index 0000000000000..8b7bdeb79bee2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.ts @@ -0,0 +1,42 @@ +// @enableNewMutationAliasingModel +import {makeArray, mutate} from 'shared-runtime'; + +/** + * Bug repro, fixed in the new mutability/aliasing inference. + * + * Previous issue: + * + * Fork of `capturing-func-alias-captured-mutate`, but instead of directly + * aliasing `y` via `[y]`, we make an opaque call. + * + * Note that the bug here is that we don't infer that `a = makeArray(y)` + * potentially captures a context variable into a local variable. As a result, + * we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're + * currently inferring that this lambda captures `y` (for a potential later + * mutation) and simply reads `x`. + * + * Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not + * used when we analyze CallExpressions. + */ +function Component({foo, bar}: {foo: number; bar: number}) { + let x = {foo}; + let y: {bar: number; x?: {foo: number}} = {bar}; + const f0 = function () { + let a = makeArray(y); // a = [y] + let b = x; + // this writes y.x = x + a[0].x = b; + }; + f0(); + mutate(y.x); + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{foo: 3, bar: 4}], + sequentialRenders: [ + {foo: 3, bar: 4}, + {foo: 3, bar: 5}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.expect.md new file mode 100644 index 0000000000000..3896e6a2f2e6c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.expect.md @@ -0,0 +1,88 @@ + +## Input + +```javascript +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel +import {useCallback, useEffect, useRef} from 'react'; +import {useHook} from 'shared-runtime'; + +// This was a false positive "can't freeze mutable function" in the old +// inference model, fixed in the new inference model. +function Component() { + const params = useHook(); + const update = useCallback( + partialParams => { + const nextParams = { + ...params, + ...partialParams, + }; + nextParams.param = 'value'; + console.log(nextParams); + }, + [params] + ); + const ref = useRef(null); + useEffect(() => { + if (ref.current === null) { + update(); + } + }, [update]); + + return 'ok'; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel +import { useCallback, useEffect, useRef } from "react"; +import { useHook } from "shared-runtime"; + +// This was a false positive "can't freeze mutable function" in the old +// inference model, fixed in the new inference model. +function Component() { + const $ = _c(5); + const params = useHook(); + let t0; + if ($[0] !== params) { + t0 = (partialParams) => { + const nextParams = { ...params, ...partialParams }; + + nextParams.param = "value"; + console.log(nextParams); + }; + $[0] = params; + $[1] = t0; + } else { + t0 = $[1]; + } + const update = t0; + + const ref = useRef(null); + let t1; + let t2; + if ($[2] !== update) { + t1 = () => { + if (ref.current === null) { + update(); + } + }; + + t2 = [update]; + $[2] = update; + $[3] = t1; + $[4] = t2; + } else { + t1 = $[3]; + t2 = $[4]; + } + useEffect(t1, t2); + return "ok"; +} + +``` + +### 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/repro-false-positive-ref-validation-in-use-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.js new file mode 100644 index 0000000000000..3ecfcca9c7410 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.js @@ -0,0 +1,28 @@ +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel +import {useCallback, useEffect, useRef} from 'react'; +import {useHook} from 'shared-runtime'; + +// This was a false positive "can't freeze mutable function" in the old +// inference model, fixed in the new inference model. +function Component() { + const params = useHook(); + const update = useCallback( + partialParams => { + const nextParams = { + ...params, + ...partialParams, + }; + nextParams.param = 'value'; + console.log(nextParams); + }, + [params] + ); + const ref = useRef(null); + useEffect(() => { + if (ref.current === null) { + update(); + } + }, [update]); + + return 'ok'; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.expect.md new file mode 100644 index 0000000000000..65ff18b65ec0d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.expect.md @@ -0,0 +1,80 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime'; + +/** + * Fixture showing an edge case for ReactiveScope variable propagation. + * Fixed in the new inference model + * + * 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]; + useIdentity(null); + mutate(obj); + if (boxedInner[0] !== obj?.inner) { + throw new Error('invariant broken'); + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{arg: 0}], + sequentialRenders: [{arg: 0}, {arg: 1}], +}; + +``` + +## Code + +```javascript +// @enableNewMutationAliasingModel +import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime"; + +/** + * Fixture showing an edge case for ReactiveScope variable propagation. + * Fixed in the new inference model + * + * 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]; + useIdentity(null); + mutate(obj); + if (boxedInner[0] !== obj?.inner) { + throw new Error("invariant broken"); + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ arg: 0 }], + sequentialRenders: [{ arg: 0 }, { arg: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.tsx new file mode 100644 index 0000000000000..23c1a070104b2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.tsx @@ -0,0 +1,32 @@ +// @enableNewMutationAliasingModel +import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime'; + +/** + * Fixture showing an edge case for ReactiveScope variable propagation. + * Fixed in the new inference model + * + * 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]; + useIdentity(null); + mutate(obj); + if (boxedInner[0] !== obj?.inner) { + throw new Error('invariant broken'); + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{arg: 0}], + sequentialRenders: [{arg: 0}, {arg: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types-explicit-types.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types-explicit-types.expect.md index d35cbe266f631..1d5ebaad9ac0f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types-explicit-types.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types-explicit-types.expect.md @@ -65,20 +65,18 @@ function Component() { } const filtered = t2; let t3; - let t4; if ($[6] !== filtered) { - t4 = filtered.map(); + t3 = filtered.map(); $[6] = filtered; - $[7] = t4; + $[7] = t3; } else { - t4 = $[7]; + t3 = $[7]; } - t3 = t4; const map = t3; const index = filtered.findIndex(_temp3); - let t5; + let t4; if ($[8] !== index || $[9] !== map) { - t5 = ( + t4 = (
{map} {index} @@ -86,11 +84,11 @@ function Component() { ); $[8] = index; $[9] = map; - $[10] = t5; + $[10] = t4; } else { - t5 = $[10]; + t4 = $[10]; } - return t5; + return t4; } function _temp3(x) { return x === null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types.expect.md index eda7a0cbfebc5..8c68340b7f254 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-memoization-lack-of-phi-types.expect.md @@ -62,20 +62,18 @@ function Component() { } const filtered = t2; let t3; - let t4; if ($[6] !== filtered) { - t4 = filtered.map(); + t3 = filtered.map(); $[6] = filtered; - $[7] = t4; + $[7] = t3; } else { - t4 = $[7]; + t3 = $[7]; } - t3 = t4; const map = t3; const index = filtered.findIndex(_temp3); - let t5; + let t4; if ($[8] !== index || $[9] !== map) { - t5 = ( + t4 = (
{map} {index} @@ -83,11 +81,11 @@ function Component() { ); $[8] = index; $[9] = map; - $[10] = t5; + $[10] = t4; } else { - t5 = $[10]; + t4 = $[10]; } - return t5; + return t4; } function _temp3(x) { return x === null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-phi-after-dce-merge-scopes.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-phi-after-dce-merge-scopes.expect.md new file mode 100644 index 0000000000000..da7220c73c9b7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-phi-after-dce-merge-scopes.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +function Component() { + let v3, v4, acc; + v3 = false; + v4 = v3; + acc = v3; + if (acc) { + acc = true; + v3 = acc; + } + if (acc) { + v3 = v4; + } + v4 = v3; + return [acc, v3, v4]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function Component() { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = [false, false, false]; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) [false,false,false] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-phi-after-dce-merge-scopes.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-phi-after-dce-merge-scopes.js new file mode 100644 index 0000000000000..298ccbebaf27d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-missing-phi-after-dce-merge-scopes.js @@ -0,0 +1,20 @@ +function Component() { + let v3, v4, acc; + v3 = false; + v4 = v3; + acc = v3; + if (acc) { + acc = true; + v3 = acc; + } + if (acc) { + v3 = v4; + } + v4 = v3; + return [acc, v3, v4]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md index 506e4ca713301..d913c4f29b38e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-no-declarations-in-reactive-scope-with-early-return.expect.md @@ -39,55 +39,51 @@ function Component() { ```javascript import { c as _c } from "react/compiler-runtime"; // @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions function Component() { - const $ = _c(7); + const $ = _c(6); const items = useItems(); let t0; let t1; - let t2; if ($[0] !== items) { - t2 = Symbol.for("react.early_return_sentinel"); + t1 = Symbol.for("react.early_return_sentinel"); bb0: { - t0 = items.filter(_temp); - const filteredItems = t0; + const filteredItems = items.filter(_temp); if (filteredItems.length === 0) { - let t3; - if ($[4] === Symbol.for("react.memo_cache_sentinel")) { - t3 = ( + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 = (
); - $[4] = t3; + $[3] = t2; } else { - t3 = $[4]; + t2 = $[3]; } - t2 = t3; + t1 = t2; break bb0; } - t1 = filteredItems.map(_temp2); + t0 = filteredItems.map(_temp2); } $[0] = items; - $[1] = t1; - $[2] = t2; - $[3] = t0; + $[1] = t0; + $[2] = t1; } else { - t1 = $[1]; - t2 = $[2]; - t0 = $[3]; + t0 = $[1]; + t1 = $[2]; } - if (t2 !== Symbol.for("react.early_return_sentinel")) { - return t2; + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; } - let t3; - if ($[5] !== t1) { - t3 = <>{t1}; - $[5] = t1; - $[6] = t3; + let t2; + if ($[4] !== t0) { + t2 = <>{t0}; + $[4] = t0; + $[5] = t2; } else { - t3 = $[6]; + t2 = $[5]; } - return t3; + return t2; } function _temp2(t0) { const [item_0] = t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md new file mode 100644 index 0000000000000..6a9225eb772ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md @@ -0,0 +1,91 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {identity, mutate} from 'shared-runtime'; + +/** + * Fixed in the new inference model. + * + * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr + * with the mutation hoisted to a named variable instead of being directly + * inlined into the Object key. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * Forget: + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}] + */ +function Component(props) { + const key = {}; + const tmp = (mutate(key), key); + const context = { + // Here, `tmp` is frozen (as it's inferred to be a primitive/string) + [tmp]: identity([props.value]), + }; + mutate(key); + return [context, key]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], + sequentialRenders: [{value: 42}, {value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { identity, mutate } from "shared-runtime"; + +/** + * Fixed in the new inference model. + * + * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr + * with the mutation hoisted to a named variable instead of being directly + * inlined into the Object key. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * Forget: + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}] + */ +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.value) { + const key = {}; + const tmp = (mutate(key), key); + const context = { [tmp]: identity([props.value]) }; + + mutate(key); + t0 = [context, key]; + $[0] = props.value; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], + sequentialRenders: [{ value: 42 }, { value: 42 }], +}; + +``` + +### Eval output +(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] +[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js new file mode 100644 index 0000000000000..71abb3bc497fd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js @@ -0,0 +1,34 @@ +// @enableNewMutationAliasingModel +import {identity, mutate} from 'shared-runtime'; + +/** + * Fixed in the new inference model. + * + * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr + * with the mutation hoisted to a named variable instead of being directly + * inlined into the Object key. + * + * Found differences in evaluator results + * Non-forget (expected): + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * Forget: + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}] + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}] + */ +function Component(props) { + const key = {}; + const tmp = (mutate(key), key); + const context = { + // Here, `tmp` is frozen (as it's inferred to be a primitive/string) + [tmp]: identity([props.value]), + }; + mutate(key); + return [context, key]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], + sequentialRenders: [{value: 42}, {value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-reassign-props.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-reassign-props.expect.md new file mode 100644 index 0000000000000..19c85c943e711 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-reassign-props.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +import {Stringify, useIdentity} from 'shared-runtime'; + +function Component({other, ...props}, ref) { + [props, ref] = useIdentity([props, ref]); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 'hello', children:
Hello
}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, useIdentity } from "shared-runtime"; + +function Component(t0, ref) { + const $ = _c(7); + let props; + if ($[0] !== t0) { + let { other, ...t1 } = t0; + props = t1; + $[0] = t0; + $[1] = props; + } else { + props = $[1]; + } + let t1; + if ($[2] !== props || $[3] !== ref) { + t1 = [props, ref]; + $[2] = props; + $[3] = ref; + $[4] = t1; + } else { + t1 = $[4]; + } + [props, ref] = useIdentity(t1); + let t2; + if ($[5] !== props) { + t2 = ; + $[5] = props; + $[6] = t2; + } else { + t2 = $[6]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 0, b: "hello", children:
Hello
}], +}; + +``` + +### Eval output +(kind: ok)
{"props":{"a":0,"b":"hello","children":{"type":"div","key":null,"props":{"children":"Hello"},"_owner":"[[ cyclic ref *3 ]]","_store":{}}}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-reassign-props.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-reassign-props.js new file mode 100644 index 0000000000000..329000aedb084 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-reassign-props.js @@ -0,0 +1,11 @@ +import {Stringify, useIdentity} from 'shared-runtime'; + +function Component({other, ...props}, ref) { + [props, ref] = useIdentity([props, ref]); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 0, b: 'hello', children:
Hello
}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-renaming-conflicting-decls.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-renaming-conflicting-decls.expect.md index 61f633ccd8491..7db207a56226b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-renaming-conflicting-decls.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-renaming-conflicting-decls.expect.md @@ -45,87 +45,83 @@ import { Stringify, identity, makeArray, toJSON } from "shared-runtime"; import { useMemo } from "react"; function Component(props) { - const $ = _c(13); + const $ = _c(12); let t0; let t1; - let t2; if ($[0] !== props) { - t2 = Symbol.for("react.early_return_sentinel"); + t1 = Symbol.for("react.early_return_sentinel"); bb0: { - t0 = toJSON(props); - const propsString = t0; + const propsString = toJSON(props); if (propsString.length <= 2) { - t2 = null; + t1 = null; break bb0; } - t1 = identity(propsString); + t0 = identity(propsString); } $[0] = props; - $[1] = t1; - $[2] = t2; - $[3] = t0; + $[1] = t0; + $[2] = t1; } else { - t1 = $[1]; - t2 = $[2]; - t0 = $[3]; + t0 = $[1]; + t1 = $[2]; } - if (t2 !== Symbol.for("react.early_return_sentinel")) { - return t2; + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; } - let t3; - if ($[4] !== t1) { - t3 = { url: t1 }; - $[4] = t1; - $[5] = t3; + let t2; + if ($[3] !== t0) { + t2 = { url: t0 }; + $[3] = t0; + $[4] = t2; } else { - t3 = $[5]; + t2 = $[4]; } - const linkProps = t3; - let t4; - if ($[6] !== linkProps) { + const linkProps = t2; + let t3; + if ($[5] !== linkProps) { const x = {}; + let t4; let t5; let t6; let t7; let t8; - let t9; - if ($[8] === Symbol.for("react.memo_cache_sentinel")) { - t5 = [1]; - t6 = [2]; - t7 = [3]; - t8 = [4]; - t9 = [5]; + if ($[7] === Symbol.for("react.memo_cache_sentinel")) { + t4 = [1]; + t5 = [2]; + t6 = [3]; + t7 = [4]; + t8 = [5]; + $[7] = t4; $[8] = t5; $[9] = t6; $[10] = t7; $[11] = t8; - $[12] = t9; } else { + t4 = $[7]; t5 = $[8]; t6 = $[9]; t7 = $[10]; t8 = $[11]; - t9 = $[12]; } - t4 = ( + t3 = ( {makeArray(x, 2)} ); - $[6] = linkProps; - $[7] = t4; + $[5] = linkProps; + $[6] = t3; } else { - t4 = $[7]; + t3 = $[6]; } - return t4; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.expect.md new file mode 100644 index 0000000000000..434cbaa908d78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.expect.md @@ -0,0 +1,149 @@ + +## Input + +```javascript +// @enableNewMutationAliasingModel +import {ValidateMemoization} from 'shared-runtime'; + +const Codes = { + en: {name: 'English'}, + ja: {name: 'Japanese'}, + ko: {name: 'Korean'}, + zh: {name: 'Chinese'}, +}; + +function Component(a) { + let keys; + if (a) { + keys = Object.keys(Codes); + } else { + return null; + } + const options = keys.map(code => { + // In the old inference model, `keys` was assumed to be mutated bc + // this callback captures its input into its output, and the return + // is treated as a mutation since it's a function expression. The new + // model understands that `code` is captured but not mutated. + const country = Codes[code]; + return { + name: country.name, + code, + }; + }); + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: false}], + sequentialRenders: [ + {a: false}, + {a: true}, + {a: true}, + {a: false}, + {a: true}, + {a: false}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel +import { ValidateMemoization } from "shared-runtime"; + +const Codes = { + en: { name: "English" }, + ja: { name: "Japanese" }, + ko: { name: "Korean" }, + zh: { name: "Chinese" }, +}; + +function Component(a) { + const $ = _c(4); + let keys; + if (a) { + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = Object.keys(Codes); + $[0] = t0; + } else { + t0 = $[0]; + } + keys = t0; + } else { + return null; + } + let t0; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t0 = keys.map(_temp); + $[1] = t0; + } else { + t0 = $[1]; + } + const options = t0; + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ( + + ); + $[2] = t1; + } else { + t1 = $[2]; + } + let t2; + if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + t2 = ( + <> + {t1} + + + ); + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +function _temp(code) { + const country = Codes[code]; + return { name: country.name, code }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: false }], + sequentialRenders: [ + { a: false }, + { a: true }, + { a: true }, + { a: false }, + { a: true }, + { a: false }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"inputs":[],"output":["en","ja","ko","zh"]}
{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}
+
{"inputs":[],"output":["en","ja","ko","zh"]}
{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}
+
{"inputs":[],"output":["en","ja","ko","zh"]}
{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}
+
{"inputs":[],"output":["en","ja","ko","zh"]}
{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}
+
{"inputs":[],"output":["en","ja","ko","zh"]}
{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}
+
{"inputs":[],"output":["en","ja","ko","zh"]}
{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.js new file mode 100644 index 0000000000000..11aaeb9450804 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.js @@ -0,0 +1,52 @@ +// @enableNewMutationAliasingModel +import {ValidateMemoization} from 'shared-runtime'; + +const Codes = { + en: {name: 'English'}, + ja: {name: 'Japanese'}, + ko: {name: 'Korean'}, + zh: {name: 'Chinese'}, +}; + +function Component(a) { + let keys; + if (a) { + keys = Object.keys(Codes); + } else { + return null; + } + const options = keys.map(code => { + // In the old inference model, `keys` was assumed to be mutated bc + // this callback captures its input into its output, and the return + // is treated as a mutation since it's a function expression. The new + // model understands that `code` is captured but not mutated. + const country = Codes[code]; + return { + name: country.name, + code, + }; + }); + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: false}], + sequentialRenders: [ + {a: false}, + {a: true}, + {a: true}, + {a: false}, + {a: true}, + {a: false}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md deleted file mode 100644 index e771bf12bde5c..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md +++ /dev/null @@ -1,77 +0,0 @@ - -## Input - -```javascript -// @flow -/** - * This hook returns a function that when called with an input object, - * will return the result of mapping that input with the supplied map - * function. Results are cached, so if the same input is passed again, - * the same output object will be returned. - * - * Note that this technically violates the rules of React and is unsafe: - * hooks must return immutable objects and be pure, and a function which - * captures and mutates a value when called is inherently not pure. - * - * However, in this case it is technically safe _if_ the mapping function - * is pure *and* the resulting objects are never modified. This is because - * the function only caches: the result of `returnedFunction(someInput)` - * strictly depends on `returnedFunction` and `someInput`, and cannot - * otherwise change over time. - */ -hook useMemoMap( - map: TInput => TOutput -): TInput => TOutput { - return useMemo(() => { - // The original issue is that `cache` was not memoized together with the returned - // function. This was because neither appears to ever be mutated — the function - // is known to mutate `cache` but the function isn't called. - // - // The fix is to detect cases like this — functions that are mutable but not called - - // and ensure that their mutable captures are aliased together into the same scope. - const cache = new WeakMap(); - return input => { - let output = cache.get(input); - if (output == null) { - output = map(input); - cache.set(input, output); - } - return output; - }; - }, [map]); -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; - -function useMemoMap(map) { - const $ = _c(2); - let t0; - let t1; - if ($[0] !== map) { - const cache = new WeakMap(); - t1 = (input) => { - let output = cache.get(input); - if (output == null) { - output = map(input); - cache.set(input, output); - } - return output; - }; - $[0] = map; - $[1] = t1; - } else { - t1 = $[1]; - } - t0 = t1; - return t0; -} - -``` - -### Eval output -(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md index 1b49552bea135..d3fe3045c020c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro.expect.md @@ -24,10 +24,9 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(7); + const $ = _c(6); const item = props.item; let baseVideos; - let t0; let thumbnails; if ($[0] !== item) { thumbnails = []; @@ -41,24 +40,21 @@ function Component(props) { }); $[0] = item; $[1] = baseVideos; - $[2] = t0; - $[3] = thumbnails; + $[2] = thumbnails; } else { baseVideos = $[1]; - t0 = $[2]; - thumbnails = $[3]; + thumbnails = $[2]; } - t0 = undefined; - let t1; - if ($[4] !== baseVideos || $[5] !== thumbnails) { - t1 = ; - $[4] = baseVideos; - $[5] = thumbnails; - $[6] = t1; + let t0; + if ($[3] !== baseVideos || $[4] !== thumbnails) { + t0 = ; + $[3] = baseVideos; + $[4] = thumbnails; + $[5] = t0; } else { - t1 = $[6]; + t0 = $[5]; } - return t1; + return t0; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reverse-postorder.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reverse-postorder.expect.md index c0ff0f7e7db78..98f2cd2190c20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reverse-postorder.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reverse-postorder.expect.md @@ -50,10 +50,8 @@ function Component(props) { case 1: { break bb0; } - case 2: { - } - default: { - } + case 2: + default: } } else { if (props.cond2) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md index 4796fbdcc2bcf..48a765d3f3631 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-switch.expect.md @@ -41,8 +41,7 @@ function foo() { case 2: { break bb0; } - default: { - } + default: } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md index c54631092c0ac..6fd911c432716 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-with-fallthrough.expect.md @@ -43,22 +43,17 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function foo(x) { bb0: switch (x) { - case 0: { - } - case 1: { - } + case 0: + case 1: case 2: { break bb0; } case 3: { break bb0; } - case 4: { - } - case 5: { - } - default: { - } + case 4: + case 5: + default: } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md index c3c45beb86544..dd2eebb606cda 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log-default-import.expect.md @@ -47,77 +47,73 @@ export function Component(t0) { const $ = _c(17); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; + const item2 = t2; typedLog(item1, item2); - let t5; + let t3; if ($[4] !== a) { - t5 = [a]; + t3 = [a]; $[4] = a; - $[5] = t5; + $[5] = t3; } else { - t5 = $[5]; + t3 = $[5]; } - let t6; - if ($[6] !== item1 || $[7] !== t5) { - t6 = ; + let t4; + if ($[6] !== item1 || $[7] !== t3) { + t4 = ; $[6] = item1; - $[7] = t5; - $[8] = t6; + $[7] = t3; + $[8] = t4; } else { - t6 = $[8]; + t4 = $[8]; } - let t7; + let t5; if ($[9] !== b) { - t7 = [b]; + t5 = [b]; $[9] = b; - $[10] = t7; + $[10] = t5; } else { - t7 = $[10]; + t5 = $[10]; } - let t8; - if ($[11] !== item2 || $[12] !== t7) { - t8 = ; + let t6; + if ($[11] !== item2 || $[12] !== t5) { + t6 = ; $[11] = item2; - $[12] = t7; - $[13] = t8; + $[12] = t5; + $[13] = t6; } else { - t8 = $[13]; + t6 = $[13]; } - let t9; - if ($[14] !== t6 || $[15] !== t8) { - t9 = ( + let t7; + if ($[14] !== t4 || $[15] !== t6) { + t7 = ( <> + {t4} {t6} - {t8} ); - $[14] = t6; - $[15] = t8; - $[16] = t9; + $[14] = t4; + $[15] = t6; + $[16] = t7; } else { - t9 = $[16]; + t7 = $[16]; } - return t9; + return t7; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md index 4acbd2dfdb3be..ba4c4fe224901 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-log.expect.md @@ -45,77 +45,73 @@ export function Component(t0) { const $ = _c(17); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; + const item2 = t2; typedLog(item1, item2); - let t5; + let t3; if ($[4] !== a) { - t5 = [a]; + t3 = [a]; $[4] = a; - $[5] = t5; + $[5] = t3; } else { - t5 = $[5]; + t3 = $[5]; } - let t6; - if ($[6] !== item1 || $[7] !== t5) { - t6 = ; + let t4; + if ($[6] !== item1 || $[7] !== t3) { + t4 = ; $[6] = item1; - $[7] = t5; - $[8] = t6; + $[7] = t3; + $[8] = t4; } else { - t6 = $[8]; + t4 = $[8]; } - let t7; + let t5; if ($[9] !== b) { - t7 = [b]; + t5 = [b]; $[9] = b; - $[10] = t7; + $[10] = t5; } else { - t7 = $[10]; + t5 = $[10]; } - let t8; - if ($[11] !== item2 || $[12] !== t7) { - t8 = ; + let t6; + if ($[11] !== item2 || $[12] !== t5) { + t6 = ; $[11] = item2; - $[12] = t7; - $[13] = t8; + $[12] = t5; + $[13] = t6; } else { - t8 = $[13]; + t6 = $[13]; } - let t9; - if ($[14] !== t6 || $[15] !== t8) { - t9 = ( + let t7; + if ($[14] !== t4 || $[15] !== t6) { + t7 = ( <> + {t4} {t6} - {t8} ); - $[14] = t6; - $[15] = t8; - $[16] = t9; + $[14] = t4; + $[15] = t6; + $[16] = t7; } else { - t9 = $[16]; + t7 = $[16]; } - return t9; + return t7; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md index 87b5d7a09e28d..1de8d9a170d20 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture-namespace-import.expect.md @@ -51,28 +51,23 @@ export function Component(t0) { const $ = _c(27); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; - let t5; + const item2 = t2; let items; if ($[4] !== item1 || $[5] !== item2) { items = []; @@ -84,77 +79,76 @@ export function Component(t0) { } else { items = $[6]; } - t5 = items; - const items_0 = t5; - let t6; + const items_0 = items; + let t3; if ($[7] !== a) { - t6 = [a]; + t3 = [a]; $[7] = a; - $[8] = t6; + $[8] = t3; } else { - t6 = $[8]; + t3 = $[8]; } - let t7; - if ($[9] !== items_0[0] || $[10] !== t6) { - t7 = ; + let t4; + if ($[9] !== items_0[0] || $[10] !== t3) { + t4 = ; $[9] = items_0[0]; - $[10] = t6; - $[11] = t7; + $[10] = t3; + $[11] = t4; } else { - t7 = $[11]; + t4 = $[11]; } - let t8; + let t5; if ($[12] !== b) { - t8 = [b]; + t5 = [b]; $[12] = b; - $[13] = t8; + $[13] = t5; } else { - t8 = $[13]; + t5 = $[13]; } - let t9; - if ($[14] !== items_0[1] || $[15] !== t8) { - t9 = ; + let t6; + if ($[14] !== items_0[1] || $[15] !== t5) { + t6 = ; $[14] = items_0[1]; - $[15] = t8; - $[16] = t9; + $[15] = t5; + $[16] = t6; } else { - t9 = $[16]; + t6 = $[16]; } - let t10; + let t7; if ($[17] !== a || $[18] !== b) { - t10 = [a, b]; + t7 = [a, b]; $[17] = a; $[18] = b; - $[19] = t10; + $[19] = t7; } else { - t10 = $[19]; + t7 = $[19]; } - let t11; - if ($[20] !== items_0 || $[21] !== t10) { - t11 = ; + let t8; + if ($[20] !== items_0 || $[21] !== t7) { + t8 = ; $[20] = items_0; - $[21] = t10; - $[22] = t11; + $[21] = t7; + $[22] = t8; } else { - t11 = $[22]; + t8 = $[22]; } - let t12; - if ($[23] !== t11 || $[24] !== t7 || $[25] !== t9) { - t12 = ( + let t9; + if ($[23] !== t4 || $[24] !== t6 || $[25] !== t8) { + t9 = ( <> - {t7} - {t9} - {t11} + {t4} + {t6} + {t8} ); - $[23] = t11; - $[24] = t7; - $[25] = t9; - $[26] = t12; + $[23] = t4; + $[24] = t6; + $[25] = t8; + $[26] = t9; } else { - t12 = $[26]; + t9 = $[26]; } - return t12; + return t9; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md index c71e4c2530fce..a3b34b0274a70 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/type-provider-store-capture.expect.md @@ -51,28 +51,23 @@ export function Component(t0) { const $ = _c(27); const { a, b } = t0; let t1; - let t2; if ($[0] !== a) { - t2 = { a }; + t1 = { a }; $[0] = a; - $[1] = t2; + $[1] = t1; } else { - t2 = $[1]; + t1 = $[1]; } - t1 = t2; const item1 = t1; - let t3; - let t4; + let t2; if ($[2] !== b) { - t4 = { b }; + t2 = { b }; $[2] = b; - $[3] = t4; + $[3] = t2; } else { - t4 = $[3]; + t2 = $[3]; } - t3 = t4; - const item2 = t3; - let t5; + const item2 = t2; let items; if ($[4] !== item1 || $[5] !== item2) { items = []; @@ -84,77 +79,76 @@ export function Component(t0) { } else { items = $[6]; } - t5 = items; - const items_0 = t5; - let t6; + const items_0 = items; + let t3; if ($[7] !== a) { - t6 = [a]; + t3 = [a]; $[7] = a; - $[8] = t6; + $[8] = t3; } else { - t6 = $[8]; + t3 = $[8]; } - let t7; - if ($[9] !== items_0[0] || $[10] !== t6) { - t7 = ; + let t4; + if ($[9] !== items_0[0] || $[10] !== t3) { + t4 = ; $[9] = items_0[0]; - $[10] = t6; - $[11] = t7; + $[10] = t3; + $[11] = t4; } else { - t7 = $[11]; + t4 = $[11]; } - let t8; + let t5; if ($[12] !== b) { - t8 = [b]; + t5 = [b]; $[12] = b; - $[13] = t8; + $[13] = t5; } else { - t8 = $[13]; + t5 = $[13]; } - let t9; - if ($[14] !== items_0[1] || $[15] !== t8) { - t9 = ; + let t6; + if ($[14] !== items_0[1] || $[15] !== t5) { + t6 = ; $[14] = items_0[1]; - $[15] = t8; - $[16] = t9; + $[15] = t5; + $[16] = t6; } else { - t9 = $[16]; + t6 = $[16]; } - let t10; + let t7; if ($[17] !== a || $[18] !== b) { - t10 = [a, b]; + t7 = [a, b]; $[17] = a; $[18] = b; - $[19] = t10; + $[19] = t7; } else { - t10 = $[19]; + t7 = $[19]; } - let t11; - if ($[20] !== items_0 || $[21] !== t10) { - t11 = ; + let t8; + if ($[20] !== items_0 || $[21] !== t7) { + t8 = ; $[20] = items_0; - $[21] = t10; - $[22] = t11; + $[21] = t7; + $[22] = t8; } else { - t11 = $[22]; + t8 = $[22]; } - let t12; - if ($[23] !== t11 || $[24] !== t7 || $[25] !== t9) { - t12 = ( + let t9; + if ($[23] !== t4 || $[24] !== t6 || $[25] !== t8) { + t9 = ( <> - {t7} - {t9} - {t11} + {t4} + {t6} + {t8} ); - $[23] = t11; - $[24] = t7; - $[25] = t9; - $[26] = t12; + $[23] = t4; + $[24] = t6; + $[25] = t8; + $[26] = t9; } else { - t12 = $[26]; + t9 = $[26]; } - return t12; + return t9; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md index 7f79cae4a0361..dad37e7dfd94d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-call-expression.expect.md @@ -70,34 +70,32 @@ function Inner(props) { const $ = _c(7); const input = use(FooContext); let t0; - let t1; if ($[0] !== input) { - t1 = [input]; + t0 = [input]; $[0] = input; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const output = t0; - let t2; + let t1; if ($[2] !== input) { - t2 = [input]; + t1 = [input]; $[2] = input; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== output || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== output || $[5] !== t1) { + t2 = ; $[4] = output; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { 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 e00a5648164a4..ab645e81e336b 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 @@ -86,34 +86,32 @@ function Inner(props) { input; let t0; - let t1; if ($[0] !== input) { - t1 = [input]; + t0 = [input]; $[0] = input; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const output = t0; - let t2; + let t1; if ($[2] !== input) { - t2 = [input]; + t1 = [input]; $[2] = input; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== output || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== output || $[5] !== t1) { + t2 = ; $[4] = output; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md index cf0093ea941bf..5eea8e6e19522 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-method-call.expect.md @@ -72,34 +72,32 @@ function Inner(props) { const $ = _c(7); const input = React.use(FooContext); let t0; - let t1; if ($[0] !== input) { - t1 = [input]; + t0 = [input]; $[0] = input; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const output = t0; - let t2; + let t1; if ($[2] !== input) { - t2 = [input]; + t1 = [input]; $[2] = input; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - let t3; - if ($[4] !== output || $[5] !== t2) { - t3 = ; + let t2; + if ($[4] !== output || $[5] !== t1) { + t2 = ; $[4] = output; - $[5] = t2; - $[6] = t3; + $[5] = t1; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md index aefbaecedb0d7..0a78a544b8ba8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-global-pruned.expect.md @@ -37,23 +37,21 @@ import { useEffect } from "react"; function someGlobal() {} function useFoo() { const $ = _c(2); + const fn = _temp; let t0; - t0 = _temp; - const fn = t0; let t1; - let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { + t0 = () => { fn(); }; - t2 = [fn]; - $[0] = t1; - $[1] = t2; + t1 = [fn]; + $[0] = t0; + $[1] = t1; } else { - t1 = $[0]; - t2 = $[1]; + t0 = $[0]; + t1 = $[1]; } - useEffect(t1, t2); + useEffect(t0, t1); return null; } function _temp() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md index 2646478d39427..f3fb51cc58bb3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useEffect-namespace-pruned.expect.md @@ -37,23 +37,21 @@ import * as React from "react"; function someGlobal() {} function useFoo() { const $ = _c(2); + const fn = _temp; let t0; - t0 = _temp; - const fn = t0; let t1; - let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => { + t0 = () => { fn(); }; - t2 = [fn]; - $[0] = t1; - $[1] = t2; + t1 = [fn]; + $[0] = t0; + $[1] = t1; } else { - t1 = $[0]; - t2 = $[1]; + t0 = $[0]; + t1 = $[1]; } - React.useEffect(t1, t2); + React.useEffect(t0, t1); return null; } function _temp() { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md index 8b3f03137ee43..479b1b2c8230a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-independently-memoizeable.expect.md @@ -21,45 +21,43 @@ import { c as _c } from "react/compiler-runtime"; function Component(props) { const $ = _c(10); let t0; - let t1; if ($[0] !== props.a) { - t1 = makeObject(props.a); + t0 = makeObject(props.a); $[0] = props.a; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - const a = t1; - let t2; + const a = t0; + let t1; if ($[2] !== props.b) { - t2 = makeObject(props.b); + t1 = makeObject(props.b); $[2] = props.b; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - const b = t2; - let t3; + const b = t1; + let t2; if ($[4] !== a || $[5] !== b) { - t3 = [a, b]; + t2 = [a, b]; $[4] = a; $[5] = b; - $[6] = t3; + $[6] = t2; } else { - t3 = $[6]; + t2 = $[6]; } - t0 = t3; - const [a_0, b_0] = t0; - let t4; + const [a_0, b_0] = t2; + let t3; if ($[7] !== a_0 || $[8] !== b_0) { - t4 = [a_0, b_0]; + t3 = [a_0, b_0]; $[7] = a_0; $[8] = b_0; - $[9] = t4; + $[9] = t3; } else { - t4 = $[9]; + t3 = $[9]; } - return t4; + return t3; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md index 9e00c1ddb9fd8..dda4a25e9f0e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-labeled-statement-unconditional-return.expect.md @@ -23,10 +23,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { - let t0; - - t0 = props.value; - const x = t0; + const x = props.value; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md index 672e8e45bc354..f944aa454d88d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-logical.expect.md @@ -19,9 +19,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { - let t0; - t0 = props.a && props.b; - const x = t0; + const x = props.a && props.b; return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md index 666fa49376b8f..8b4de101cacc3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-dont-preserve-memoization-guarantees.expect.md @@ -51,13 +51,11 @@ function Component(props) { const part = free2.part; useHook(); - let t0; const x = makeObject_Primitives(); x.value = props.value; mutate(x, free, part); - t0 = x; - const object = t0; + const object = x; identity(free); identity(part); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md index 05f33ccd3801b..a9207d39203dc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-mabye-modified-free-variable-preserve-memoization-guarantees.expect.md @@ -71,7 +71,6 @@ function Component(props) { const part = free2.part; useHook(); - let t2; let x; if ($[2] !== props.value) { x = makeObject_Primitives(); @@ -82,8 +81,7 @@ function Component(props) { } else { x = $[3]; } - t2 = x; - const object = t2; + const object = x; identity(free); identity(part); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md index 7a3834fc6f68a..ac8c52187ed06 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-dont-preserve-memoization-guarantees.expect.md @@ -27,18 +27,14 @@ import { useMemo } from "react"; import { identity, makeObject_Primitives, mutate } from "shared-runtime"; function Component(props) { - const $ = _c(2); - let t0; + const $ = _c(1); let object; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = makeObject_Primitives(); - object = t0; + object = makeObject_Primitives(); identity(object); $[0] = object; - $[1] = t0; } else { object = $[0]; - t0 = $[1]; } return object; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md index 7d8d77527e97c..7eddc14c7967b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-maybe-modified-later-preserve-memoization-guarantees.expect.md @@ -29,14 +29,12 @@ import { identity, makeObject_Primitives, mutate } from "shared-runtime"; function Component(props) { const $ = _c(1); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t1 = makeObject_Primitives(); - $[0] = t1; + t0 = makeObject_Primitives(); + $[0] = t0; } else { - t1 = $[0]; + t0 = $[0]; } - t0 = t1; const object = t0; identity(object); return object; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md index 12f51643dd4d4..f7b3605f4d5a0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-nested-ifs.expect.md @@ -24,12 +24,10 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function Component(props) { - let t0; if (props.cond) { if (props.cond) { } } - t0 = undefined; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md index b348ae34b6f56..be20ee39bd4ec 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-return-empty.expect.md @@ -15,10 +15,7 @@ function component(a) { ```javascript function component(a) { - let t0; - mutate(a); - t0 = undefined; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md index 712cb156d8dbb..301d9860f33ba 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-simple.expect.md @@ -16,25 +16,23 @@ import { c as _c } from "react/compiler-runtime"; function component(a) { const $ = _c(4); let t0; - let t1; if ($[0] !== a) { - t1 = [a]; + t0 = [a]; $[0] = a; - $[1] = t1; + $[1] = t0; } else { - t1 = $[1]; + t0 = $[1]; } - t0 = t1; const x = t0; - let t2; + let t1; if ($[2] !== x) { - t2 = ; + t1 = ; $[2] = x; - $[3] = t2; + $[3] = t1; } else { - t2 = $[3]; + t1 = $[3]; } - return t2; + return t1; } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md new file mode 100644 index 0000000000000..260d695e09d8a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.expect.md @@ -0,0 +1,47 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +function Component(props) { + return ( + useMemo(() => { + return [props.value]; + }) || [] + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.value) { + t0 = (() => [props.value])() || []; + $[0] = props.value; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 1 }], +}; + +``` + +### Eval output +(kind: ok) [1] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js new file mode 100644 index 0000000000000..a96c044a3b86b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-with-optional.js @@ -0,0 +1,13 @@ +import {useMemo} from 'react'; +function Component(props) { + return ( + useMemo(() => { + return [props.value]; + }) || [] + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md index aebaedf6a8754..a6def457a4194 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -13,9 +14,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } @@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; import { ValidateMemoization } from "shared-runtime"; function Component(t0) { @@ -76,7 +90,9 @@ function Component(t0) { } let t2; if ($[7] !== map || $[8] !== t1) { - t2 = ; + t2 = ( + + ); $[7] = map; $[8] = t1; $[9] = t2; @@ -94,7 +110,13 @@ function Component(t0) { } let t4; if ($[13] !== mapAlias || $[14] !== t3) { - t4 = ; + t4 = ( + + ); $[13] = mapAlias; $[14] = t3; $[15] = t4; @@ -119,7 +141,9 @@ function Component(t0) { } let t7; if ($[20] !== t5 || $[21] !== t6) { - t7 = ; + t7 = ( + + ); $[20] = t5; $[21] = t6; $[22] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js index ecdec6c9e9621..d005c9f271d17 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakmap-constructor.js @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -9,9 +10,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md index 5ebf32d533c91..94e0c7f05547b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.expect.md @@ -2,6 +2,7 @@ ## Input ```javascript +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -13,9 +14,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } @@ -44,6 +57,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { useMemo } from "react"; import { ValidateMemoization } from "shared-runtime"; function Component(t0) { @@ -76,7 +90,9 @@ function Component(t0) { } let t2; if ($[7] !== set || $[8] !== t1) { - t2 = ; + t2 = ( + + ); $[7] = set; $[8] = t1; $[9] = t2; @@ -94,7 +110,13 @@ function Component(t0) { } let t4; if ($[13] !== setAlias || $[14] !== t3) { - t4 = ; + t4 = ( + + ); $[13] = setAlias; $[14] = t3; $[15] = t4; @@ -119,7 +141,9 @@ function Component(t0) { } let t7; if ($[20] !== t5 || $[21] !== t6) { - t7 = ; + t7 = ( + + ); $[20] = t5; $[21] = t6; $[22] = t7; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js index 8c0a7deb186f6..9114233812e55 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/weakset-constructor.js @@ -1,3 +1,4 @@ +import {useMemo} from 'react'; import {ValidateMemoization} from 'shared-runtime'; function Component({a, b, c}) { @@ -9,9 +10,21 @@ function Component({a, b, c}) { return ( <> - - - + + + ); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts index cbae672e50674..bbd814b2b60e3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts @@ -30,6 +30,7 @@ export { export { Effect, ValueKind, + ValueReason, printHIR, printFunctionWithOutlined, validateEnvironmentConfig, diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index 62b8a7703fddb..02cb3775cb549 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -460,6 +460,7 @@ const skipFilter = new Set([ 'fbt/bug-fbt-plural-multiple-function-calls', 'fbt/bug-fbt-plural-multiple-mixed-call-tag', 'bug-invalid-phi-as-dependency', + 'bug-ref-prefix-postfix-operator', // 'react-compiler-runtime' not yet supported 'flag-enable-emit-hook-guards', @@ -485,6 +486,7 @@ const skipFilter = new Set([ 'todo.lower-context-access-array-destructuring', 'lower-context-selector-simple', 'lower-context-acess-multiple', + 'bug-separate-memoization-due-to-callback-capturing', ]); export default skipFilter; diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index 7241ed51492bc..a159359773f31 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -18,7 +18,11 @@ import type { CompilerReactTarget, CompilerPipelineValue, } from 'babel-plugin-react-compiler/src/Entrypoint'; -import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src/HIR'; +import type { + Effect, + ValueKind, + ValueReason, +} from 'babel-plugin-react-compiler/src/HIR'; import type {parseConfigPragmaForTests as ParseConfigPragma} from 'babel-plugin-react-compiler/src/Utils/TestUtils'; import * as HermesParser from 'hermes-parser'; import invariant from 'invariant'; @@ -42,6 +46,7 @@ function makePluginOptions( debugIRLogger: (value: CompilerPipelineValue) => void, EffectEnum: typeof Effect, ValueKindEnum: typeof ValueKind, + ValueReasonEnum: typeof ValueReason, ): [PluginOptions, Array<{filename: string | null; event: LoggerEvent}>] { // TODO(@mofeiZ) rewrite snap fixtures to @validatePreserveExistingMemo:false let validatePreserveExistingMemoizationGuarantees = false; @@ -77,6 +82,7 @@ function makePluginOptions( moduleTypeProvider: makeSharedRuntimeTypeProvider({ EffectEnum, ValueKindEnum, + ValueReasonEnum, }), assertValidMutableRanges: true, validatePreserveExistingMemoizationGuarantees, @@ -209,6 +215,7 @@ export async function transformFixtureInput( debugIRLogger: (value: CompilerPipelineValue) => void, EffectEnum: typeof Effect, ValueKindEnum: typeof ValueKind, + ValueReasonEnum: typeof ValueReason, ): Promise<{kind: 'ok'; value: TransformResult} | {kind: 'err'; msg: string}> { // Extract the first line to quickly check for custom test directives const firstLine = input.substring(0, input.indexOf('\n')); @@ -237,6 +244,7 @@ export async function transformFixtureInput( debugIRLogger, EffectEnum, ValueKindEnum, + ValueReasonEnum, ); const forgetResult = transformFromAstSync(inputAst, input, { filename: virtualFilepath, diff --git a/compiler/packages/snap/src/runner-worker.ts b/compiler/packages/snap/src/runner-worker.ts index 2478e6a545b72..fd4763b20322e 100644 --- a/compiler/packages/snap/src/runner-worker.ts +++ b/compiler/packages/snap/src/runner-worker.ts @@ -24,6 +24,7 @@ import type { CompilerPipelineValue, Effect, ValueKind, + ValueReason, } from 'babel-plugin-react-compiler/src'; import chalk from 'chalk'; @@ -78,6 +79,9 @@ async function compile( const ValueKindEnum = importedCompilerPlugin[ 'ValueKind' ] as typeof ValueKind; + const ValueReasonEnum = importedCompilerPlugin[ + 'ValueReason' + ] as typeof ValueReason; const printFunctionWithOutlined = importedCompilerPlugin[ PRINT_HIR_IMPORT ] as typeof PrintFunctionWithOutlined; @@ -128,6 +132,7 @@ async function compile( debugIRLogger, EffectEnum, ValueKindEnum, + ValueReasonEnum, ); if (result.kind === 'err') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts index 4c1d77f2f8986..58b007c1c7355 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime-type-provider.ts @@ -5,15 +5,21 @@ * LICENSE file in the root directory of this source tree. */ -import type {Effect, ValueKind} from 'babel-plugin-react-compiler/src'; +import type { + Effect, + ValueKind, + ValueReason, +} from 'babel-plugin-react-compiler/src'; import type {TypeConfig} from 'babel-plugin-react-compiler/src/HIR/TypeSchema'; export function makeSharedRuntimeTypeProvider({ EffectEnum, ValueKindEnum, + ValueReasonEnum, }: { EffectEnum: typeof Effect; ValueKindEnum: typeof ValueKind; + ValueReasonEnum: typeof ValueReason; }) { return function sharedRuntimeTypeProvider( moduleName: string, @@ -69,6 +75,127 @@ export function makeSharedRuntimeTypeProvider({ returnValueKind: ValueKindEnum.Mutable, noAlias: true, }, + typedIdentity: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'Assign', from: '@value', into: '@return'}], + }, + }, + typedAssign: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'Assign', from: '@value', into: '@return'}], + }, + }, + typedAlias: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Mutable, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Alias', from: '@value', into: '@return'}, + ], + }, + }, + typedCapture: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Array'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Mutable, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Capture', from: '@value', into: '@return'}, + ], + }, + }, + typedCreateFrom: { + kind: 'function', + positionalParams: [EffectEnum.Read], + restParam: null, + calleeEffect: EffectEnum.Read, + returnType: {kind: 'type', name: 'Any'}, + returnValueKind: ValueKindEnum.Mutable, + aliasing: { + receiver: '@receiver', + params: ['@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [{kind: 'CreateFrom', from: '@value', into: '@return'}], + }, + }, + typedMutate: { + kind: 'function', + positionalParams: [EffectEnum.Read, EffectEnum.Capture], + restParam: null, + calleeEffect: EffectEnum.Store, + returnType: {kind: 'type', name: 'Primitive'}, + returnValueKind: ValueKindEnum.Primitive, + aliasing: { + receiver: '@receiver', + params: ['@object', '@value'], + rest: null, + returns: '@return', + temporaries: [], + effects: [ + { + kind: 'Create', + into: '@return', + value: ValueKindEnum.Primitive, + reason: ValueReasonEnum.KnownReturnSignature, + }, + {kind: 'Mutate', value: '@object'}, + {kind: 'Capture', from: '@value', into: '@object'}, + ], + }, + }, }, }; } else if (moduleName === 'ReactCompilerTest') { diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 569d31cbd4da1..f37ca82709022 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -272,7 +272,7 @@ export function ValidateMemoization({ }: { inputs: Array; output: any; - onlyCheckCompiled: boolean; + onlyCheckCompiled?: boolean; }): React.ReactElement { 'use no forget'; // Wrap rawOutput as it might be a function, which useState would invoke. @@ -280,8 +280,9 @@ export function ValidateMemoization({ const [previousInputs, setPreviousInputs] = React.useState(inputs); const [previousOutput, setPreviousOutput] = React.useState(output); if ( - onlyCheckCompiled && - (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget' + !onlyCheckCompiled || + (onlyCheckCompiled && + (globalThis as any).__SNAP_EVALUATOR_MODE === 'forget') ) { if ( inputs.length !== previousInputs.length || @@ -396,4 +397,28 @@ export function typedLog(...values: Array): void { console.log(...values); } +export function typedIdentity(value: T): T { + return value; +} + +export function typedAssign(x: T): T { + return x; +} + +export function typedAlias(x: T): T { + return x; +} + +export function typedCapture(x: T): Array { + return [x]; +} + +export function typedCreateFrom(array: Array): T { + return array[0]; +} + +export function typedMutate(x: any, v: any = null): void { + x.property = v; +} + export default typedLog; diff --git a/fixtures/flight/server/global.js b/fixtures/flight/server/global.js index a2fa737ae0f4d..f097378056a46 100644 --- a/fixtures/flight/server/global.js +++ b/fixtures/flight/server/global.js @@ -101,6 +101,12 @@ async function renderApp(req, res, next) { } else if (req.get('Content-type')) { proxiedHeaders['Content-type'] = req.get('Content-type'); } + if (req.headers['cache-control']) { + proxiedHeaders['Cache-Control'] = req.get('cache-control'); + } + if (req.get('rsc-request-id')) { + proxiedHeaders['rsc-request-id'] = req.get('rsc-request-id'); + } const requestsPrerender = req.path === '/prerender'; diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index 6896713e41cbf..564c7b78dd9d0 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -50,7 +50,36 @@ const {readFile} = require('fs').promises; const React = require('react'); -async function renderApp(res, returnValue, formState) { +const activeDebugChannels = + process.env.NODE_ENV === 'development' ? new Map() : null; + +function filterStackFrame(sourceURL, functionName) { + return ( + sourceURL !== '' && + !sourceURL.startsWith('node:') && + !sourceURL.includes('node_modules') && + !sourceURL.endsWith('library.js') + ); +} + +function getDebugChannel(req) { + if (process.env.NODE_ENV !== 'development') { + return undefined; + } + const requestId = req.get('rsc-request-id'); + if (!requestId) { + return undefined; + } + return activeDebugChannels.get(requestId); +} + +async function renderApp( + res, + returnValue, + formState, + noCache, + promiseForDebugChannel +) { const {renderToPipeableStream} = await import( 'react-server-dom-webpack/server' ); @@ -97,15 +126,18 @@ async function renderApp(res, returnValue, formState) { key: filename, }) ), - React.createElement(App) + React.createElement(App, {noCache}) ); // For client-invoked server actions we refresh the tree and return a return value. const payload = {root, returnValue, formState}; - const {pipe} = renderToPipeableStream(payload, moduleMap); + const {pipe} = renderToPipeableStream(payload, moduleMap, { + debugChannel: await promiseForDebugChannel, + filterStackFrame, + }); pipe(res); } -async function prerenderApp(res, returnValue, formState) { +async function prerenderApp(res, returnValue, formState, noCache) { const {unstable_prerenderToNodeStream: prerenderToNodeStream} = await import( 'react-server-dom-webpack/static' ); @@ -152,23 +184,28 @@ async function prerenderApp(res, returnValue, formState) { key: filename, }) ), - React.createElement(App, {prerender: true}) + React.createElement(App, {prerender: true, noCache}) ); // For client-invoked server actions we refresh the tree and return a return value. const payload = {root, returnValue, formState}; - const {prelude} = await prerenderToNodeStream(payload, moduleMap); + const {prelude} = await prerenderToNodeStream(payload, moduleMap, { + filterStackFrame, + }); prelude.pipe(res); } app.get('/', async function (req, res) { + const noCache = req.get('cache-control') === 'no-cache'; + if ('prerender' in req.query) { - await prerenderApp(res, null, null); + await prerenderApp(res, null, null, noCache); } else { - await renderApp(res, null, null); + await renderApp(res, null, null, noCache, getDebugChannel(req)); } }); app.post('/', bodyParser.text(), async function (req, res) { + const noCache = req.headers['cache-control'] === 'no-cache'; const {decodeReply, decodeReplyFromBusboy, decodeAction, decodeFormState} = await import('react-server-dom-webpack/server'); const serverReference = req.get('rsc-action'); @@ -201,7 +238,7 @@ app.post('/', bodyParser.text(), async function (req, res) { // We handle the error on the client } // Refresh the client and return the value - renderApp(res, result, null); + renderApp(res, result, null, noCache, getDebugChannel(req)); } else { // This is the progressive enhancement case const UndiciRequest = require('undici').Request; @@ -217,11 +254,11 @@ app.post('/', bodyParser.text(), async function (req, res) { // Wait for any mutations const result = await action(); const formState = decodeFormState(result, formData); - renderApp(res, null, formState); + renderApp(res, null, formState, noCache, undefined); } catch (x) { const {setServerState} = await import('../src/ServerState.js'); setServerState('Error: ' + x.message); - renderApp(res, null, null); + renderApp(res, null, null, noCache, undefined); } } }); @@ -321,7 +358,7 @@ if (process.env.NODE_ENV === 'development') { }); } -app.listen(3001, () => { +const httpServer = app.listen(3001, () => { console.log('Regional Flight Server listening on port 3001...'); }); @@ -343,3 +380,27 @@ app.on('error', function (error) { throw error; } }); + +if (process.env.NODE_ENV === 'development') { + // Open a websocket server for Debug information + const WebSocket = require('ws'); + const webSocketServer = new WebSocket.Server({noServer: true}); + + httpServer.on('upgrade', (request, socket, head) => { + const DEBUG_CHANNEL_PATH = '/debug-channel?'; + if (request.url.startsWith(DEBUG_CHANNEL_PATH)) { + const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length); + const promiseForWs = new Promise(resolve => { + webSocketServer.handleUpgrade(request, socket, head, ws => { + ws.on('close', () => { + activeDebugChannels.delete(requestId); + }); + resolve(ws); + }); + }); + activeDebugChannels.set(requestId, promiseForWs); + } else { + socket.destroy(); + } + }); +} diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js index 08eaefc90f887..e6366f4bd0ea4 100644 --- a/fixtures/flight/src/App.js +++ b/fixtures/flight/src/App.js @@ -1,6 +1,6 @@ import * as React from 'react'; -import {renderToPipeableStream} from 'react-server-dom-webpack/server'; -import {createFromNodeStream} from 'react-server-dom-webpack/client'; +import {renderToReadableStream} from 'react-server-dom-webpack/server'; +import {createFromReadableStream} from 'react-server-dom-webpack/client'; import {PassThrough, Readable} from 'stream'; import Container from './Container.js'; @@ -24,6 +24,7 @@ import {GenerateImage} from './GenerateImage.js'; import {like, greet, increment} from './actions.js'; import {getServerState} from './ServerState.js'; +import {sdkMethod} from './library.js'; const promisedText = new Promise(resolve => setTimeout(() => resolve('deferred text'), 50) @@ -33,62 +34,158 @@ function Foo({children}) { return
{children}
; } +async function delayedError(text, ms) { + return new Promise((_, reject) => + setTimeout(() => reject(new Error(text)), ms) + ); +} + +async function delay(text, ms) { + return new Promise(resolve => setTimeout(() => resolve(text), ms)); +} + +async function delayTwice() { + try { + await delayedError('Delayed exception', 20); + } catch (x) { + // Ignored + } + await delay('', 10); +} + +async function delayTrice() { + const p = delayTwice(); + await delay('', 40); + return p; +} + async function Bar({children}) { - await new Promise(resolve => setTimeout(() => resolve('deferred text'), 10)); + await delayTrice(); return
{children}
; } async function ThirdPartyComponent() { - return new Promise(resolve => - setTimeout(() => resolve('hello from a 3rd party'), 30) - ); + return await delay('hello from a 3rd party', 30); } -// Using Web streams for tee'ing convenience here. -let cachedThirdPartyReadableWeb; - -function fetchThirdParty(Component) { - if (cachedThirdPartyReadableWeb) { - const [readableWeb1, readableWeb2] = cachedThirdPartyReadableWeb.tee(); - cachedThirdPartyReadableWeb = readableWeb1; +let cachedThirdPartyStream; + +// We create the Component outside of AsyncLocalStorage so that it has no owner. +// That way it gets the owner from the call to createFromNodeStream. +const thirdPartyComponent = ; + +function simulateFetch(cb, latencyMs) { + return new Promise(resolve => { + // Request latency + setTimeout(() => { + const result = cb(); + // Response latency + setTimeout(() => { + resolve(result); + }, latencyMs); + }, latencyMs); + }); +} - return createFromNodeStream(Readable.fromWeb(readableWeb2), { - moduleMap: {}, - moduleLoading: {}, - }); +async function fetchThirdParty(noCache) { + // We're using the Web Streams APIs for tee'ing convenience. + let stream; + if (cachedThirdPartyStream && !noCache) { + stream = cachedThirdPartyStream; + } else { + stream = await simulateFetch( + () => + renderToReadableStream( + thirdPartyComponent, + {}, + {environmentName: 'third-party'} + ), + 25 + ); } - const stream = renderToPipeableStream( - , - {}, - {environmentName: 'third-party'} - ); + const [stream1, stream2] = stream.tee(); + cachedThirdPartyStream = stream1; - const readable = new PassThrough(); - // React currently only supports piping to one stream, so we convert, tee, and - // convert back again. - // TODO: Switch to web streams without converting when #33442 has landed. - const [readableWeb1, readableWeb2] = Readable.toWeb(readable).tee(); - cachedThirdPartyReadableWeb = readableWeb1; - const result = createFromNodeStream(Readable.fromWeb(readableWeb2), { - moduleMap: {}, - moduleLoading: {}, + return createFromReadableStream(stream2, { + serverConsumerManifest: { + moduleMap: {}, + serverModuleMap: null, + moduleLoading: null, + }, }); - stream.pipe(readable); - - return result; } -async function ServerComponent() { - await new Promise(resolve => setTimeout(() => resolve('deferred text'), 50)); - return await fetchThirdParty(); +async function ServerComponent({noCache}) { + await delay('deferred text', 50); + return await fetchThirdParty(noCache); } -export default async function App({prerender}) { +let veryDeepObject = [ + { + bar: { + baz: { + a: {}, + }, + }, + }, + { + bar: { + baz: { + a: {}, + }, + }, + }, + { + bar: { + baz: { + a: {}, + }, + }, + }, + { + bar: { + baz: { + a: { + b: { + c: { + d: { + e: { + f: { + g: { + h: { + i: { + j: { + k: { + l: { + m: { + yay: 'You reached the end', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +]; + +export default async function App({prerender, noCache}) { const res = await fetch('http://localhost:3001/todos'); const todos = await res.json(); + await sdkMethod('http://localhost:3001/todos'); + + console.log('Expand me:', veryDeepObject); - const dedupedChild = ; + const dedupedChild = ; const message = getServerState(); return ( diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index f08f7a110bf61..7e4e8c801ef95 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -16,18 +16,49 @@ function findSourceMapURL(fileName) { let updateRoot; async function callServer(id, args) { - const response = fetch('/', { - method: 'POST', - headers: { - Accept: 'text/x-component', - 'rsc-action': id, - }, - body: await encodeReply(args), - }); - const {returnValue, root} = await createFromFetch(response, { - callServer, - findSourceMapURL, - }); + let response; + if ( + process.env.NODE_ENV === 'development' && + typeof WebSocketStream === 'function' + ) { + const requestId = crypto.randomUUID(); + const wss = new WebSocketStream( + 'ws://localhost:3001/debug-channel?' + requestId + ); + const debugChannel = await wss.opened; + response = createFromFetch( + fetch('/', { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'rsc-action': id, + 'rsc-request-id': requestId, + }, + body: await encodeReply(args), + }), + { + callServer, + debugChannel, + findSourceMapURL, + } + ); + } else { + response = createFromFetch( + fetch('/', { + method: 'POST', + headers: { + Accept: 'text/x-component', + 'rsc-action': id, + }, + body: await encodeReply(args), + }), + { + callServer, + findSourceMapURL, + } + ); + } + const {returnValue, root} = await response; // Refresh the tree with the new RSC payload. startTransition(() => { updateRoot(root); @@ -42,17 +73,43 @@ function Shell({data}) { } async function hydrateApp() { - const {root, returnValue, formState} = await createFromFetch( - fetch('/', { - headers: { - Accept: 'text/x-component', - }, - }), - { - callServer, - findSourceMapURL, - } - ); + let response; + if ( + process.env.NODE_ENV === 'development' && + typeof WebSocketStream === 'function' + ) { + const requestId = crypto.randomUUID(); + const wss = new WebSocketStream( + 'ws://localhost:3001/debug-channel?' + requestId + ); + const debugChannel = await wss.opened; + response = createFromFetch( + fetch('/', { + headers: { + Accept: 'text/x-component', + 'rsc-request-id': requestId, + }, + }), + { + callServer, + debugChannel, + findSourceMapURL, + } + ); + } else { + response = createFromFetch( + fetch('/', { + headers: { + Accept: 'text/x-component', + }, + }), + { + callServer, + findSourceMapURL, + } + ); + } + const {root, returnValue, formState} = await response; ReactDOM.hydrateRoot( document, diff --git a/fixtures/flight/src/library.js b/fixtures/flight/src/library.js new file mode 100644 index 0000000000000..744205d1c40fe --- /dev/null +++ b/fixtures/flight/src/library.js @@ -0,0 +1,9 @@ +export async function sdkMethod(input, init) { + return fetch(input, init).then(async response => { + await new Promise(resolve => { + setTimeout(resolve, 10); + }); + + return response; + }); +} diff --git a/fixtures/view-transition/loader/package.json b/fixtures/view-transition/loader/package.json new file mode 100644 index 0000000000000..3dbc1ca591c05 --- /dev/null +++ b/fixtures/view-transition/loader/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/fixtures/view-transition/loader/server.js b/fixtures/view-transition/loader/server.js new file mode 100644 index 0000000000000..f56ac9fd039b5 --- /dev/null +++ b/fixtures/view-transition/loader/server.js @@ -0,0 +1,54 @@ +import babel from '@babel/core'; + +const babelOptions = { + babelrc: false, + ignore: [/\/(build|node_modules)\//], + plugins: [ + '@babel/plugin-syntax-import-meta', + '@babel/plugin-transform-react-jsx', + ], +}; + +export async function load(url, context, defaultLoad) { + if (url.endsWith('.css')) { + return {source: 'export default {}', format: 'module', shortCircuit: true}; + } + const {format} = context; + const result = await defaultLoad(url, context, defaultLoad); + if (result.format === 'module') { + const opt = Object.assign({filename: url}, babelOptions); + const newResult = await babel.transformAsync(result.source, opt); + if (!newResult) { + if (typeof result.source === 'string') { + return result; + } + return { + source: Buffer.from(result.source).toString('utf8'), + format: 'module', + }; + } + return {source: newResult.code, format: 'module'}; + } + return defaultLoad(url, context, defaultLoad); +} + +async function babelTransformSource(source, context, defaultTransformSource) { + const {format} = context; + if (format === 'module') { + const opt = Object.assign({filename: context.url}, babelOptions); + const newResult = await babel.transformAsync(source, opt); + if (!newResult) { + if (typeof source === 'string') { + return {source}; + } + return { + source: Buffer.from(source).toString('utf8'), + }; + } + return {source: newResult.code}; + } + return defaultTransformSource(source, context, defaultTransformSource); +} + +export const transformSource = + process.version < 'v16' ? babelTransformSource : undefined; diff --git a/fixtures/view-transition/package.json b/fixtures/view-transition/package.json index 8d222b29d3c07..44a8ff0bfa541 100644 --- a/fixtures/view-transition/package.json +++ b/fixtures/view-transition/package.json @@ -13,7 +13,8 @@ "express": "^4.14.0", "ignore-styles": "^5.0.1", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "animation-timelines": "^0.0.4" }, "eslintConfig": { "extends": [ @@ -27,8 +28,8 @@ "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev:client": "BROWSER=none PORT=3001 react-scripts start", - "dev:server": "NODE_ENV=development node server", - "start": "react-scripts build && NODE_ENV=production node server", + "dev:server": "NODE_ENV=development node --experimental-loader ./loader/server.js server", + "start": "react-scripts build && NODE_ENV=production node --experimental-loader ./loader/server.js server", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" diff --git a/fixtures/view-transition/server/index.js b/fixtures/view-transition/server/index.js index 3f542b8f6e67d..e13d4706b9ef9 100644 --- a/fixtures/view-transition/server/index.js +++ b/fixtures/view-transition/server/index.js @@ -20,13 +20,15 @@ if (process.env.NODE_ENV === 'development') { for (var key in require.cache) { delete require.cache[key]; } - const render = require('./render').default; - render(req.url, res); + import('./render.js').then(({default: render}) => { + render(req.url, res); + }); }); } else { - const render = require('./render').default; - app.get('/', function (req, res) { - render(req.url, res); + import('./render.js').then(({default: render}) => { + app.get('/', function (req, res) { + render(req.url, res); + }); }); } diff --git a/fixtures/view-transition/server/render.js b/fixtures/view-transition/server/render.js index 11d352eabdd72..08224a57c4da2 100644 --- a/fixtures/view-transition/server/render.js +++ b/fixtures/view-transition/server/render.js @@ -1,7 +1,7 @@ import React from 'react'; import {renderToPipeableStream} from 'react-dom/server'; -import App from '../src/components/App'; +import App from '../src/components/App.js'; let assets; if (process.env.NODE_ENV === 'development') { diff --git a/fixtures/view-transition/src/components/App.js b/fixtures/view-transition/src/components/App.js index 275e594d87a1d..6b41bdf4eac2a 100644 --- a/fixtures/view-transition/src/components/App.js +++ b/fixtures/view-transition/src/components/App.js @@ -6,8 +6,8 @@ import React, { unstable_addTransitionType as addTransitionType, } from 'react'; -import Chrome from './Chrome'; -import Page from './Page'; +import Chrome from './Chrome.js'; +import Page from './Page.js'; const enableNavigationAPI = typeof navigation === 'object'; diff --git a/fixtures/view-transition/src/components/NestedReveal.js b/fixtures/view-transition/src/components/NestedReveal.js new file mode 100644 index 0000000000000..497f4430f6cfb --- /dev/null +++ b/fixtures/view-transition/src/components/NestedReveal.js @@ -0,0 +1,36 @@ +import React, {Suspense, use} from 'react'; + +async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function Use({useable}) { + use(useable); + return null; +} + +let delay1; +let delay2; + +export default function NestedReveal({}) { + if (!delay1) { + delay1 = sleep(100); + // Needs to happen before the throttled reveal of delay 1 + delay2 = sleep(200); + } + + return ( +
+ Shell + +
Level 1
+ + + +
Level 2
+ +
+
+
+ ); +} diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 39d0803af7700..ef1a855320634 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -13,11 +13,12 @@ import React, { import {createPortal} from 'react-dom'; -import SwipeRecognizer from './SwipeRecognizer'; +import SwipeRecognizer from './SwipeRecognizer.js'; import './Page.css'; import transitions from './Transitions.module.css'; +import NestedReveal from './NestedReveal.js'; async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -241,6 +242,7 @@ export default function Page({url, navigate}) {
+
); } diff --git a/fixtures/view-transition/src/components/SwipeRecognizer.js b/fixtures/view-transition/src/components/SwipeRecognizer.js index 7e7176d194d83..df4d743e1ba7f 100644 --- a/fixtures/view-transition/src/components/SwipeRecognizer.js +++ b/fixtures/view-transition/src/components/SwipeRecognizer.js @@ -5,6 +5,16 @@ import React, { unstable_startGestureTransition as startGestureTransition, } from 'react'; +import ScrollTimelinePolyfill from 'animation-timelines/scroll-timeline'; +import TouchPanTimeline from 'animation-timelines/touch-pan-timeline'; + +const ua = typeof navigator === 'undefined' ? '' : navigator.userAgent; +const isSafariMobile = + ua.indexOf('Safari') !== -1 && + (ua.indexOf('iPhone') !== -1 || + ua.indexOf('iPad') !== -1 || + ua.indexOf('iPod') !== -1); + // Example of a Component that can recognize swipe gestures using a ScrollTimeline // without scrolling its own content. Allowing it to be used as an inert gesture // recognizer to drive a View Transition. @@ -21,18 +31,72 @@ export default function SwipeRecognizer({ const scrollRef = useRef(null); const activeGesture = useRef(null); - function onScroll() { - if (activeGesture.current !== null) { + const touchTimeline = useRef(null); + + function onTouchStart(event) { + if (!isSafariMobile && typeof ScrollTimeline === 'function') { + // If not Safari and native ScrollTimeline is supported, then we use that. return; } - if (typeof ScrollTimeline !== 'function') { + if (touchTimeline.current) { + // We can catch the gesture before it settles. return; } - // eslint-disable-next-line no-undef - const scrollTimeline = new ScrollTimeline({ - source: scrollRef.current, + const scrollElement = scrollRef.current; + const bounds = + axis === 'x' ? scrollElement.clientWidth : scrollElement.clientHeight; + const range = + direction === 'left' || direction === 'up' ? [bounds, 0] : [0, -bounds]; + const timeline = new TouchPanTimeline({ + touch: event, + source: scrollElement, axis: axis, + range: range, + snap: range, }); + touchTimeline.current = timeline; + timeline.settled.then(() => { + if (touchTimeline.current !== timeline) { + return; + } + touchTimeline.current = null; + const changed = + direction === 'left' || direction === 'up' + ? timeline.currentTime < 50 + : timeline.currentTime > 50; + onGestureEnd(changed); + }); + } + + function onTouchEnd() { + if (activeGesture.current === null) { + // If we didn't start a gesture before we release, we can release our + // timeline. + touchTimeline.current = null; + } + } + + function onScroll() { + if (activeGesture.current !== null) { + return; + } + + let scrollTimeline; + if (touchTimeline.current) { + // We're in a polyfilled touch gesture. Let's use that timeline instead. + scrollTimeline = touchTimeline.current; + } else if (typeof ScrollTimeline === 'function') { + // eslint-disable-next-line no-undef + scrollTimeline = new ScrollTimeline({ + source: scrollRef.current, + axis: axis, + }); + } else { + scrollTimeline = new ScrollTimelinePolyfill({ + source: scrollRef.current, + axis: axis, + }); + } activeGesture.current = startGestureTransition( scrollTimeline, () => { @@ -49,7 +113,23 @@ export default function SwipeRecognizer({ } ); } + function onGestureEnd(changed) { + // Reset scroll + if (changed) { + // Trigger side-effects + startTransition(action); + } + if (activeGesture.current !== null) { + const cancelGesture = activeGesture.current; + activeGesture.current = null; + cancelGesture(); + } + } function onScrollEnd() { + if (touchTimeline.current) { + // We have a touch gesture controlling the swipe. + return; + } let changed; const scrollElement = scrollRef.current; if (axis === 'x') { @@ -67,16 +147,7 @@ export default function SwipeRecognizer({ ? scrollElement.scrollTop < halfway : scrollElement.scrollTop > halfway; } - // Reset scroll - if (changed) { - // Trigger side-effects - startTransition(action); - } - if (activeGesture.current !== null) { - const cancelGesture = activeGesture.current; - activeGesture.current = null; - cancelGesture(); - } + onGestureEnd(changed); } useEffect(() => { @@ -168,6 +239,9 @@ export default function SwipeRecognizer({ return (
diff --git a/fixtures/view-transition/src/index.js b/fixtures/view-transition/src/index.js index 8c2fac3e67ada..29b53bf037928 100644 --- a/fixtures/view-transition/src/index.js +++ b/fixtures/view-transition/src/index.js @@ -1,7 +1,7 @@ import React from 'react'; import {hydrateRoot} from 'react-dom/client'; -import App from './components/App'; +import App from './components/App.js'; hydrateRoot( document, diff --git a/fixtures/view-transition/yarn.lock b/fixtures/view-transition/yarn.lock index 76a6af00ca2ef..3efb208f1ec1a 100644 --- a/fixtures/view-transition/yarn.lock +++ b/fixtures/view-transition/yarn.lock @@ -2427,6 +2427,11 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +animation-timelines@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/animation-timelines/-/animation-timelines-0.0.4.tgz#7ac4614bae73c4d1ea2ff18d5d87a518793258af" + integrity sha512-HwCE3m1nM8ZdLbwDwD1j5ZNKmY+3J2CliXJNIsf3y1Si927SIaWpfxkycTg5nWLJSHgjsYxrmOy2Jbo4JR1e9A== + ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" diff --git a/package.json b/package.json index 6ad9211568541..9e7eb8dc5334d 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "lint-build": "node ./scripts/rollup/validate/index.js", "extract-errors": "node scripts/error-codes/extract-errors.js", "postinstall": "node ./scripts/flow/createFlowConfigs.js", - "pretest": "./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh", "test": "node ./scripts/jest/jest-cli.js", "test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable", "test-www": "node ./scripts/jest/jest-cli.js --release-channel=www-modern", diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 9bb797bac395b..ecb97b3a03059 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -156,7 +156,8 @@ function normalizeCodeLocInfo(str) { // at Component (/path/filename.js:123:45) // React format: // in Component (at filename.js:123) - return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return str.replace(/\n +(?:at|in) ([^(\[\n]+)[^\n]*/g, function (m, name) { + name = name.trim(); if (name.endsWith('.render')) { // Class components will have the `render` method as part of their stack trace. // We strip that out in our normalization to make it look more like component stacks. diff --git a/packages/react-client/src/ReactClientConsoleConfigBrowser.js b/packages/react-client/src/ReactClientConsoleConfigBrowser.js index 355edc9c08197..bc39763275240 100644 --- a/packages/react-client/src/ReactClientConsoleConfigBrowser.js +++ b/packages/react-client/src/ReactClientConsoleConfigBrowser.js @@ -7,6 +7,7 @@ * @flow */ +// Keep in sync with ReactServerConsoleConfig const badgeFormat = '%c%s%c '; // Same badge styling as DevTools. const badgeStyle = diff --git a/packages/react-client/src/ReactClientConsoleConfigPlain.js b/packages/react-client/src/ReactClientConsoleConfigPlain.js index 6b41ad4effe98..5fe553744a9fd 100644 --- a/packages/react-client/src/ReactClientConsoleConfigPlain.js +++ b/packages/react-client/src/ReactClientConsoleConfigPlain.js @@ -7,6 +7,7 @@ * @flow */ +// Keep in sync with ReactServerConsoleConfig const badgeFormat = '[%s] '; const pad = ' '; diff --git a/packages/react-client/src/ReactClientConsoleConfigServer.js b/packages/react-client/src/ReactClientConsoleConfigServer.js index efbcd2865d712..1978a4bc8b8de 100644 --- a/packages/react-client/src/ReactClientConsoleConfigServer.js +++ b/packages/react-client/src/ReactClientConsoleConfigServer.js @@ -7,6 +7,7 @@ * @flow */ +// Keep in sync with ReactServerConsoleConfig // This flips color using ANSI, then sets a color styling, then resets. const badgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c '; // Same badge styling as DevTools. diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 78a2d85eea287..da00763840a75 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -77,9 +77,13 @@ import { markAllTracksInOrder, logComponentRender, logDedupedComponentRender, + logComponentAborted, logComponentErrored, logIOInfo, + logIOInfoErrored, logComponentAwait, + logComponentAwaitAborted, + logComponentAwaitErrored, } from './ReactFlightPerformanceTrack'; import { @@ -96,6 +100,8 @@ import {getOwnerStackByComponentInfoInDev} from 'shared/ReactComponentInfoStack' import {injectInternals} from './ReactFlightClientDevToolsHook'; +import {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess'; + import ReactVersion from 'shared/ReactVersion'; import isArray from 'shared/isArray'; @@ -155,21 +161,20 @@ const RESOLVED_MODEL = 'resolved_model'; const RESOLVED_MODULE = 'resolved_module'; const INITIALIZED = 'fulfilled'; const ERRORED = 'rejected'; +const HALTED = 'halted'; // DEV-only. Means it never resolves even if connection closes. type PendingChunk = { status: 'pending', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, + value: null | Array mixed)>, + reason: null | Array mixed)>, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type BlockedChunk = { status: 'blocked', - value: null | Array<(T) => mixed>, - reason: null | Array<(mixed) => mixed>, - _response: Response, + value: null | Array mixed)>, + reason: null | Array mixed)>, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -177,8 +182,7 @@ type BlockedChunk = { type ResolvedModelChunk = { status: 'resolved_model', value: UninitializedModel, - reason: null, - _response: Response, + reason: Response, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -187,7 +191,6 @@ type ResolvedModuleChunk = { status: 'resolved_module', value: ClientReference, reason: null, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -196,7 +199,6 @@ type InitializedChunk = { status: 'fulfilled', value: T, reason: null | FlightStreamController, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -207,7 +209,6 @@ type InitializedStreamChunk< status: 'fulfilled', value: T, reason: FlightStreamController, - _response: Response, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, @@ -216,7 +217,14 @@ type ErroredChunk = { status: 'rejected', value: null, reason: mixed, - _response: Response, + _children: Array> | ProfilingResult, // Profiling-only + _debugInfo?: null | ReactDebugInfo, // DEV-only + then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, +}; +type HaltedChunk = { + status: 'halted', + value: null, + reason: null, _children: Array> | ProfilingResult, // Profiling-only _debugInfo?: null | ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, @@ -227,19 +235,14 @@ type SomeChunk = | ResolvedModelChunk | ResolvedModuleChunk | InitializedChunk - | ErroredChunk; + | ErroredChunk + | HaltedChunk; // $FlowFixMe[missing-this-annot] -function ReactPromise( - status: any, - value: any, - reason: any, - response: Response, -) { +function ReactPromise(status: any, value: any, reason: any) { this.status = status; this.value = value; this.reason = reason; - this._response = response; if (enableProfilerTimer && enableComponentPerformanceTrack) { this._children = []; } @@ -290,25 +293,32 @@ ReactPromise.prototype.then = function ( // The status might have changed after initialization. switch (chunk.status) { case INITIALIZED: - resolve(chunk.value); + if (typeof resolve === 'function') { + resolve(chunk.value); + } break; case PENDING: case BLOCKED: - if (resolve) { + if (typeof resolve === 'function') { if (chunk.value === null) { - chunk.value = ([]: Array<(T) => mixed>); + chunk.value = ([]: Array mixed)>); } chunk.value.push(resolve); } - if (reject) { + if (typeof reject === 'function') { if (chunk.reason === null) { - chunk.reason = ([]: Array<(mixed) => mixed>); + chunk.reason = ([]: Array< + InitializationReference | (mixed => mixed), + >); } chunk.reason.push(reject); } break; + case HALTED: { + break; + } default: - if (reject) { + if (typeof reject === 'function') { reject(chunk.reason); } break; @@ -320,7 +330,9 @@ export type FindSourceMapURLCallback = ( environmentName: string, ) => null | string; -export type Response = { +export type DebugChannelCallback = (message: string) => void; + +type Response = { _bundlerConfig: ServerConsumerModuleMap, _serverReferenceConfig: null | ServerManifest, _moduleLoading: ModuleLoading, @@ -339,14 +351,66 @@ export type Response = { _closedReason: mixed, _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from _timeOrigin: number, // Profiling-only + _pendingInitialRender: null | TimeoutID, // Profiling-only, + _pendingChunks: number, // DEV-only + _weakResponse: WeakResponse, // DEV-only _debugRootOwner?: null | ReactComponentInfo, // DEV-only _debugRootStack?: null | Error, // DEV-only _debugRootTask?: null | ConsoleTask, // DEV-only _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only + _debugChannel?: void | DebugChannelCallback, // DEV-only _replayConsole: boolean, // DEV-only _rootEnvironmentName: string, // DEV-only, the requested environment name. }; +// This indirection exists only to clean up DebugChannel when all Lazy References are GC:ed. +// Therefore we only use the indirection in DEV. +type WeakResponse = { + weak: WeakRef, + response: null | Response, // This is null when there are no pending chunks. +}; + +export type {WeakResponse as Response}; + +function hasGCedResponse(weakResponse: WeakResponse): boolean { + return __DEV__ && weakResponse.weak.deref() === undefined; +} + +function unwrapWeakResponse(weakResponse: WeakResponse): Response { + if (__DEV__) { + const response = weakResponse.weak.deref(); + if (response === undefined) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'We did not expect to receive new data after GC:ing the response.', + ); + } + return response; + } else { + return (weakResponse: any); // In prod we just use the real Response directly. + } +} + +function getWeakResponse(response: Response): WeakResponse { + if (__DEV__) { + return response._weakResponse; + } else { + return (response: any); // In prod we just use the real Response directly. + } +} + +function cleanupDebugChannel(debugChannel: DebugChannelCallback): void { + // When a Response gets GC:ed because nobody is referring to any of the objects that lazily + // loads from the Response anymore, then we can close the debug channel. + debugChannel(''); +} + +// If FinalizationRegistry doesn't exist, we cannot use the debugChannel. +const debugChannelRegistry = + __DEV__ && typeof FinalizationRegistry === 'function' + ? new FinalizationRegistry(cleanupDebugChannel) + : null; + function readChunk(chunk: SomeChunk): T { // If we have resolved content, we try to initialize it first which // might put us back into one of the other states. @@ -364,6 +428,7 @@ function readChunk(chunk: SomeChunk): T { return chunk.value; case PENDING: case BLOCKED: + case HALTED: // eslint-disable-next-line no-throw-literal throw ((chunk: any): Thenable); default: @@ -371,19 +436,48 @@ function readChunk(chunk: SomeChunk): T { } } -export function getRoot(response: Response): Thenable { +export function getRoot(weakResponse: WeakResponse): Thenable { + const response = unwrapWeakResponse(weakResponse); const chunk = getChunk(response, 0); return (chunk: any); } function createPendingChunk(response: Response): PendingChunk { + if (__DEV__) { + // Retain a strong reference to the Response while we wait for the result. + if (response._pendingChunks++ === 0) { + response._weakResponse.response = response; + if (response._pendingInitialRender !== null) { + clearTimeout(response._pendingInitialRender); + response._pendingInitialRender = null; + } + } + } // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(PENDING, null, null, response); + return new ReactPromise(PENDING, null, null); +} + +function releasePendingChunk(response: Response, chunk: SomeChunk): void { + if (__DEV__ && chunk.status === PENDING) { + if (--response._pendingChunks === 0) { + // We're no longer waiting for any more chunks. We can release the strong reference + // to the response. We'll regain it if we ask for any more data later on. + response._weakResponse.response = null; + // Wait a short period to see if any more chunks get asked for. E.g. by a React render. + // These chunks might discover more pending chunks. + // If we don't ask for more then we assume that those chunks weren't blocking initial + // render and are excluded from the performance track. + response._pendingInitialRender = setTimeout( + flushInitialRenderPerformance.bind(null, response), + 100, + ); + } + } } function createBlockedChunk(response: Response): BlockedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(BLOCKED, null, null, response); + return new ReactPromise(BLOCKED, null, null); } function createErrorChunk( @@ -391,27 +485,99 @@ function createErrorChunk( error: mixed, ): ErroredChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(ERRORED, null, error, response); + return new ReactPromise(ERRORED, null, error); } -function wakeChunk(listeners: Array<(T) => mixed>, value: T): void { +function wakeChunk( + listeners: Array mixed)>, + value: T, +): void { for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; - listener(value); + if (typeof listener === 'function') { + listener(value); + } else { + fulfillReference(listener, value); + } + } +} + +function rejectChunk( + listeners: Array mixed)>, + error: mixed, +): void { + for (let i = 0; i < listeners.length; i++) { + const listener = listeners[i]; + if (typeof listener === 'function') { + listener(error); + } else { + rejectReference(listener, error); + } + } +} + +function resolveBlockedCycle( + resolvedChunk: SomeChunk, + reference: InitializationReference, +): null | InitializationHandler { + const referencedChunk = reference.handler.chunk; + if (referencedChunk === null) { + return null; + } + if (referencedChunk === resolvedChunk) { + // We found the cycle. We can resolve the blocked cycle now. + return reference.handler; + } + const resolveListeners = referencedChunk.value; + if (resolveListeners !== null) { + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener !== 'function') { + const foundHandler = resolveBlockedCycle(resolvedChunk, listener); + if (foundHandler !== null) { + return foundHandler; + } + } + } } + return null; } function wakeChunkIfInitialized( chunk: SomeChunk, - resolveListeners: Array<(T) => mixed>, - rejectListeners: null | Array<(mixed) => mixed>, + resolveListeners: Array mixed)>, + rejectListeners: null | Array mixed)>, ): void { switch (chunk.status) { case INITIALIZED: wakeChunk(resolveListeners, chunk.value); break; - case PENDING: case BLOCKED: + // It is possible that we're blocked on our own chunk if it's a cycle. + // Before adding back the listeners to the chunk, let's check if it would + // result in a cycle. + for (let i = 0; i < resolveListeners.length; i++) { + const listener = resolveListeners[i]; + if (typeof listener !== 'function') { + const reference: InitializationReference = listener; + const cyclicHandler = resolveBlockedCycle(chunk, reference); + if (cyclicHandler !== null) { + // This reference points back to this chunk. We can resolve the cycle by + // using the value from that handler. + fulfillReference(reference, cyclicHandler.value); + resolveListeners.splice(i, 1); + i--; + if (rejectListeners !== null) { + const rejectionIdx = rejectListeners.indexOf(reference); + if (rejectionIdx !== -1) { + rejectListeners.splice(rejectionIdx, 1); + } + } + } + } + } + // Fallthrough + case PENDING: if (chunk.value) { for (let i = 0; i < resolveListeners.length; i++) { chunk.value.push(resolveListeners[i]); @@ -433,13 +599,17 @@ function wakeChunkIfInitialized( break; case ERRORED: if (rejectListeners) { - wakeChunk(rejectListeners, chunk.reason); + rejectChunk(rejectListeners, chunk.reason); } break; } } -function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { +function triggerErrorOnChunk( + response: Response, + chunk: SomeChunk, + error: mixed, +): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { // If we get more data to an already resolved ID, we assume that it's // a stream chunk since any other row shouldn't have more than one entry. @@ -449,12 +619,13 @@ function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { controller.error(error); return; } + releasePendingChunk(response, chunk); const listeners = chunk.reason; const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; erroredChunk.reason = error; if (listeners !== null) { - wakeChunk(listeners, error); + rejectChunk(listeners, error); } } @@ -463,7 +634,7 @@ function createResolvedModelChunk( value: UninitializedModel, ): ResolvedModelChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODEL, value, null, response); + return new ReactPromise(RESOLVED_MODEL, value, response); } function createResolvedModuleChunk( @@ -471,7 +642,7 @@ function createResolvedModuleChunk( value: ClientReference, ): ResolvedModuleChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODULE, value, null, response); + return new ReactPromise(RESOLVED_MODULE, value, null); } function createInitializedTextChunk( @@ -479,7 +650,7 @@ function createInitializedTextChunk( value: string, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null); } function createInitializedBufferChunk( @@ -487,7 +658,7 @@ function createInitializedBufferChunk( value: $ArrayBufferView | ArrayBuffer, ): InitializedChunk { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, null, response); + return new ReactPromise(INITIALIZED, value, null); } function createInitializedIteratorResultChunk( @@ -496,12 +667,7 @@ function createInitializedIteratorResultChunk( done: boolean, ): InitializedChunk> { // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise( - INITIALIZED, - {done: done, value: value}, - null, - response, - ); + return new ReactPromise(INITIALIZED, {done: done, value: value}, null); } function createInitializedStreamChunk< @@ -514,7 +680,7 @@ function createInitializedStreamChunk< // We use the reason field to stash the controller since we already have that // field. It's a bit of a hack but efficient. // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(INITIALIZED, value, controller, response); + return new ReactPromise(INITIALIZED, value, controller); } function createResolvedIteratorResultChunk( @@ -526,10 +692,11 @@ function createResolvedIteratorResultChunk( const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, null, response); + return new ReactPromise(RESOLVED_MODEL, iteratorResultJSON, response); } function resolveIteratorResultChunk( + response: Response, chunk: SomeChunk>, value: UninitializedModel, done: boolean, @@ -537,10 +704,11 @@ function resolveIteratorResultChunk( // To reuse code as much code as possible we add the wrapper element as part of the JSON. const iteratorResultJSON = (done ? '{"done":true,"value":' : '{"done":false,"value":') + value + '}'; - resolveModelChunk(chunk, iteratorResultJSON); + resolveModelChunk(response, chunk, iteratorResultJSON); } function resolveModelChunk( + response: Response, chunk: SomeChunk, value: UninitializedModel, ): void { @@ -552,11 +720,13 @@ function resolveModelChunk( controller.enqueueModel(value); return; } + releasePendingChunk(response, chunk); const resolveListeners = chunk.value; const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModelChunk = (chunk: any); resolvedChunk.status = RESOLVED_MODEL; resolvedChunk.value = value; + resolvedChunk.reason = response; if (resolveListeners !== null) { // This is unfortunate that we're reading this eagerly if // we already have listeners attached since they might no @@ -568,6 +738,7 @@ function resolveModelChunk( } function resolveModuleChunk( + response: Response, chunk: SomeChunk, value: ClientReference, ): void { @@ -575,6 +746,7 @@ function resolveModuleChunk( // We already resolved. We didn't expect to see this. return; } + releasePendingChunk(response, chunk); const resolveListeners = chunk.value; const rejectListeners = chunk.reason; const resolvedChunk: ResolvedModuleChunk = (chunk: any); @@ -586,6 +758,19 @@ function resolveModuleChunk( } } +type InitializationReference = { + response: Response, // TODO: Remove Response from here and pass it through instead. + handler: InitializationHandler, + parentObject: Object, + key: string, + map: ( + response: Response, + model: any, + parentObject: Object, + key: string, + ) => any, + path: Array, +}; type InitializationHandler = { parent: null | InitializationHandler, chunk: null | BlockedChunk, @@ -602,6 +787,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { initializingHandler = null; const resolvedModel = chunk.value; + const response = chunk.reason; // We go to the BLOCKED state until we've fully resolved this. // We do this before parsing in case we try to initialize the same chunk @@ -616,7 +802,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { } try { - const value: T = parseModel(chunk._response, resolvedModel); + const value: T = parseModel(response, resolvedModel); // Invoke any listeners added while resolving this model. I.e. cyclic // references. This may or may not fully resolve the model depending on // if they were blocked. @@ -668,7 +854,15 @@ function initializeModuleChunk(chunk: ResolvedModuleChunk): void { // Report that any missing chunks in the model is now going to throw this // error upon read. Also notify any pending promises. -export function reportGlobalError(response: Response, error: Error): void { +export function reportGlobalError( + weakResponse: WeakResponse, + error: Error, +): void { + if (hasGCedResponse(weakResponse)) { + // Ignore close signal if we are not awaiting any more pending chunks. + return; + } + const response = unwrapWeakResponse(weakResponse); response._closed = true; response._closedReason = error; response._chunks.forEach(chunk => { @@ -676,18 +870,17 @@ export function reportGlobalError(response: Response, error: Error): void { // trigger an error but if it wasn't then we need to // because we won't be getting any new data to resolve it. if (chunk.status === PENDING) { - triggerErrorOnChunk(chunk, error); + triggerErrorOnChunk(response, chunk, error); } }); - if (enableProfilerTimer && enableComponentPerformanceTrack) { - markAllTracksInOrder(); - flushComponentPerformance( - response, - getChunk(response, 0), - 0, - -Infinity, - -Infinity, - ); + if (__DEV__) { + const debugChannel = response._debugChannel; + if (debugChannel !== undefined) { + // If we don't have any more ways of reading data, we don't have to send any + // more neither. So we close the writable side. + debugChannel(''); + response._debugChannel = undefined; + } } } @@ -742,13 +935,85 @@ function getTaskName(type: mixed): string { } } +function initializeElement(response: Response, element: any): void { + if (!__DEV__) { + return; + } + const stack = element._debugStack; + const owner = element._owner; + if (owner === null) { + element._owner = response._debugRootOwner; + } + let env = response._rootEnvironmentName; + if (owner !== null && owner.env != null) { + // Interestingly we don't actually have the environment name of where + // this JSX was created if it doesn't have an owner but if it does + // it must be the same environment as the owner. We could send it separately + // but it seems a bit unnecessary for this edge case. + env = owner.env; + } + let normalizedStackTrace: null | Error = null; + if (owner === null && response._debugRootStack != null) { + // We override the stack if we override the owner since the stack where the root JSX + // was created on the server isn't very useful but where the request was made is. + normalizedStackTrace = response._debugRootStack; + } else if (stack !== null) { + // We create a fake stack and then create an Error object inside of it. + // This means that the stack trace is now normalized into the native format + // of the browser and the stack frames will have been registered with + // source mapping information. + // This can unfortunately happen within a user space callstack which will + // remain on the stack. + normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack, env); + } + element._debugStack = normalizedStackTrace; + let task: null | ConsoleTask = null; + if (supportsCreateTask && stack !== null) { + const createTaskFn = (console: any).createTask.bind( + console, + getTaskName(element.type), + ); + const callStack = buildFakeCallStack( + response, + stack, + env, + false, + createTaskFn, + ); + // This owner should ideally have already been initialized to avoid getting + // user stack frames on the stack. + const ownerTask = + owner === null ? null : initializeFakeTask(response, owner); + if (ownerTask === null) { + const rootTask = response._debugRootTask; + if (rootTask != null) { + task = rootTask.run(callStack); + } else { + task = callStack(); + } + } else { + task = ownerTask.run(callStack); + } + } + element._debugTask = task; + + // This owner should ideally have already been initialized to avoid getting + // user stack frames on the stack. + if (owner !== null) { + initializeFakeStack(response, owner); + } + // TODO: We should be freezing the element but currently, we might write into + // _debugInfo later. We could move it into _store which remains mutable. + Object.freeze(element.props); +} + function createElement( response: Response, type: mixed, key: mixed, props: mixed, - owner: null | ReactComponentInfo, // DEV-only - stack: null | ReactStackTrace, // DEV-only + owner: ?ReactComponentInfo, // DEV-only + stack: ?ReactStackTrace, // DEV-only validated: number, // DEV-only ): | React$Element @@ -761,7 +1026,7 @@ function createElement( type, key, props, - _owner: __DEV__ && owner === null ? response._debugRootOwner : owner, + _owner: owner === undefined ? null : owner, }: any); Object.defineProperty(element, 'ref', { enumerable: false, @@ -799,75 +1064,18 @@ function createElement( writable: true, value: null, }); - let env = response._rootEnvironmentName; - if (owner !== null && owner.env != null) { - // Interestingly we don't actually have the environment name of where - // this JSX was created if it doesn't have an owner but if it does - // it must be the same environment as the owner. We could send it separately - // but it seems a bit unnecessary for this edge case. - env = owner.env; - } - let normalizedStackTrace: null | Error = null; - if (owner === null && response._debugRootStack != null) { - // We override the stack if we override the owner since the stack where the root JSX - // was created on the server isn't very useful but where the request was made is. - normalizedStackTrace = response._debugRootStack; - } else if (stack !== null) { - // We create a fake stack and then create an Error object inside of it. - // This means that the stack trace is now normalized into the native format - // of the browser and the stack frames will have been registered with - // source mapping information. - // This can unfortunately happen within a user space callstack which will - // remain on the stack. - normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack, env); - } Object.defineProperty(element, '_debugStack', { configurable: false, enumerable: false, writable: true, - value: normalizedStackTrace, + value: stack === undefined ? null : stack, }); - - let task: null | ConsoleTask = null; - if (supportsCreateTask && stack !== null) { - const createTaskFn = (console: any).createTask.bind( - console, - getTaskName(type), - ); - const callStack = buildFakeCallStack( - response, - stack, - env, - false, - createTaskFn, - ); - // This owner should ideally have already been initialized to avoid getting - // user stack frames on the stack. - const ownerTask = - owner === null ? null : initializeFakeTask(response, owner, env); - if (ownerTask === null) { - const rootTask = response._debugRootTask; - if (rootTask != null) { - task = rootTask.run(callStack); - } else { - task = callStack(); - } - } else { - task = ownerTask.run(callStack); - } - } Object.defineProperty(element, '_debugTask', { configurable: false, enumerable: false, writable: true, - value: task, + value: null, }); - - // This owner should ideally have already been initialized to avoid getting - // user stack frames on the stack. - if (owner !== null) { - initializeFakeStack(response, owner); - } } if (initializingHandler !== null) { @@ -883,6 +1091,7 @@ function createElement( handler.value, ); if (__DEV__) { + initializeElement(response, element); // Conceptually the error happened inside this Element but right before // it was rendered. We don't have a client side component to render but // we can add some DebugInfo to explain that this was conceptually a @@ -911,15 +1120,15 @@ function createElement( handler.value = element; handler.chunk = blockedChunk; if (__DEV__) { - const freeze = Object.freeze.bind(Object, element.props); - blockedChunk.then(freeze, freeze); + /// After we have initialized any blocked references, initialize stack etc. + const init = initializeElement.bind(null, response, element); + blockedChunk.then(init, init); } return createLazyChunkWrapper(blockedChunk); } - } else if (__DEV__) { - // TODO: We should be freezing the element but currently, we might write into - // _debugInfo later. We could move it into _store which remains mutable. - Object.freeze(element.props); + } + if (__DEV__) { + initializeElement(response, element); } return element; @@ -958,8 +1167,191 @@ function getChunk(response: Response, id: number): SomeChunk { return chunk; } +function fulfillReference( + reference: InitializationReference, + value: any, +): void { + const {response, handler, parentObject, key, map, path} = reference; + + for (let i = 1; i < path.length; i++) { + while (value.$$typeof === REACT_LAZY_TYPE) { + // We never expect to see a Lazy node on this path because we encode those as + // separate models. This must mean that we have inserted an extra lazy node + // e.g. to replace a blocked element. We must instead look for it inside. + const referencedChunk: SomeChunk = value._payload; + if (referencedChunk === handler.chunk) { + // This is a reference to the thing we're currently blocking. We can peak + // inside of it to get the value. + value = handler.value; + continue; + } else { + switch (referencedChunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(referencedChunk); + break; + case RESOLVED_MODULE: + initializeModuleChunk(referencedChunk); + break; + } + switch (referencedChunk.status) { + case INITIALIZED: { + value = referencedChunk.value; + continue; + } + case BLOCKED: { + // It is possible that we're blocked on our own chunk if it's a cycle. + // Before adding the listener to the inner chunk, let's check if it would + // result in a cycle. + const cyclicHandler = resolveBlockedCycle( + referencedChunk, + reference, + ); + if (cyclicHandler !== null) { + // This reference points back to this chunk. We can resolve the cycle by + // using the value from that handler. + value = cyclicHandler.value; + continue; + } + // Fallthrough + } + case PENDING: { + // If we're not yet initialized we need to skip what we've already drilled + // through and then wait for the next value to become available. + path.splice(0, i - 1); + // Add "listener" to our new chunk dependency. + if (referencedChunk.value === null) { + referencedChunk.value = [reference]; + } else { + referencedChunk.value.push(reference); + } + if (referencedChunk.reason === null) { + referencedChunk.reason = [reference]; + } else { + referencedChunk.reason.push(reference); + } + return; + } + case HALTED: { + // Do nothing. We couldn't fulfill. + // TODO: Mark downstreams as halted too. + return; + } + default: { + rejectReference(reference, referencedChunk.reason); + return; + } + } + } + } + value = value[path[i]]; + } + const mappedValue = map(response, value, parentObject, key); + parentObject[key] = mappedValue; + + // If this is the root object for a model reference, where `handler.value` + // is a stale `null`, the resolved value can be used directly. + if (key === '' && handler.value === null) { + handler.value = mappedValue; + } + + // If the parent object is an unparsed React element tuple, we also need to + // update the props and owner of the parsed element object (i.e. + // handler.value). + if ( + parentObject[0] === REACT_ELEMENT_TYPE && + typeof handler.value === 'object' && + handler.value !== null && + handler.value.$$typeof === REACT_ELEMENT_TYPE + ) { + const element: any = handler.value; + switch (key) { + case '3': + element.props = mappedValue; + break; + case '4': + if (__DEV__) { + element._owner = mappedValue; + } + break; + case '5': + if (__DEV__) { + element._debugStack = mappedValue; + } + break; + } + } + + handler.deps--; + + if (handler.deps === 0) { + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + const resolveListeners = chunk.value; + const initializedChunk: InitializedChunk = (chunk: any); + initializedChunk.status = INITIALIZED; + initializedChunk.value = handler.value; + if (resolveListeners !== null) { + wakeChunk(resolveListeners, handler.value); + } + } +} + +function rejectReference( + reference: InitializationReference, + error: mixed, +): void { + const {handler, response} = reference; + + if (handler.errored) { + // We've already errored. We could instead build up an AggregateError + // but if there are multiple errors we just take the first one like + // Promise.all. + return; + } + const blockedValue = handler.value; + handler.errored = true; + handler.value = error; + const chunk = handler.chunk; + if (chunk === null || chunk.status !== BLOCKED) { + return; + } + + if (__DEV__) { + if ( + typeof blockedValue === 'object' && + blockedValue !== null && + blockedValue.$$typeof === REACT_ELEMENT_TYPE + ) { + const element = blockedValue; + // Conceptually the error happened inside this Element but right before + // it was rendered. We don't have a client side component to render but + // we can add some DebugInfo to explain that this was conceptually a + // Server side error that errored inside this element. That way any stack + // traces will point to the nearest JSX that errored - e.g. during + // serialization. + const erroredComponent: ReactComponentInfo = { + name: getComponentNameFromType(element.type) || '', + owner: element._owner, + }; + // $FlowFixMe[cannot-write] + erroredComponent.debugStack = element._debugStack; + if (supportsCreateTask) { + // $FlowFixMe[cannot-write] + erroredComponent.debugTask = element._debugTask; + } + const chunkDebugInfo: ReactDebugInfo = + chunk._debugInfo || (chunk._debugInfo = []); + chunkDebugInfo.push(erroredComponent); + } + } + + triggerErrorOnChunk(response, chunk, error); +} + function waitForReference( - referencedChunk: SomeChunk, + referencedChunk: PendingChunk | BlockedChunk, parentObject: Object, key: string, response: Response, @@ -980,128 +1372,27 @@ function waitForReference( }; } - function fulfill(value: any): void { - for (let i = 1; i < path.length; i++) { - while (value.$$typeof === REACT_LAZY_TYPE) { - // We never expect to see a Lazy node on this path because we encode those as - // separate models. This must mean that we have inserted an extra lazy node - // e.g. to replace a blocked element. We must instead look for it inside. - const chunk: SomeChunk = value._payload; - if (chunk === handler.chunk) { - // This is a reference to the thing we're currently blocking. We can peak - // inside of it to get the value. - value = handler.value; - continue; - } else if (chunk.status === INITIALIZED) { - value = chunk.value; - continue; - } else { - // If we're not yet initialized we need to skip what we've already drilled - // through and then wait for the next value to become available. - path.splice(0, i - 1); - chunk.then(fulfill, reject); - return; - } - } - value = value[path[i]]; - } - const mappedValue = map(response, value, parentObject, key); - parentObject[key] = mappedValue; - - // If this is the root object for a model reference, where `handler.value` - // is a stale `null`, the resolved value can be used directly. - if (key === '' && handler.value === null) { - handler.value = mappedValue; - } - - // If the parent object is an unparsed React element tuple, we also need to - // update the props and owner of the parsed element object (i.e. - // handler.value). - if ( - parentObject[0] === REACT_ELEMENT_TYPE && - typeof handler.value === 'object' && - handler.value !== null && - handler.value.$$typeof === REACT_ELEMENT_TYPE - ) { - const element: any = handler.value; - switch (key) { - case '3': - element.props = mappedValue; - break; - case '4': - if (__DEV__) { - element._owner = mappedValue; - } - break; - } - } - - handler.deps--; + const reference: InitializationReference = { + response, + handler, + parentObject, + key, + map, + path, + }; - if (handler.deps === 0) { - const chunk = handler.chunk; - if (chunk === null || chunk.status !== BLOCKED) { - return; - } - const resolveListeners = chunk.value; - const initializedChunk: InitializedChunk = (chunk: any); - initializedChunk.status = INITIALIZED; - initializedChunk.value = handler.value; - if (resolveListeners !== null) { - wakeChunk(resolveListeners, handler.value); - } - } + // Add "listener". + if (referencedChunk.value === null) { + referencedChunk.value = [reference]; + } else { + referencedChunk.value.push(reference); } - - function reject(error: mixed): void { - if (handler.errored) { - // We've already errored. We could instead build up an AggregateError - // but if there are multiple errors we just take the first one like - // Promise.all. - return; - } - const blockedValue = handler.value; - handler.errored = true; - handler.value = error; - const chunk = handler.chunk; - if (chunk === null || chunk.status !== BLOCKED) { - return; - } - - if (__DEV__) { - if ( - typeof blockedValue === 'object' && - blockedValue !== null && - blockedValue.$$typeof === REACT_ELEMENT_TYPE - ) { - const element = blockedValue; - // Conceptually the error happened inside this Element but right before - // it was rendered. We don't have a client side component to render but - // we can add some DebugInfo to explain that this was conceptually a - // Server side error that errored inside this element. That way any stack - // traces will point to the nearest JSX that errored - e.g. during - // serialization. - const erroredComponent: ReactComponentInfo = { - name: getComponentNameFromType(element.type) || '', - owner: element._owner, - }; - // $FlowFixMe[cannot-write] - erroredComponent.debugStack = element._debugStack; - if (supportsCreateTask) { - // $FlowFixMe[cannot-write] - erroredComponent.debugTask = element._debugTask; - } - const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = []); - chunkDebugInfo.push(erroredComponent); - } - } - - triggerErrorOnChunk(chunk, error); + if (referencedChunk.reason === null) { + referencedChunk.reason = [reference]; + } else { + referencedChunk.reason.push(reference); } - referencedChunk.then(fulfill, reject); - // Return a place holder value for now. return (null: any); } @@ -1275,7 +1566,7 @@ function loadServerReference, T>( } } - triggerErrorOnChunk(chunk, error); + triggerErrorOnChunk(response, chunk, error); } promise.then(fulfill, reject); @@ -1314,17 +1605,65 @@ function getOutlinedModel( for (let i = 1; i < path.length; i++) { while (value.$$typeof === REACT_LAZY_TYPE) { const referencedChunk: SomeChunk = value._payload; - if (referencedChunk.status === INITIALIZED) { - value = referencedChunk.value; - } else { - return waitForReference( - referencedChunk, - parentObject, - key, - response, - map, - path.slice(i - 1), - ); + switch (referencedChunk.status) { + case RESOLVED_MODEL: + initializeModelChunk(referencedChunk); + break; + case RESOLVED_MODULE: + initializeModuleChunk(referencedChunk); + break; + } + switch (referencedChunk.status) { + case INITIALIZED: { + value = referencedChunk.value; + break; + } + case BLOCKED: + case PENDING: { + return waitForReference( + referencedChunk, + parentObject, + key, + response, + map, + path.slice(i - 1), + ); + } + case HALTED: { + // Add a dependency that will never resolve. + // TODO: Mark downstreams as halted too. + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; + } else { + handler = initializingHandler = { + parent: null, + chunk: null, + value: null, + deps: 1, + errored: false, + }; + } + return (null: any); + } + default: { + // This is an error. Instead of erroring directly, we're going to encode this on + // an initialization handler so that we can catch it at the nearest Element. + if (initializingHandler) { + initializingHandler.errored = true; + initializingHandler.value = referencedChunk.reason; + } else { + initializingHandler = { + parent: null, + chunk: null, + value: referencedChunk.reason, + deps: 0, + errored: true, + }; + } + return (null: any); + } } } value = value[path[i]]; @@ -1360,6 +1699,24 @@ function getOutlinedModel( case PENDING: case BLOCKED: return waitForReference(chunk, parentObject, key, response, map, path); + case HALTED: { + // Add a dependency that will never resolve. + // TODO: Mark downstreams as halted too. + let handler: InitializationHandler; + if (initializingHandler) { + handler = initializingHandler; + handler.deps++; + } else { + handler = initializingHandler = { + parent: null, + chunk: null, + value: null, + deps: 1, + errored: false, + }; + } + return (null: any); + } default: // This is an error. Instead of erroring directly, we're going to encode this on // an initialization handler so that we can catch it at the nearest Element. @@ -1406,6 +1763,51 @@ function createFormData( return formData; } +function applyConstructor( + response: Response, + model: Function, + parentObject: Object, + key: string, +): void { + Object.setPrototypeOf(parentObject, model.prototype); + // Delete the property. It was just a placeholder. + return undefined; +} + +function defineLazyGetter( + response: Response, + chunk: SomeChunk, + parentObject: Object, + key: string, +): any { + // We don't immediately initialize it even if it's resolved. + // Instead, we wait for the getter to get accessed. + Object.defineProperty(parentObject, key, { + get: function () { + if (chunk.status === RESOLVED_MODEL) { + // If it was now resolved, then we initialize it. This may then discover + // a new set of lazy references that are then asked for eagerly in case + // we get that deep. + initializeModelChunk(chunk); + } + switch (chunk.status) { + case INITIALIZED: { + return chunk.value; + } + case ERRORED: + throw chunk.reason; + } + // Otherwise, we didn't have enough time to load the object before it was + // accessed or the connection closed. So we just log that it was omitted. + // TODO: We should ideally throw here to indicate a difference. + return OMITTED_PROP_ERROR; + }, + enumerable: true, + configurable: false, + }); + return null; +} + function extractIterator(response: Response, model: Array): Iterator { // $FlowFixMe[incompatible-use]: This uses raw Symbols because we're extracting from a native array. return model[Symbol.iterator](); @@ -1462,10 +1864,6 @@ function parseModelString( } case '@': { // Promise - if (value.length === 2) { - // Infinite promise that never resolves. - return new Promise(() => {}); - } const id = parseInt(value.slice(2), 16); const chunk = getChunk(response, id); if (enableProfilerTimer && enableComponentPerformanceTrack) { @@ -1586,16 +1984,60 @@ function parseModelString( // BigInt return BigInt(value.slice(2)); } + case 'P': { + if (__DEV__) { + // In DEV mode we allow debug objects to specify themselves as instances of + // another constructor. + const ref = value.slice(2); + return getOutlinedModel( + response, + ref, + parentObject, + key, + applyConstructor, + ); + } + //Fallthrough + } case 'E': { if (__DEV__) { // In DEV mode we allow indirect eval to produce functions for logging. // This should not compile to eval() because then it has local scope access. + const code = value.slice(2); try { // eslint-disable-next-line no-eval - return (0, eval)(value.slice(2)); + return (0, eval)(code); } catch (x) { // We currently use this to express functions so we fail parsing it, // let's just return a blank function as a place holder. + if (code.startsWith('(async function')) { + const idx = code.indexOf('(', 15); + if (idx !== -1) { + const name = code.slice(15, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)( + '({' + JSON.stringify(name) + ':async function(){}})', + )[name]; + } + } else if (code.startsWith('(function')) { + const idx = code.indexOf('(', 9); + if (idx !== -1) { + const name = code.slice(9, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)( + '({' + JSON.stringify(name) + ':function(){}})', + )[name]; + } + } else if (code.startsWith('(class')) { + const idx = code.indexOf('{', 6); + if (idx !== -1) { + const name = code.slice(6, idx).trim(); + // eslint-disable-next-line no-eval + return (0, eval)('({' + JSON.stringify(name) + ':class{}})')[ + name + ]; + } + } return function () {}; } } @@ -1603,17 +2045,44 @@ function parseModelString( } case 'Y': { if (__DEV__) { + if (value.length > 2) { + const debugChannel = response._debugChannel; + if (debugChannel) { + if (value[2] === '@') { + // This is a deferred Promise. + const ref = value.slice(3); // We assume this doesn't have a path just id. + const id = parseInt(ref, 16); + if (!response._chunks.has(id)) { + // We haven't seen this id before. Query the server to start sending it. + debugChannel('P:' + ref); + } + // Start waiting. This now creates a pending chunk if it doesn't already exist. + // This is the actual Promise we're waiting for. + return getChunk(response, id); + } + const ref = value.slice(2); // We assume this doesn't have a path just id. + const id = parseInt(ref, 16); + if (!response._chunks.has(id)) { + // We haven't seen this id before. Query the server to start sending it. + debugChannel('Q:' + ref); + } + // Start waiting. This now creates a pending chunk if it doesn't already exist. + const chunk = getChunk(response, id); + if (chunk.status === INITIALIZED) { + // We already loaded this before. We can just use the real value. + return chunk.value; + } + return defineLazyGetter(response, chunk, parentObject, key); + } + } + // In DEV mode we encode omitted objects in logs as a getter that throws // so that when you try to access it on the client, you know why that // happened. Object.defineProperty(parentObject, key, { get: function () { // TODO: We should ideally throw here to indicate a difference. - return ( - 'This object has been omitted by React in the console log ' + - 'to avoid sending too much data from the server. Try logging smaller ' + - 'or more specific objects.' - ); + return OMITTED_PROP_ERROR; }, enumerable: true, configurable: false, @@ -1670,9 +2139,10 @@ function ResponseInstance( encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, - findSourceMapURL: void | FindSourceMapURLCallback, - replayConsole: boolean, - environmentName: void | string, + findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only + replayConsole: boolean, // DEV-only + environmentName: void | string, // DEV-only + debugChannel: void | DebugChannelCallback, // DEV-only ) { const chunks: Map> = new Map(); this._bundlerConfig = bundlerConfig; @@ -1694,8 +2164,14 @@ function ResponseInstance( this._tempRefs = temporaryReferences; if (enableProfilerTimer && enableComponentPerformanceTrack) { this._timeOrigin = 0; + this._pendingInitialRender = null; } if (__DEV__) { + this._pendingChunks = 0; + this._weakResponse = { + weak: new WeakRef(this), + response: this, + }; // TODO: The Flight Client can be used in a Client Environment too and we should really support // getting the owner there as well, but currently the owner of ReactComponentInfo is typed as only // supporting other ReactComponentInfo as owners (and not Fiber or Fizz's ComponentStackNode). @@ -1727,9 +2203,27 @@ function ResponseInstance( ); } this._debugFindSourceMapURL = findSourceMapURL; + this._debugChannel = debugChannel; this._replayConsole = replayConsole; this._rootEnvironmentName = rootEnv; + if (debugChannel) { + if (debugChannelRegistry === null) { + // We can't safely clean things up later, so we immediately close the debug channel. + debugChannel(''); + this._debugChannel = undefined; + } else { + debugChannelRegistry.register(this, debugChannel); + } + } } + if (enableProfilerTimer && enableComponentPerformanceTrack) { + // Since we don't know when recording of profiles will start and stop, we have to + // mark the order over and over again. + if (replayConsole) { + markAllTracksInOrder(); + } + } + // Don't inline this call because it causes closure to outline the call above. this._fromJSON = createFromJSONCallback(this); } @@ -1742,25 +2236,46 @@ export function createResponse( encodeFormAction: void | EncodeFormActionCallback, nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, - findSourceMapURL: void | FindSourceMapURLCallback, - replayConsole: boolean, - environmentName: void | string, -): Response { - // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors - return new ResponseInstance( - bundlerConfig, - serverReferenceConfig, - moduleLoading, - callServer, - encodeFormAction, - nonce, - temporaryReferences, - findSourceMapURL, - replayConsole, - environmentName, + findSourceMapURL: void | FindSourceMapURLCallback, // DEV-only + replayConsole: boolean, // DEV-only + environmentName: void | string, // DEV-only + debugChannel: void | DebugChannelCallback, // DEV-only +): WeakResponse { + return getWeakResponse( + // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors + new ResponseInstance( + bundlerConfig, + serverReferenceConfig, + moduleLoading, + callServer, + encodeFormAction, + nonce, + temporaryReferences, + findSourceMapURL, + replayConsole, + environmentName, + debugChannel, + ), ); } +function resolveDebugHalt(response: Response, id: number): void { + const chunks = response._chunks; + let chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, (chunk = createPendingChunk(response))); + } else { + } + if (chunk.status !== PENDING && chunk.status !== BLOCKED) { + return; + } + releasePendingChunk(response, chunk); + const haltedChunk: HaltedChunk = (chunk: any); + haltedChunk.status = HALTED; + haltedChunk.value = null; + haltedChunk.reason = null; +} + function resolveModel( response: Response, id: number, @@ -1771,7 +2286,7 @@ function resolveModel( if (!chunk) { chunks.set(id, createResolvedModelChunk(response, model)); } else { - resolveModelChunk(chunk, model); + resolveModelChunk(response, chunk, model); } } @@ -1786,6 +2301,9 @@ function resolveText(response: Response, id: number, text: string): void { controller.enqueueValue(text); return; } + if (chunk) { + releasePendingChunk(response, chunk); + } chunks.set(id, createInitializedTextChunk(response, text)); } @@ -1804,6 +2322,9 @@ function resolveBuffer( controller.enqueueValue(buffer); return; } + if (chunk) { + releasePendingChunk(response, chunk); + } chunks.set(id, createInitializedBufferChunk(response, buffer)); } @@ -1841,14 +2362,15 @@ function resolveModule( blockedChunk = createBlockedChunk(response); chunks.set(id, blockedChunk); } else { + releasePendingChunk(response, chunk); // This can't actually happen because we don't have any forward // references to modules. blockedChunk = (chunk: any); blockedChunk.status = BLOCKED; } promise.then( - () => resolveModuleChunk(blockedChunk, clientReference), - error => triggerErrorOnChunk(blockedChunk, error), + () => resolveModuleChunk(response, blockedChunk, clientReference), + error => triggerErrorOnChunk(response, blockedChunk, error), ); } else { if (!chunk) { @@ -1856,7 +2378,7 @@ function resolveModule( } else { // This can't actually happen because we don't have any forward // references to modules. - resolveModuleChunk(chunk, clientReference); + resolveModuleChunk(response, chunk, clientReference); } } } @@ -1877,6 +2399,7 @@ function resolveStream>( // We already resolved. We didn't expect to see this. return; } + releasePendingChunk(response, chunk); const resolveListeners = chunk.value; const resolvedChunk: InitializedStreamChunk = (chunk: any); resolvedChunk.status = INITIALIZED; @@ -1945,7 +2468,7 @@ function startReadableStream( // to synchronous emitting. previousBlockedChunk = null; } - resolveModelChunk(chunk, json); + resolveModelChunk(response, chunk, json); }); } }, @@ -2033,7 +2556,12 @@ function startAsyncIterable( false, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, false); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + false, + ); } nextWriteIndex++; }, @@ -2046,12 +2574,18 @@ function startAsyncIterable( true, ); } else { - resolveIteratorResultChunk(buffer[nextWriteIndex], value, true); + resolveIteratorResultChunk( + response, + buffer[nextWriteIndex], + value, + true, + ); } nextWriteIndex++; while (nextWriteIndex < buffer.length) { // In generators, any extra reads from the iterator have the value undefined. resolveIteratorResultChunk( + response, buffer[nextWriteIndex++], '"$undefined"', true, @@ -2065,36 +2599,37 @@ function startAsyncIterable( createPendingChunk>(response); } while (nextWriteIndex < buffer.length) { - triggerErrorOnChunk(buffer[nextWriteIndex++], error); + triggerErrorOnChunk(response, buffer[nextWriteIndex++], error); } }, }; - const iterable: $AsyncIterable = { - [ASYNC_ITERATOR](): $AsyncIterator { - let nextReadIndex = 0; - return createIterator(arg => { - if (arg !== undefined) { - throw new Error( - 'Values cannot be passed to next() of AsyncIterables passed to Client Components.', + + const iterable: $AsyncIterable = ({}: any); + // $FlowFixMe[cannot-write] + iterable[ASYNC_ITERATOR] = (): $AsyncIterator => { + let nextReadIndex = 0; + return createIterator(arg => { + if (arg !== undefined) { + throw new Error( + 'Values cannot be passed to next() of AsyncIterables passed to Client Components.', + ); + } + if (nextReadIndex === buffer.length) { + if (closed) { + // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors + return new ReactPromise( + INITIALIZED, + {done: true, value: undefined}, + null, ); } - if (nextReadIndex === buffer.length) { - if (closed) { - // $FlowFixMe[invalid-constructor] Flow doesn't support functions as constructors - return new ReactPromise( - INITIALIZED, - {done: true, value: undefined}, - null, - response, - ); - } - buffer[nextReadIndex] = - createPendingChunk>(response); - } - return buffer[nextReadIndex++]; - }); - }, + buffer[nextReadIndex] = + createPendingChunk>(response); + } + return buffer[nextReadIndex++]; + }); }; + // TODO: If it's a single shot iterator we can optimize memory by cleaning up the buffer after // reading through the end, but currently we favor code size over this optimization. resolveStream( @@ -2201,7 +2736,7 @@ function resolvePostponeProd(response: Response, id: number): void { if (!chunk) { chunks.set(id, createErrorChunk(response, postponeInstance)); } else { - triggerErrorOnChunk(chunk, postponeInstance); + triggerErrorOnChunk(response, chunk, postponeInstance); } } @@ -2240,7 +2775,7 @@ function resolvePostponeDev( if (!chunk) { chunks.set(id, createErrorChunk(response, postponeInstance)); } else { - triggerErrorOnChunk(chunk, postponeInstance); + triggerErrorOnChunk(response, chunk, postponeInstance); } } @@ -2494,7 +3029,6 @@ function getRootTask( function initializeFakeTask( response: Response, debugInfo: ReactComponentInfo | ReactAsyncInfo | ReactIOInfo, - childEnvironmentName: string, ): null | ConsoleTask { if (!supportsCreateTask) { return null; @@ -2504,6 +3038,10 @@ function initializeFakeTask( // If it's null, we can't initialize a task. return null; } + const cachedEntry = debugInfo.debugTask; + if (cachedEntry !== undefined) { + return cachedEntry; + } // Workaround for a bug where Chrome Performance tracking uses the enclosing line/column // instead of the callsite. For ReactAsyncInfo/ReactIOInfo, the only thing we're going @@ -2516,47 +3054,35 @@ function initializeFakeTask( const stack = debugInfo.stack; const env: string = debugInfo.env == null ? response._rootEnvironmentName : debugInfo.env; - if (env !== childEnvironmentName) { + const ownerEnv: string = + debugInfo.owner == null || debugInfo.owner.env == null + ? response._rootEnvironmentName + : debugInfo.owner.env; + const ownerTask = + debugInfo.owner == null + ? null + : initializeFakeTask(response, debugInfo.owner); + const taskName = // This is the boundary between two environments so we'll annotate the task name. - // That is unusual so we don't cache it. - const ownerTask = - debugInfo.owner == null - ? null - : initializeFakeTask(response, debugInfo.owner, env); - return buildFakeTask( - response, - ownerTask, - stack, - '"use ' + childEnvironmentName.toLowerCase() + '"', - env, - useEnclosingLine, - ); - } else { - const cachedEntry = debugInfo.debugTask; - if (cachedEntry !== undefined) { - return cachedEntry; - } - const ownerTask = - debugInfo.owner == null - ? null - : initializeFakeTask(response, debugInfo.owner, env); - // Some unfortunate pattern matching to refine the type. - const taskName = - debugInfo.key !== undefined + // We assume that the stack frame of the entry into the new environment was done + // from the old environment. So we use the owner's environment as the current. + env !== ownerEnv + ? '"use ' + env.toLowerCase() + '"' + : // Some unfortunate pattern matching to refine the type. + debugInfo.key !== undefined ? getServerComponentTaskName(((debugInfo: any): ReactComponentInfo)) : debugInfo.name !== undefined ? getIOInfoTaskName(((debugInfo: any): ReactIOInfo)) : getAsyncInfoTaskName(((debugInfo: any): ReactAsyncInfo)); - // $FlowFixMe[cannot-write]: We consider this part of initialization. - return (debugInfo.debugTask = buildFakeTask( - response, - ownerTask, - stack, - taskName, - env, - useEnclosingLine, - )); - } + // $FlowFixMe[cannot-write]: We consider this part of initialization. + return (debugInfo.debugTask = buildFakeTask( + response, + ownerTask, + stack, + taskName, + ownerEnv, + useEnclosingLine, + )); } function buildFakeTask( @@ -2588,7 +3114,7 @@ function buildFakeTask( } const createFakeJSXCallStack = { - 'react-stack-bottom-frame': function ( + react_stack_bottom_frame: function ( response: Response, stack: ReactStackTrace, environmentName: string, @@ -2610,7 +3136,7 @@ const createFakeJSXCallStackInDEV: ( environmentName: string, ) => Error = __DEV__ ? // We use this technique to trick minifiers to preserve the function name. - (createFakeJSXCallStack['react-stack-bottom-frame'].bind( + (createFakeJSXCallStack.react_stack_bottom_frame.bind( createFakeJSXCallStack, ): any) : (null: any); @@ -2636,9 +3162,15 @@ function initializeFakeStack( // $FlowFixMe[cannot-write] debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env); } - if (debugInfo.owner != null) { + const owner = debugInfo.owner; + if (owner != null) { // Initialize any owners not yet initialized. - initializeFakeStack(response, debugInfo.owner); + initializeFakeStack(response, owner); + if (owner.debugLocation === undefined && debugInfo.debugStack != null) { + // If we are the child of this owner, then the owner should be the bottom frame + // our stack. We can use it as the implied location of the owner. + owner.debugLocation = debugInfo.debugStack; + } } } @@ -2658,27 +3190,30 @@ function resolveDebugInfo( 'resolveDebugInfo should never be called in production mode. This is a bug in React.', ); } - // We eagerly initialize the fake task because this resolving happens outside any - // render phase so we're not inside a user space stack at this point. If we waited - // to initialize it when we need it, we might be inside user code. - const env = - debugInfo.env === undefined ? response._rootEnvironmentName : debugInfo.env; if (debugInfo.stack !== undefined) { const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = // $FlowFixMe[incompatible-type] debugInfo; - initializeFakeTask(response, componentInfoOrAsyncInfo, env); + // We eagerly initialize the fake task because this resolving happens outside any + // render phase so we're not inside a user space stack at this point. If we waited + // to initialize it when we need it, we might be inside user code. + initializeFakeTask(response, componentInfoOrAsyncInfo); } - if (debugInfo.owner === null && response._debugRootOwner != null) { + if (debugInfo.owner == null && response._debugRootOwner != null) { const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = // $FlowFixMe: By narrowing `owner` to `null`, we narrowed `debugInfo` to `ReactComponentInfo` debugInfo; // $FlowFixMe[cannot-write] componentInfoOrAsyncInfo.owner = response._debugRootOwner; + // We clear the parsed stack frames to indicate that it needs to be re-parsed from debugStack. + // $FlowFixMe[cannot-write] + componentInfoOrAsyncInfo.stack = null; // We override the stack if we override the owner since the stack where the root JSX // was created on the server isn't very useful but where the request was made is. // $FlowFixMe[cannot-write] componentInfoOrAsyncInfo.debugStack = response._debugRootStack; + // $FlowFixMe[cannot-write] + componentInfoOrAsyncInfo.debugTask = response._debugRootTask; } else if (debugInfo.stack !== undefined) { const componentInfoOrAsyncInfo: ReactComponentInfo | ReactAsyncInfo = // $FlowFixMe[incompatible-type] @@ -2715,7 +3250,7 @@ function getCurrentStackInDEV(): string { } const replayConsoleWithCallStack = { - 'react-stack-bottom-frame': function ( + react_stack_bottom_frame: function ( response: Response, methodName: string, stackTrace: ReactStackTrace, @@ -2738,7 +3273,7 @@ const replayConsoleWithCallStack = { bindToConsole(methodName, args, env), ); if (owner != null) { - const task = initializeFakeTask(response, owner, env); + const task = initializeFakeTask(response, owner); initializeFakeStack(response, owner); if (task !== null) { task.run(callStack); @@ -2767,7 +3302,7 @@ const replayConsoleWithCallStackInDEV: ( args: Array, ) => void = __DEV__ ? // We use this technique to trick minifiers to preserve the function name. - (replayConsoleWithCallStack['react-stack-bottom-frame'].bind( + (replayConsoleWithCallStack.react_stack_bottom_frame.bind( replayConsoleWithCallStack, ): any) : (null: any); @@ -2812,10 +3347,8 @@ function resolveConsoleEntry( } function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void { - const env = - ioInfo.env === undefined ? response._rootEnvironmentName : ioInfo.env; if (ioInfo.stack !== undefined) { - initializeFakeTask(response, ioInfo, env); + initializeFakeTask(response, ioInfo); initializeFakeStack(response, ioInfo); } // Adjust the time to the current environment's time space. @@ -2824,7 +3357,31 @@ function initializeIOInfo(response: Response, ioInfo: ReactIOInfo): void { // $FlowFixMe[cannot-write] ioInfo.end += response._timeOrigin; - logIOInfo(ioInfo, response._rootEnvironmentName); + if (response._replayConsole) { + const env = response._rootEnvironmentName; + const promise = ioInfo.value; + if (promise) { + const thenable: Thenable = (promise: any); + switch (thenable.status) { + case INITIALIZED: + logIOInfo(ioInfo, env, thenable.value); + break; + case ERRORED: + logIOInfoErrored(ioInfo, env, thenable.reason); + break; + default: + // If we haven't resolved the Promise yet, wait to log until have so we can include + // its data in the log. + promise.then( + logIOInfo.bind(null, ioInfo, env), + logIOInfoErrored.bind(null, ioInfo, env), + ); + break; + } + } else { + logIOInfo(ioInfo, env, undefined); + } + } } function resolveIOInfo( @@ -2839,7 +3396,7 @@ function resolveIOInfo( chunks.set(id, chunk); initializeModelChunk(chunk); } else { - resolveModelChunk(chunk, model); + resolveModelChunk(response, chunk, model); if (chunk.status === RESOLVED_MODEL) { initializeModelChunk(chunk); } @@ -2910,6 +3467,46 @@ function resolveTypedArray( resolveBuffer(response, id, view); } +function logComponentInfo( + response: Response, + root: SomeChunk, + componentInfo: ReactComponentInfo, + trackIdx: number, + startTime: number, + componentEndTime: number, + childrenEndTime: number, + isLastComponent: boolean, +): void { + // $FlowFixMe: Refined. + if ( + isLastComponent && + root.status === ERRORED && + root.reason !== response._closedReason + ) { + // If this is the last component to render before this chunk rejected, then conceptually + // this component errored. If this was a cancellation then it wasn't this component that + // errored. + logComponentErrored( + componentInfo, + trackIdx, + startTime, + componentEndTime, + childrenEndTime, + response._rootEnvironmentName, + root.reason, + ); + } else { + logComponentRender( + componentInfo, + trackIdx, + startTime, + componentEndTime, + childrenEndTime, + response._rootEnvironmentName, + ); + } +} + function flushComponentPerformance( response: Response, root: SomeChunk, @@ -2954,32 +3551,25 @@ function flushComponentPerformance( return previousResult; } const children = root._children; - if (root.status === RESOLVED_MODEL) { - // If the model is not initialized by now, do that now so we can find its - // children. This part is a little sketchy since it significantly changes - // the performance characteristics of the app by profiling. - initializeModelChunk(root); - } // First find the start time of the first component to know if it was running // in parallel with the previous. const debugInfo = __DEV__ && root._debugInfo; if (debugInfo) { - for (let i = 1; i < debugInfo.length; i++) { + let startTime = 0; + for (let i = 0; i < debugInfo.length; i++) { const info = debugInfo[i]; + if (typeof info.time === 'number') { + startTime = info.time; + } if (typeof info.name === 'string') { - // $FlowFixMe: Refined. - const startTimeInfo = debugInfo[i - 1]; - if (typeof startTimeInfo.time === 'number') { - const startTime = startTimeInfo.time; - if (startTime < trackTime) { - // The start time of this component is before the end time of the previous - // component on this track so we need to bump the next one to a parallel track. - trackIdx++; - } - trackTime = startTime; - break; + if (startTime < trackTime) { + // The start time of this component is before the end time of the previous + // component on this track so we need to bump the next one to a parallel track. + trackIdx++; } + trackTime = startTime; + break; } } for (let i = debugInfo.length - 1; i >= 0; i--) { @@ -2987,6 +3577,7 @@ function flushComponentPerformance( if (typeof info.time === 'number') { if (info.time > parentEndTime) { parentEndTime = info.time; + break; // We assume the highest number is at the end. } } } @@ -3014,91 +3605,172 @@ function flushComponentPerformance( } childTrackIdx = childResult.track; const childEndTime = childResult.endTime; - childTrackTime = childEndTime; + if (childEndTime > childTrackTime) { + childTrackTime = childEndTime; + } if (childEndTime > childrenEndTime) { childrenEndTime = childEndTime; } } if (debugInfo) { - let endTime = 0; + // Write debug info in reverse order (just like stack traces). + let componentEndTime = 0; let isLastComponent = true; + let endTime = -1; + let endTimeIdx = -1; for (let i = debugInfo.length - 1; i >= 0; i--) { const info = debugInfo[i]; - if (typeof info.time === 'number') { - if (info.time > childrenEndTime) { - childrenEndTime = info.time; - } - if (endTime === 0) { - // Last timestamp is the end of the last component. - endTime = info.time; - } + if (typeof info.time !== 'number') { + continue; } - if (typeof info.name === 'string' && i > 0) { - // $FlowFixMe: Refined. - const componentInfo: ReactComponentInfo = info; - const startTimeInfo = debugInfo[i - 1]; - if (typeof startTimeInfo.time === 'number') { - const startTime = startTimeInfo.time; - if ( - isLastComponent && - root.status === ERRORED && - root.reason !== response._closedReason - ) { - // If this is the last component to render before this chunk rejected, then conceptually - // this component errored. If this was a cancellation then it wasn't this component that - // errored. - logComponentErrored( + if (componentEndTime === 0) { + // Last timestamp is the end of the last component. + componentEndTime = info.time; + } + const time = info.time; + if (endTimeIdx > -1) { + // Now that we know the start and end time, we can emit the entries between. + for (let j = endTimeIdx - 1; j > i; j--) { + const candidateInfo = debugInfo[j]; + if (typeof candidateInfo.name === 'string') { + if (componentEndTime > childrenEndTime) { + childrenEndTime = componentEndTime; + } + // $FlowFixMe: Refined. + const componentInfo: ReactComponentInfo = candidateInfo; + logComponentInfo( + response, + root, componentInfo, trackIdx, - startTime, - endTime, + time, + componentEndTime, childrenEndTime, - response._rootEnvironmentName, - root.reason, + isLastComponent, ); - } else { - logComponentRender( + componentEndTime = time; // The end time of previous component is the start time of the next. + // Track the root most component of the result for deduping logging. + result.component = componentInfo; + isLastComponent = false; + } else if (candidateInfo.awaited) { + if (endTime > childrenEndTime) { + childrenEndTime = endTime; + } + // $FlowFixMe: Refined. + const asyncInfo: ReactAsyncInfo = candidateInfo; + const env = response._rootEnvironmentName; + const promise = asyncInfo.awaited.value; + if (promise) { + const thenable: Thenable = (promise: any); + switch (thenable.status) { + case INITIALIZED: + logComponentAwait( + asyncInfo, + trackIdx, + time, + endTime, + env, + thenable.value, + ); + break; + case ERRORED: + logComponentAwaitErrored( + asyncInfo, + trackIdx, + time, + endTime, + env, + thenable.reason, + ); + break; + default: + // We assume that we should have received the data by now since this is logged at the + // end of the response stream. This is more sensitive to ordering so we don't wait + // to log it. + logComponentAwait( + asyncInfo, + trackIdx, + time, + endTime, + env, + undefined, + ); + break; + } + } else { + logComponentAwait( + asyncInfo, + trackIdx, + time, + endTime, + env, + undefined, + ); + } + } + } + } else { + // Anything between the end and now was aborted if it has no end time. + // Either because the client stream was aborted reading it or the server stream aborted. + endTime = time; // If we don't find anything else the endTime is the start time. + for (let j = debugInfo.length - 1; j > i; j--) { + const candidateInfo = debugInfo[j]; + if (typeof candidateInfo.name === 'string') { + if (componentEndTime > childrenEndTime) { + childrenEndTime = componentEndTime; + } + // $FlowFixMe: Refined. + const componentInfo: ReactComponentInfo = candidateInfo; + const env = response._rootEnvironmentName; + logComponentAborted( componentInfo, trackIdx, - startTime, - endTime, + time, + componentEndTime, childrenEndTime, - response._rootEnvironmentName, + env, ); + componentEndTime = time; // The end time of previous component is the start time of the next. + // Track the root most component of the result for deduping logging. + result.component = componentInfo; + isLastComponent = false; + } else if (candidateInfo.awaited) { + // If we don't have an end time for an await, that means we aborted. + const asyncInfo: ReactAsyncInfo = candidateInfo; + const env = response._rootEnvironmentName; + if (asyncInfo.awaited.end > endTime) { + endTime = asyncInfo.awaited.end; // Take the end time of the I/O as the await end. + } + if (endTime > childrenEndTime) { + childrenEndTime = endTime; + } + logComponentAwaitAborted(asyncInfo, trackIdx, time, endTime, env); } - // Track the root most component of the result for deduping logging. - result.component = componentInfo; - // Set the end time of the previous component to the start of the previous. - endTime = startTime; - } - isLastComponent = false; - } else if (info.awaited && i > 0 && i < debugInfo.length - 2) { - // $FlowFixMe: Refined. - const asyncInfo: ReactAsyncInfo = info; - const startTimeInfo = debugInfo[i - 1]; - const endTimeInfo = debugInfo[i + 1]; - if ( - typeof startTimeInfo.time === 'number' && - typeof endTimeInfo.time === 'number' - ) { - const awaitStartTime = startTimeInfo.time; - const awaitEndTime = endTimeInfo.time; - logComponentAwait( - asyncInfo, - trackIdx, - awaitStartTime, - awaitEndTime, - response._rootEnvironmentName, - ); } } + endTime = time; // The end time of the next entry is this time. + endTimeIdx = i; } } result.endTime = childrenEndTime; return result; } +function flushInitialRenderPerformance(response: Response): void { + if ( + enableProfilerTimer && + enableComponentPerformanceTrack && + response._replayConsole + ) { + const rootChunk = getChunk(response, 0); + if (isArray(rootChunk._children)) { + markAllTracksInOrder(); + flushComponentPerformance(response, rootChunk, 0, -Infinity, -Infinity); + } + } +} + function processFullBinaryRow( response: Response, id: number, @@ -3193,7 +3865,7 @@ function processFullStringRow( if (!chunk) { chunks.set(id, createErrorChunk(response, errorWithDigest)); } else { - triggerErrorOnChunk(chunk, errorWithDigest); + triggerErrorOnChunk(response, chunk, errorWithDigest); } return; } @@ -3310,6 +3982,10 @@ function processFullStringRow( } // Fallthrough default: /* """ "{" "[" "t" "f" "n" "0" - "9" */ { + if (__DEV__ && row === '') { + resolveDebugHalt(response, id); + return; + } // We assume anything else is JSON. resolveModel(response, id, row); return; @@ -3318,9 +3994,14 @@ function processFullStringRow( } export function processBinaryChunk( - response: Response, + weakResponse: WeakResponse, chunk: Uint8Array, ): void { + if (hasGCedResponse(weakResponse)) { + // Ignore more chunks if we've already GC:ed all listeners. + return; + } + const response = unwrapWeakResponse(weakResponse); let i = 0; let rowState = response._rowState; let rowID = response._rowID; @@ -3437,7 +4118,15 @@ export function processBinaryChunk( response._rowLength = rowLength; } -export function processStringChunk(response: Response, chunk: string): void { +export function processStringChunk( + weakResponse: WeakResponse, + chunk: string, +): void { + if (hasGCedResponse(weakResponse)) { + // Ignore more chunks if we've already GC:ed all listeners. + return; + } + const response = unwrapWeakResponse(weakResponse); // This is a fork of processBinaryChunk that takes a string as input. // This can't be just any binary chunk coverted to a string. It needs to be // in the same offsets given from the Flight Server. E.g. if it's shifted by @@ -3599,12 +4288,12 @@ function createFromJSONCallback(response: Response) { }; } -export function close(response: Response): void { +export function close(weakResponse: WeakResponse): void { // In case there are any remaining unresolved chunks, they won't // be resolved now. So we need to issue an error to those. // Ideally we should be able to early bail out if we kept a // ref count of pending chunks. - reportGlobalError(response, new Error('Connection closed.')); + reportGlobalError(weakResponse, new Error('Connection closed.')); } function getCurrentOwnerInDEV(): null | ReactComponentInfo { diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index faa5cf9650d9c..8022a5ad7768f 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -17,10 +17,18 @@ import type { import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; +import { + addValueToProperties, + addObjectToProperties, +} from 'shared/ReactPerformanceTrackProperties'; + const supportsUserTiming = enableProfilerTimer && typeof console !== 'undefined' && - typeof console.timeStamp === 'function'; + typeof console.timeStamp === 'function' && + typeof performance !== 'undefined' && + // $FlowFixMe[method-unbinding] + typeof performance.measure === 'function'; const IO_TRACK = 'Server Requests ⚛'; const COMPONENTS_TRACK = 'Server Components ⚛'; @@ -93,17 +101,27 @@ export function logComponentRender( isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; const debugTask = componentInfo.debugTask; if (__DEV__ && debugTask) { + const properties: Array<[string, string]> = []; + if (componentInfo.key != null) { + addValueToProperties('key', componentInfo.key, properties, 0, ''); + } + if (componentInfo.props != null) { + addObjectToProperties(componentInfo.props, properties, 0, ''); + } debugTask.run( // $FlowFixMe[method-unbinding] - console.timeStamp.bind( - console, - entryName, - startTime < 0 ? 0 : startTime, - childrenEndTime, - trackNames[trackIdx], - COMPONENTS_TRACK, - color, - ), + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: childrenEndTime, + detail: { + devtools: { + color: color, + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + }, + }, + }), ); } else { console.timeStamp( @@ -118,6 +136,59 @@ export function logComponentRender( } } +export function logComponentAborted( + componentInfo: ReactComponentInfo, + trackIdx: number, + startTime: number, + endTime: number, + childrenEndTime: number, + rootEnv: string, +): void { + if (supportsUserTiming) { + const env = componentInfo.env; + const name = componentInfo.name; + const isPrimaryEnv = env === rootEnv; + const entryName = + isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + if (__DEV__) { + const properties = [ + [ + 'Aborted', + 'The stream was aborted before this Component finished rendering.', + ], + ]; + if (componentInfo.key != null) { + addValueToProperties('key', componentInfo.key, properties, 0, ''); + } + if (componentInfo.props != null) { + addObjectToProperties(componentInfo.props, properties, 0, ''); + } + performance.measure(entryName, { + start: startTime < 0 ? 0 : startTime, + end: childrenEndTime, + detail: { + devtools: { + color: 'warning', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + tooltipText: entryName + ' Aborted', + properties, + }, + }, + }); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + childrenEndTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + 'warning', + ); + } + } +} + export function logComponentErrored( componentInfo: ReactComponentInfo, trackIdx: number, @@ -133,12 +204,7 @@ export function logComponentErrored( const isPrimaryEnv = env === rootEnv; const entryName = isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; - if ( - __DEV__ && - typeof performance !== 'undefined' && - // $FlowFixMe[method-unbinding] - typeof performance.measure === 'function' - ) { + if (__DEV__) { const message = typeof error === 'object' && error !== null && @@ -148,6 +214,12 @@ export function logComponentErrored( : // eslint-disable-next-line react-internal/safe-string-coercion String(error); const properties = [['Error', message]]; + if (componentInfo.key != null) { + addValueToProperties('key', componentInfo.key, properties, 0, ''); + } + if (componentInfo.props != null) { + addObjectToProperties(componentInfo.props, properties, 0, ''); + } performance.measure(entryName, { start: startTime < 0 ? 0 : startTime, end: childrenEndTime, @@ -228,7 +300,126 @@ function getIOColor( } } -export function logComponentAwait( +function getIODescription(value: any): string { + if (!__DEV__) { + return ''; + } + try { + switch (typeof value) { + case 'object': + // Test the object for a bunch of common property names that are useful identifiers. + // While we only have the return value here, it should ideally be a name that + // describes the arguments requested. + if (value === null) { + return ''; + } else if (value instanceof Error) { + // eslint-disable-next-line react-internal/safe-string-coercion + return String(value.message); + } else if (typeof value.url === 'string') { + return value.url; + } else if (typeof value.command === 'string') { + return value.command; + } else if ( + typeof value.request === 'object' && + typeof value.request.url === 'string' + ) { + return value.request.url; + } else if ( + typeof value.response === 'object' && + typeof value.response.url === 'string' + ) { + return value.response.url; + } else if ( + typeof value.id === 'string' || + typeof value.id === 'number' || + typeof value.id === 'bigint' + ) { + // eslint-disable-next-line react-internal/safe-string-coercion + return String(value.id); + } else if (typeof value.name === 'string') { + return value.name; + } else { + const str = value.toString(); + if (str.startWith('[object ') || str.length < 5 || str.length > 500) { + // This is probably not a useful description. + return ''; + } + return str; + } + case 'string': + if (value.length < 5 || value.length > 500) { + return ''; + } + return value; + case 'number': + case 'bigint': + // eslint-disable-next-line react-internal/safe-string-coercion + return String(value); + default: + // Not useful descriptors. + return ''; + } + } catch (x) { + return ''; + } +} + +function getIOLongName( + ioInfo: ReactIOInfo, + description: string, + env: void | string, + rootEnv: string, +): string { + const name = ioInfo.name; + const longName = description === '' ? name : name + ' (' + description + ')'; + const isPrimaryEnv = env === rootEnv; + return isPrimaryEnv || env === undefined + ? longName + : longName + ' [' + env + ']'; +} + +function getIOShortName( + ioInfo: ReactIOInfo, + description: string, + env: void | string, + rootEnv: string, +): string { + const name = ioInfo.name; + const isPrimaryEnv = env === rootEnv; + const envSuffix = isPrimaryEnv || env === undefined ? '' : ' [' + env + ']'; + let desc = ''; + const descMaxLength = 30 - name.length - envSuffix.length; + if (descMaxLength > 1) { + const l = description.length; + if (l > 0 && l <= descMaxLength) { + // We can fit the full description + desc = ' (' + description + ')'; + } else if ( + description.startsWith('http://') || + description.startsWith('https://') || + description.startsWith('/') + ) { + // Looks like a URL. Let's see if we can extract something shorter. + // We don't have to do a full parse so let's try something cheaper. + let queryIdx = description.indexOf('?'); + if (queryIdx === -1) { + queryIdx = description.length; + } + if (description.charCodeAt(queryIdx - 1) === 47 /* "/" */) { + // Ends with slash. Look before that. + queryIdx--; + } + const slashIdx = description.lastIndexOf('/', queryIdx - 1); + if (queryIdx - slashIdx < descMaxLength) { + // This may now be either the file name or the host. + desc = ' (' + description.slice(slashIdx + 1, queryIdx) + ')'; + } + } + } + return name + desc + envSuffix; +} + +export function logComponentAwaitAborted( asyncInfo: ReactAsyncInfo, trackIdx: number, startTime: number, @@ -236,26 +427,148 @@ export function logComponentAwait( rootEnv: string, ): void { if (supportsUserTiming && endTime > 0) { - const env = asyncInfo.env; - const name = asyncInfo.awaited.name; - const isPrimaryEnv = env === rootEnv; - const color = getIOColor(name); + const entryName = + 'await ' + getIOShortName(asyncInfo.awaited, '', asyncInfo.env, rootEnv); + const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask; + if (__DEV__ && debugTask) { + const properties = [ + ['Aborted', 'The stream was aborted before this Promise resolved.'], + ]; + const tooltipText = + getIOLongName(asyncInfo.awaited, '', asyncInfo.env, rootEnv) + + ' Aborted'; + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: 'warning', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + 'warning', + ); + } + } +} + +export function logComponentAwaitErrored( + asyncInfo: ReactAsyncInfo, + trackIdx: number, + startTime: number, + endTime: number, + rootEnv: string, + error: mixed, +): void { + if (supportsUserTiming && endTime > 0) { + const description = getIODescription(error); const entryName = 'await ' + - (isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'); - const debugTask = asyncInfo.debugTask; + getIOShortName(asyncInfo.awaited, description, asyncInfo.env, rootEnv); + const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask; if (__DEV__ && debugTask) { + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + const properties = [['Rejected', message]]; + const tooltipText = + getIOLongName(asyncInfo.awaited, description, asyncInfo.env, rootEnv) + + ' Rejected'; debugTask.run( // $FlowFixMe[method-unbinding] - console.timeStamp.bind( - console, - entryName, - startTime < 0 ? 0 : startTime, - endTime, - trackNames[trackIdx], - COMPONENTS_TRACK, - color, - ), + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + trackNames[trackIdx], + COMPONENTS_TRACK, + 'error', + ); + } + } +} + +export function logComponentAwait( + asyncInfo: ReactAsyncInfo, + trackIdx: number, + startTime: number, + endTime: number, + rootEnv: string, + value: mixed, +): void { + if (supportsUserTiming && endTime > 0) { + const description = getIODescription(value); + const name = getIOShortName( + asyncInfo.awaited, + description, + asyncInfo.env, + rootEnv, + ); + const entryName = 'await ' + name; + const color = getIOColor(name); + const debugTask = asyncInfo.debugTask || asyncInfo.awaited.debugTask; + if (__DEV__ && debugTask) { + const properties: Array<[string, string]> = []; + if (typeof value === 'object' && value !== null) { + addObjectToProperties(value, properties, 0, ''); + } else if (value !== undefined) { + addValueToProperties('Resolved', value, properties, 0, ''); + } + const tooltipText = getIOLongName( + asyncInfo.awaited, + description, + asyncInfo.env, + rootEnv, + ); + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: color, + track: trackNames[trackIdx], + trackGroup: COMPONENTS_TRACK, + properties, + tooltipText, + }, + }, + }), ); } else { console.timeStamp( @@ -270,29 +583,96 @@ export function logComponentAwait( } } -export function logIOInfo(ioInfo: ReactIOInfo, rootEnv: string): void { +export function logIOInfoErrored( + ioInfo: ReactIOInfo, + rootEnv: string, + error: mixed, +): void { + const startTime = ioInfo.start; + const endTime = ioInfo.end; + if (supportsUserTiming && endTime >= 0) { + const description = getIODescription(error); + const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); + const debugTask = ioInfo.debugTask; + if (__DEV__ && debugTask) { + const message = + typeof error === 'object' && + error !== null && + typeof error.message === 'string' + ? // eslint-disable-next-line react-internal/safe-string-coercion + String(error.message) + : // eslint-disable-next-line react-internal/safe-string-coercion + String(error); + const properties = [['Rejected', message]]; + const tooltipText = + getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected'; + debugTask.run( + // $FlowFixMe[method-unbinding] + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: 'error', + track: IO_TRACK, + properties, + tooltipText, + }, + }, + }), + ); + } else { + console.timeStamp( + entryName, + startTime < 0 ? 0 : startTime, + endTime, + IO_TRACK, + undefined, + 'error', + ); + } + } +} + +export function logIOInfo( + ioInfo: ReactIOInfo, + rootEnv: string, + value: mixed, +): void { const startTime = ioInfo.start; const endTime = ioInfo.end; if (supportsUserTiming && endTime >= 0) { - const name = ioInfo.name; - const env = ioInfo.env; - const isPrimaryEnv = env === rootEnv; - const entryName = - isPrimaryEnv || env === undefined ? name : name + ' [' + env + ']'; + const description = getIODescription(value); + const entryName = getIOShortName(ioInfo, description, ioInfo.env, rootEnv); + const color = getIOColor(entryName); const debugTask = ioInfo.debugTask; - const color = getIOColor(name); if (__DEV__ && debugTask) { + const properties: Array<[string, string]> = []; + if (typeof value === 'object' && value !== null) { + addObjectToProperties(value, properties, 0, ''); + } else if (value !== undefined) { + addValueToProperties('Resolved', value, properties, 0, ''); + } + const tooltipText = getIOLongName( + ioInfo, + description, + ioInfo.env, + rootEnv, + ); debugTask.run( // $FlowFixMe[method-unbinding] - console.timeStamp.bind( - console, - entryName, - startTime < 0 ? 0 : startTime, - endTime, - IO_TRACK, - undefined, - color, - ), + performance.measure.bind(performance, entryName, { + start: startTime < 0 ? 0 : startTime, + end: endTime, + detail: { + devtools: { + color: color, + track: IO_TRACK, + properties, + tooltipText, + }, + }, + }), ); } else { console.timeStamp( diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 6a0a37b787d34..d22a894b5157c 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -18,13 +18,10 @@ import type { import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; -import {enableRenderableContext} from 'shared/ReactFeatureFlags'; - import { REACT_ELEMENT_TYPE, REACT_LAZY_TYPE, REACT_CONTEXT_TYPE, - REACT_PROVIDER_TYPE, getIteratorFn, ASYNC_ITERATOR, } from 'shared/ReactSymbols'; @@ -699,10 +696,7 @@ export function processReply( return serializeTemporaryReferenceMarker(); } if (__DEV__) { - if ( - (value: any).$$typeof === - (enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE) - ) { + if ((value: any).$$typeof === REACT_CONTEXT_TYPE) { console.error( 'React Context Providers cannot be passed to Server Functions from the Client.%s', describeObjectForErrorMessage(parent, key), @@ -1112,7 +1106,7 @@ function createFakeServerFunction, T>( '\n//# sourceURL=rsc://React/' + encodeURIComponent(environmentName) + '/' + - filename + + encodeURI(filename) + '?s' + // We add an extra s here to distinguish from the fake stack frames fakeServerFunctionIdx++; code += '\n//# sourceMappingURL=' + sourceMap; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index ef9864200588a..6c1b22f1414ed 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -69,7 +69,7 @@ function getErrorForJestMatcher(error) { function normalizeComponentInfo(debugInfo) { if (Array.isArray(debugInfo.stack)) { - const {debugTask, debugStack, ...copy} = debugInfo; + const {debugTask, debugStack, debugLocation, ...copy} = debugInfo; copy.stack = formatV8Stack(debugInfo.stack); if (debugInfo.owner) { copy.owner = normalizeComponentInfo(debugInfo.owner); @@ -92,21 +92,27 @@ function getDebugInfo(obj) { return debugInfo; } -const heldValues = []; -let finalizationCallback; +const finalizationRegistries = []; function FinalizationRegistryMock(callback) { - finalizationCallback = callback; + this._heldValues = []; + this._callback = callback; + finalizationRegistries.push(this); } FinalizationRegistryMock.prototype.register = function (target, heldValue) { - heldValues.push(heldValue); + this._heldValues.push(heldValue); }; global.FinalizationRegistry = FinalizationRegistryMock; function gc() { - for (let i = 0; i < heldValues.length; i++) { - finalizationCallback(heldValues[i]); + for (let i = 0; i < finalizationRegistries.length; i++) { + const registry = finalizationRegistries[i]; + const callback = registry._callback; + const heldValues = registry._heldValues; + for (let j = 0; j < heldValues.length; j++) { + callback(heldValues[j]); + } + heldValues.length = 0; } - heldValues.length = 0; } let act; @@ -320,7 +326,6 @@ describe('ReactFlight', () => { name: 'Greeting', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { firstName: 'Seb', @@ -364,7 +369,6 @@ describe('ReactFlight', () => { name: 'Greeting', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { firstName: 'Seb', @@ -1308,6 +1312,11 @@ describe('ReactFlight', () => { ' at file:///testing.js:42:3', // async anon function (https://github.com/ChromeDevTools/devtools-frontend/blob/831be28facb4e85de5ee8c1acc4d98dfeda7a73b/test/unittests/front_end/panels/console/ErrorStackParser_test.ts#L130C9-L130C41) ' at async file:///testing.js:42:3', + // third-party RSC frame + // Ideally this would be a real frame produced by React not a mocked one. + ' at ThirdParty (rsc://React/ThirdParty/file:///code/%5Broot%2520of%2520the%2520server%5D.js?42:1:1)', + // We'll later filter this out based on line/column in `filterStackFrame`. + ' at ThirdPartyModule (file:///file-with-index-source-map.js:52656:16374)', // host component in parent stack ' at div ()', ...originalStackLines.slice(2), @@ -1356,13 +1365,19 @@ describe('ReactFlight', () => { } return `digest(${String(x)})`; }, - filterStackFrame(filename, functionName) { + filterStackFrame(filename, functionName, lineNumber, columnNumber) { + if (lineNumber === 52656 && columnNumber === 16374) { + return false; + } if (!filename) { // Allow anonymous return functionName === 'div'; } return ( - !filename.startsWith('node:') && !filename.includes('node_modules') + !filename.startsWith('node:') && + !filename.includes('node_modules') && + // sourceURL from an ES module in `/code/[root of the server].js` + filename !== 'file:///code/[root%20of%20the%20server].js' ); }, }); @@ -2812,7 +2827,6 @@ describe('ReactFlight', () => { name: 'ServerComponent', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { transport: expect.arrayContaining([]), @@ -2829,16 +2843,15 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyChildren[0])).toEqual( __DEV__ ? [ - {time: 14}, + {time: 22}, // Clamped to the start { name: 'ThirdPartyComponent', env: 'third-party', key: null, - owner: null, stack: ' in Object. (at **)', props: {}, }, - {time: 15}, + {time: 22}, {time: 23}, // This last one is when the promise resolved into the first party. ] : undefined, @@ -2846,32 +2859,30 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyChildren[1])).toEqual( __DEV__ ? [ - {time: 16}, + {time: 22}, // Clamped to the start { name: 'ThirdPartyLazyComponent', env: 'third-party', key: null, - owner: null, stack: ' in myLazy (at **)\n in lazyInitializer (at **)', props: {}, }, - {time: 17}, + {time: 22}, ] : undefined, ); expect(getDebugInfo(thirdPartyChildren[2])).toEqual( __DEV__ ? [ - {time: 12}, + {time: 22}, { name: 'ThirdPartyFragmentComponent', env: 'third-party', key: '3', - owner: null, stack: ' in Object. (at **)', props: {}, }, - {time: 13}, + {time: 22}, ] : undefined, ); @@ -2941,7 +2952,6 @@ describe('ReactFlight', () => { name: 'ServerComponent', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { transport: expect.arrayContaining([]), @@ -2961,7 +2971,6 @@ describe('ReactFlight', () => { name: 'Keyed', env: 'Server', key: 'keyed', - owner: null, stack: ' in ServerComponent (at **)', props: { children: {}, @@ -2975,16 +2984,15 @@ describe('ReactFlight', () => { expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual( __DEV__ ? [ - {time: 12}, + {time: 19}, // Clamp to the start { name: 'ThirdPartyAsyncIterableComponent', env: 'third-party', key: null, - owner: null, stack: ' in Object. (at **)', props: {}, }, - {time: 13}, + {time: 19}, ] : undefined, ); @@ -3000,6 +3008,64 @@ describe('ReactFlight', () => { ); }); + // @gate !__DEV__ || enableComponentPerformanceTrack + it('preserves debug info for server-to-server through use()', async () => { + function ThirdPartyComponent() { + return 'hi'; + } + + function ServerComponent({transport}) { + // This is a Server Component that receives other Server Components from a third party. + const text = ReactServer.use(ReactNoopFlightClient.read(transport)); + return
{text.toUpperCase()}
; + } + + const thirdPartyTransport = ReactNoopFlightServer.render( + , + { + environmentName: 'third-party', + }, + ); + + const transport = ReactNoopFlightServer.render( + , + ); + + await act(async () => { + const promise = ReactNoopFlightClient.read(transport); + expect(getDebugInfo(promise)).toEqual( + __DEV__ + ? [ + {time: 16}, + { + name: 'ServerComponent', + env: 'Server', + key: null, + stack: ' in Object. (at **)', + props: { + transport: expect.arrayContaining([]), + }, + }, + {time: 16}, + { + name: 'ThirdPartyComponent', + env: 'third-party', + key: null, + stack: ' in Object. (at **)', + props: {}, + }, + {time: 16}, + {time: 17}, + ] + : undefined, + ); + const result = await promise; + ReactNoop.render(result); + }); + + expect(ReactNoop).toMatchRenderedOutput(
HI
); + }); + it('preserves error stacks passed through server-to-server with source maps', async () => { async function ServerComponent({transport}) { // This is a Server Component that receives other Server Components from a third party. @@ -3137,7 +3203,6 @@ describe('ReactFlight', () => { name: 'Component', env: 'A', key: null, - owner: null, stack: ' in Object. (at **)', props: {}, }, @@ -3160,12 +3225,29 @@ describe('ReactFlight', () => { return 'hello'; } + class MyClass { + constructor() { + this.x = 1; + } + method() {} + get y() { + return this.x + 1; + } + get z() { + return this.x + 5; + } + } + Object.defineProperty(MyClass.prototype, 'y', {enumerable: true}); + function ServerComponent() { console.log('hi', { prop: 123, fn: foo, map: new Map([['foo', foo]]), - promise: new Promise(() => {}), + promise: Promise.resolve('yo'), + infinitePromise: new Promise(() => {}), + Class: MyClass, + instance: new MyClass(), }); throw new Error('err'); } @@ -3210,9 +3292,14 @@ describe('ReactFlight', () => { }); ownerStacks = []; + // Let the Promises resolve. + await 0; + await 0; + await 0; + // The error should not actually get logged because we're not awaiting the root // so it's not thrown but the server log also shouldn't be replayed. - await ReactNoopFlightClient.read(transport); + await ReactNoopFlightClient.read(transport, {close: true}); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); @@ -3228,9 +3315,40 @@ describe('ReactFlight', () => { expect(typeof loggedFn2).toBe('function'); expect(loggedFn2).not.toBe(foo); expect(loggedFn2.toString()).toBe(foo.toString()); + expect(loggedFn2).toBe(loggedFn); const promise = mockConsoleLog.mock.calls[0][1].promise; expect(promise).toBeInstanceOf(Promise); + expect(await promise).toBe('yo'); + + const infinitePromise = mockConsoleLog.mock.calls[0][1].infinitePromise; + expect(infinitePromise).toBeInstanceOf(Promise); + let resolved = false; + infinitePromise.then( + () => (resolved = true), + x => { + console.error(x); + resolved = true; + }, + ); + await 0; + await 0; + await 0; + // This should not reject upon aborting the stream. + expect(resolved).toBe(false); + + const Class = mockConsoleLog.mock.calls[0][1].Class; + const instance = mockConsoleLog.mock.calls[0][1].instance; + expect(typeof Class).toBe('function'); + expect(Class.prototype.constructor).toBe(Class); + expect(instance instanceof Class).toBe(true); + expect(Object.getPrototypeOf(instance)).toBe(Class.prototype); + expect(instance.x).toBe(1); + expect(instance.hasOwnProperty('y')).toBe(true); + expect(instance.y).toBe(2); // Enumerable getter was reified + expect(instance.hasOwnProperty('z')).toBe(false); + expect(instance.z).toBe(6); // Not enumerable getter was transferred as part of the toString() of the class + expect(typeof instance.method).toBe('function'); // Methods are included only if they're part of the toString() expect(ownerStacks).toEqual(['\n in App (at **)']); }); @@ -3279,19 +3397,10 @@ describe('ReactFlight', () => { await ReactNoopFlightClient.read(transport); expect(mockConsoleLog).toHaveBeenCalledTimes(1); - // TODO: Support cyclic objects in console encoding. - // expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); - // const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic; - // expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned - // expect(cyclic2.cycle).toBe(cyclic2); - expect(mockConsoleLog.mock.calls[0][0]).toBe( - 'Unknown Value: React could not send it from the server.', - ); - expect(mockConsoleLog.mock.calls[0][1].message).toBe( - 'Converting circular structure to JSON\n' + - " --> starting at object with constructor 'Object'\n" + - " --- property 'cycle' closes the circle", - ); + expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); + const cyclic2 = mockConsoleLog.mock.calls[0][1].cyclic; + expect(cyclic2).not.toBe(cyclic); // Was serialized and therefore cloned + expect(cyclic2.cycle).toBe(cyclic2); }); // @gate !__DEV__ || enableComponentPerformanceTrack @@ -3325,7 +3434,6 @@ describe('ReactFlight', () => { name: 'Greeting', env: 'Server', key: null, - owner: null, stack: ' in Object. (at **)', props: { firstName: 'Seb', @@ -3585,7 +3693,7 @@ describe('ReactFlight', () => { onError(x) { return `digest("${x.message}")`; }, - filterStackFrame(url, functionName) { + filterStackFrame(url, functionName, lineNumber, columnNumber) { return functionName !== 'intermediate'; }, }, diff --git a/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js new file mode 100644 index 0000000000000..e9428c3ba4074 --- /dev/null +++ b/packages/react-client/src/__tests__/ReactFlightDebugChannel-test.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + * @jest-environment node + */ + +'use strict'; + +if (typeof Blob === 'undefined') { + global.Blob = require('buffer').Blob; +} +if (typeof File === 'undefined' || typeof FormData === 'undefined') { + global.File = require('undici').File; + global.FormData = require('undici').FormData; +} + +function formatV8Stack(stack) { + let v8StyleStack = ''; + if (stack) { + for (let i = 0; i < stack.length; i++) { + const [name] = stack[i]; + if (v8StyleStack !== '') { + v8StyleStack += '\n'; + } + v8StyleStack += ' in ' + name + ' (at **)'; + } + } + return v8StyleStack; +} + +function normalizeComponentInfo(debugInfo) { + if (Array.isArray(debugInfo.stack)) { + const {debugTask, debugStack, ...copy} = debugInfo; + copy.stack = formatV8Stack(debugInfo.stack); + if (debugInfo.owner) { + copy.owner = normalizeComponentInfo(debugInfo.owner); + } + return copy; + } else { + return debugInfo; + } +} + +function getDebugInfo(obj) { + const debugInfo = obj._debugInfo; + if (debugInfo) { + const copy = []; + for (let i = 0; i < debugInfo.length; i++) { + copy.push(normalizeComponentInfo(debugInfo[i])); + } + return copy; + } + return debugInfo; +} + +let act; +let React; +let ReactNoop; +let ReactNoopFlightServer; +let ReactNoopFlightClient; + +describe('ReactFlight', () => { + beforeEach(() => { + // Mock performance.now for timing tests + let time = 10; + const now = jest.fn().mockImplementation(() => { + return time++; + }); + Object.defineProperty(performance, 'timeOrigin', { + value: time, + configurable: true, + }); + Object.defineProperty(performance, 'now', { + value: now, + configurable: true, + }); + + jest.resetModules(); + jest.mock('react', () => require('react/react.react-server')); + ReactNoopFlightServer = require('react-noop-renderer/flight-server'); + // This stores the state so we need to preserve it + const flightModules = require('react-noop-renderer/flight-modules'); + jest.resetModules(); + __unmockReact(); + jest.mock('react-noop-renderer/flight-modules', () => flightModules); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + ReactNoopFlightClient = require('react-noop-renderer/flight-client'); + act = require('internal-test-utils').act; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + // @gate __DEV__ && enableComponentPerformanceTrack + it('can render deep but cut off JSX in debug info', async () => { + function createDeepJSX(n) { + if (n <= 0) { + return null; + } + return
{createDeepJSX(n - 1)}
; + } + + function ServerComponent(props) { + return
not using props
; + } + + const debugChannel = {onMessage(message) {}}; + + const transport = ReactNoopFlightServer.render( + { + root: ( + + {createDeepJSX(100) /* deper than objectLimit */} + + ), + }, + {debugChannel}, + ); + + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport, { + debugChannel, + }); + const root = rootModel.root; + const children = getDebugInfo(root)[1].props.children; + expect(children.type).toBe('div'); + expect(children.props.children.type).toBe('div'); + ReactNoop.render(root); + }); + + expect(ReactNoop).toMatchRenderedOutput(
not using props
); + }); +}); diff --git a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webstreams.js b/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webstreams.js deleted file mode 100644 index eb9ad28d46fa3..0000000000000 --- a/packages/react-client/src/forks/ReactFlightClientConfig.dom-node-webstreams.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -export {default as rendererVersion} from 'shared/ReactVersion'; -export const rendererPackageName = 'react-server-dom-webpack'; - -export * from 'react-client/src/ReactFlightClientStreamConfigWeb'; -export * from 'react-client/src/ReactClientConsoleConfigServer'; -export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigBundlerNode'; -export * from 'react-server-dom-webpack/src/client/ReactFlightClientConfigTargetWebpackServer'; -export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM'; -export const usedWithSSR = true; diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 2b93c8f06e2b2..e245adcb1380a 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -14,7 +14,6 @@ let React; let ReactTestRenderer; let ReactDebugTools; let act; -let assertConsoleErrorDev; let useMemoCache; function normalizeSourceLoc(tree) { @@ -34,7 +33,7 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({act} = require('internal-test-utils')); ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; }); @@ -2321,57 +2320,6 @@ describe('ReactHooksInspectionIntegration', () => { }); }); - // @gate !disableDefaultPropsExceptForClasses - it('should support defaultProps and lazy', async () => { - const Suspense = React.Suspense; - - function Foo(props) { - const [value] = React.useState(props.defaultValue.slice(0, 3)); - return
{value}
; - } - Foo.defaultProps = { - defaultValue: 'default', - }; - - async function fakeImport(result) { - return {default: result}; - } - - const LazyFoo = React.lazy(() => fakeImport(Foo)); - - const renderer = ReactTestRenderer.create( - - - , - ); - - await act(async () => await LazyFoo); - assertConsoleErrorDev([ - 'Foo: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', - ]); - - const childFiber = renderer.root._currentFiber(); - const tree = ReactDebugTools.inspectHooksOfFiber(childFiber); - expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(` - [ - { - "debugInfo": null, - "hookSource": { - "columnNumber": 0, - "fileName": "**", - "functionName": "Foo", - "lineNumber": 0, - }, - "id": 0, - "isStateEditable": true, - "name": "State", - "subHooks": [], - "value": "def", - }, - ] - `); - }); - // This test case is based on an open source bug report: // https://github.com/facebookincubator/redux-react-hook/issues/34#issuecomment-466693787 it('should properly advance the current hook for useContext', async () => { diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 581a24edba342..8ce8f436e815d 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "6.1.2", + "version": "6.1.5", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index fa4607d1c69cd..127185f73e430 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "6.1.2", - "version_name": "6.1.2", + "version": "6.1.5", + "version_name": "6.1.5", "minimum_chrome_version": "114", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 59d4a8b1ff996..f930a6d0ac7cf 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "6.1.2", - "version_name": "6.1.2", + "version": "6.1.5", + "version_name": "6.1.5", "minimum_chrome_version": "114", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 3080eadbb003b..24b5b11323f69 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "6.1.2", + "version": "6.1.5", "browser_specific_settings": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index bfa564b73bb91..8d3f1e71c10ff 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "6.1.2", + "version": "6.1.5", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 94246df6485e4..e24734b0ab032 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -5831,13 +5831,21 @@ export function attach( } function getSourceForInstance(instance: DevToolsInstance): Source | null { - const unresolvedSource = instance.source; + let unresolvedSource = instance.source; if (unresolvedSource === null) { // We don't have any source yet. We can try again later in case an owned child mounts later. // TODO: We won't have any information here if the child is filtered. return null; } + if (instance.kind === VIRTUAL_INSTANCE) { + // We might have found one on the virtual instance. + const debugLocation = instance.data.debugLocation; + if (debugLocation != null) { + unresolvedSource = debugLocation; + } + } + // If we have the debug stack (the creation stack of the JSX) for any owned child of this // component, then at the bottom of that stack will be a stack frame that is somewhere within // the component's function body. Typically it would be the callsite of the JSX unless there's diff --git a/packages/react-devtools-shared/src/backend/profilingHooks.js b/packages/react-devtools-shared/src/backend/profilingHooks.js index c84bb88250375..ca58cf655ce53 100644 --- a/packages/react-devtools-shared/src/backend/profilingHooks.js +++ b/packages/react-devtools-shared/src/backend/profilingHooks.js @@ -298,14 +298,16 @@ export function createProfilingHooks({ } function markCommitStarted(lanes: Lanes): void { - if (isProfiling) { - recordReactMeasureStarted('commit', lanes); - - // TODO (timeline) Re-think this approach to "batching"; I don't think it works for Suspense or pre-rendering. - // This issue applies to the User Timing data also. - nextRenderShouldStartNewBatch = true; + if (!isProfiling) { + return; } + recordReactMeasureStarted('commit', lanes); + + // TODO (timeline) Re-think this approach to "batching"; I don't think it works for Suspense or pre-rendering. + // This issue applies to the User Timing data also. + nextRenderShouldStartNewBatch = true; + if (supportsUserTimingV3) { markAndClear(`--commit-start-${lanes}`); @@ -318,54 +320,55 @@ export function createProfilingHooks({ } function markCommitStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('commit'); - recordReactMeasureCompleted('render-idle'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('commit'); + recordReactMeasureCompleted('render-idle'); if (supportsUserTimingV3) { markAndClear('--commit-stop'); } } function markComponentRenderStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'render', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--component-render-start-${componentName}`); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'render', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-render-start-${componentName}`); } } function markComponentRenderStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -374,43 +377,43 @@ export function createProfilingHooks({ } function markComponentLayoutEffectMountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'layout-effect-mount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--component-layout-effect-mount-start-${componentName}`); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'layout-effect-mount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-layout-effect-mount-start-${componentName}`); } } function markComponentLayoutEffectMountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -419,45 +422,43 @@ export function createProfilingHooks({ } function markComponentLayoutEffectUnmountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'layout-effect-unmount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear( - `--component-layout-effect-unmount-start-${componentName}`, - ); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'layout-effect-unmount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-layout-effect-unmount-start-${componentName}`); } } function markComponentLayoutEffectUnmountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -466,43 +467,43 @@ export function createProfilingHooks({ } function markComponentPassiveEffectMountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'passive-effect-mount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--component-passive-effect-mount-start-${componentName}`); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'passive-effect-mount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-passive-effect-mount-start-${componentName}`); } } function markComponentPassiveEffectMountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -511,45 +512,43 @@ export function createProfilingHooks({ } function markComponentPassiveEffectUnmountStarted(fiber: Fiber): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (isProfiling) { - currentReactComponentMeasure = { - componentName, - duration: 0, - timestamp: getRelativeTime(), - type: 'passive-effect-unmount', - warning: null, - }; - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear( - `--component-passive-effect-unmount-start-${componentName}`, - ); - } + // TODO (timeline) Record and cache component stack + currentReactComponentMeasure = { + componentName, + duration: 0, + timestamp: getRelativeTime(), + type: 'passive-effect-unmount', + warning: null, + }; + + if (supportsUserTimingV3) { + markAndClear(`--component-passive-effect-unmount-start-${componentName}`); } } function markComponentPassiveEffectUnmountStopped(): void { - if (isProfiling) { - if (currentReactComponentMeasure) { - if (currentTimelineData) { - currentTimelineData.componentMeasures.push( - currentReactComponentMeasure, - ); - } + if (!isProfiling) { + return; + } - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentReactComponentMeasure.duration = - // $FlowFixMe[incompatible-use] found when upgrading Flow - getRelativeTime() - currentReactComponentMeasure.timestamp; - currentReactComponentMeasure = null; + if (currentReactComponentMeasure) { + if (currentTimelineData) { + currentTimelineData.componentMeasures.push( + currentReactComponentMeasure, + ); } + + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentReactComponentMeasure.duration = + // $FlowFixMe[incompatible-use] found when upgrading Flow + getRelativeTime() - currentReactComponentMeasure.timestamp; + currentReactComponentMeasure = null; } if (supportsUserTimingV3) { @@ -562,37 +561,37 @@ export function createProfilingHooks({ thrownValue: mixed, lanes: Lanes, ): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - const phase = fiber.alternate === null ? 'mount' : 'update'; - - let message = ''; - if ( - thrownValue !== null && - typeof thrownValue === 'object' && - typeof thrownValue.message === 'string' - ) { - message = thrownValue.message; - } else if (typeof thrownValue === 'string') { - message = thrownValue; - } + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (currentTimelineData) { - currentTimelineData.thrownErrors.push({ - componentName, - message, - phase, - timestamp: getRelativeTime(), - type: 'thrown-error', - }); - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + const phase = fiber.alternate === null ? 'mount' : 'update'; - if (supportsUserTimingV3) { - markAndClear(`--error-${componentName}-${phase}-${message}`); - } + let message = ''; + if ( + thrownValue !== null && + typeof thrownValue === 'object' && + typeof thrownValue.message === 'string' + ) { + message = thrownValue.message; + } else if (typeof thrownValue === 'string') { + message = thrownValue; + } + + // TODO (timeline) Record and cache component stack + if (currentTimelineData) { + currentTimelineData.thrownErrors.push({ + componentName, + message, + phase, + timestamp: getRelativeTime(), + type: 'thrown-error', + }); + } + + if (supportsUserTimingV3) { + markAndClear(`--error-${componentName}-${phase}-${message}`); } } @@ -613,44 +612,44 @@ export function createProfilingHooks({ wakeable: Wakeable, lanes: Lanes, ): void { - if (isProfiling || supportsUserTimingV3) { - const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend'; - const id = getWakeableID(wakeable); - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - const phase = fiber.alternate === null ? 'mount' : 'update'; - - // Following the non-standard fn.displayName convention, - // frameworks like Relay may also annotate Promises with a displayName, - // describing what operation/data the thrown Promise is related to. - // When this is available we should pass it along to the Timeline. - const displayName = (wakeable: any).displayName || ''; - - let suspenseEvent: SuspenseEvent | null = null; - if (isProfiling) { - // TODO (timeline) Record and cache component stack - suspenseEvent = { - componentName, - depth: 0, - duration: 0, - id: `${id}`, - phase, - promiseName: displayName, - resolution: 'unresolved', - timestamp: getRelativeTime(), - type: 'suspense', - warning: null, - }; - - if (currentTimelineData) { - currentTimelineData.suspenseEvents.push(suspenseEvent); - } - } + if (!isProfiling) { + return; + } - if (supportsUserTimingV3) { - markAndClear( - `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`, - ); - } + const eventType = wakeableIDs.has(wakeable) ? 'resuspend' : 'suspend'; + const id = getWakeableID(wakeable); + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + const phase = fiber.alternate === null ? 'mount' : 'update'; + + // Following the non-standard fn.displayName convention, + // frameworks like Relay may also annotate Promises with a displayName, + // describing what operation/data the thrown Promise is related to. + // When this is available we should pass it along to the Timeline. + const displayName = (wakeable: any).displayName || ''; + + let suspenseEvent: SuspenseEvent | null = null; + // TODO (timeline) Record and cache component stack + suspenseEvent = { + componentName, + depth: 0, + duration: 0, + id: `${id}`, + phase, + promiseName: displayName, + resolution: 'unresolved', + timestamp: getRelativeTime(), + type: 'suspense', + warning: null, + }; + + if (currentTimelineData) { + currentTimelineData.suspenseEvents.push(suspenseEvent); + } + + if (supportsUserTimingV3) { + markAndClear( + `--suspense-${eventType}-${id}-${componentName}-${phase}-${lanes}-${displayName}`, + ); wakeable.then( () => { @@ -680,100 +679,109 @@ export function createProfilingHooks({ } function markLayoutEffectsStarted(lanes: Lanes): void { - if (isProfiling) { - recordReactMeasureStarted('layout-effects', lanes); + if (!isProfiling) { + return; } + recordReactMeasureStarted('layout-effects', lanes); if (supportsUserTimingV3) { markAndClear(`--layout-effects-start-${lanes}`); } } function markLayoutEffectsStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('layout-effects'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('layout-effects'); if (supportsUserTimingV3) { markAndClear('--layout-effects-stop'); } } function markPassiveEffectsStarted(lanes: Lanes): void { - if (isProfiling) { - recordReactMeasureStarted('passive-effects', lanes); + if (!isProfiling) { + return; } + recordReactMeasureStarted('passive-effects', lanes); if (supportsUserTimingV3) { markAndClear(`--passive-effects-start-${lanes}`); } } function markPassiveEffectsStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('passive-effects'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('passive-effects'); if (supportsUserTimingV3) { markAndClear('--passive-effects-stop'); } } function markRenderStarted(lanes: Lanes): void { - if (isProfiling) { - if (nextRenderShouldStartNewBatch) { - nextRenderShouldStartNewBatch = false; - currentBatchUID++; - } + if (!isProfiling) { + return; + } - // If this is a new batch of work, wrap an "idle" measure around it. - // Log it before the "render" measure to preserve the stack ordering. - if ( - currentReactMeasuresStack.length === 0 || - currentReactMeasuresStack[currentReactMeasuresStack.length - 1].type !== - 'render-idle' - ) { - recordReactMeasureStarted('render-idle', lanes); - } + if (nextRenderShouldStartNewBatch) { + nextRenderShouldStartNewBatch = false; + currentBatchUID++; + } - recordReactMeasureStarted('render', lanes); + // If this is a new batch of work, wrap an "idle" measure around it. + // Log it before the "render" measure to preserve the stack ordering. + if ( + currentReactMeasuresStack.length === 0 || + currentReactMeasuresStack[currentReactMeasuresStack.length - 1].type !== + 'render-idle' + ) { + recordReactMeasureStarted('render-idle', lanes); } + recordReactMeasureStarted('render', lanes); if (supportsUserTimingV3) { markAndClear(`--render-start-${lanes}`); } } function markRenderYielded(): void { - if (isProfiling) { - recordReactMeasureCompleted('render'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('render'); if (supportsUserTimingV3) { markAndClear('--render-yield'); } } function markRenderStopped(): void { - if (isProfiling) { - recordReactMeasureCompleted('render'); + if (!isProfiling) { + return; } + recordReactMeasureCompleted('render'); if (supportsUserTimingV3) { markAndClear('--render-stop'); } } function markRenderScheduled(lane: Lane): void { - if (isProfiling) { - if (currentTimelineData) { - currentTimelineData.schedulingEvents.push({ - lanes: laneToLanesArray(lane), - timestamp: getRelativeTime(), - type: 'schedule-render', - warning: null, - }); - } + if (!isProfiling) { + return; + } + + if (currentTimelineData) { + currentTimelineData.schedulingEvents.push({ + lanes: laneToLanesArray(lane), + timestamp: getRelativeTime(), + type: 'schedule-render', + warning: null, + }); } if (supportsUserTimingV3) { @@ -782,25 +790,25 @@ export function createProfilingHooks({ } function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (currentTimelineData) { - currentTimelineData.schedulingEvents.push({ - componentName, - lanes: laneToLanesArray(lane), - timestamp: getRelativeTime(), - type: 'schedule-force-update', - warning: null, - }); - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--schedule-forced-update-${lane}-${componentName}`); - } + // TODO (timeline) Record and cache component stack + if (currentTimelineData) { + currentTimelineData.schedulingEvents.push({ + componentName, + lanes: laneToLanesArray(lane), + timestamp: getRelativeTime(), + type: 'schedule-force-update', + warning: null, + }); + } + + if (supportsUserTimingV3) { + markAndClear(`--schedule-forced-update-${lane}-${componentName}`); } } @@ -815,30 +823,30 @@ export function createProfilingHooks({ } function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void { - if (isProfiling || supportsUserTimingV3) { - const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; + if (!isProfiling) { + return; + } - if (isProfiling) { - // TODO (timeline) Record and cache component stack - if (currentTimelineData) { - const event: ReactScheduleStateUpdateEvent = { - componentName, - // Store the parent fibers so we can post process - // them after we finish profiling - lanes: laneToLanesArray(lane), - timestamp: getRelativeTime(), - type: 'schedule-state-update', - warning: null, - }; - currentFiberStacks.set(event, getParentFibers(fiber)); - // $FlowFixMe[incompatible-use] found when upgrading Flow - currentTimelineData.schedulingEvents.push(event); - } - } + const componentName = getDisplayNameForFiber(fiber) || 'Unknown'; - if (supportsUserTimingV3) { - markAndClear(`--schedule-state-update-${lane}-${componentName}`); - } + // TODO (timeline) Record and cache component stack + if (currentTimelineData) { + const event: ReactScheduleStateUpdateEvent = { + componentName, + // Store the parent fibers so we can post process + // them after we finish profiling + lanes: laneToLanesArray(lane), + timestamp: getRelativeTime(), + type: 'schedule-state-update', + warning: null, + }; + currentFiberStacks.set(event, getParentFibers(fiber)); + // $FlowFixMe[incompatible-use] found when upgrading Flow + currentTimelineData.schedulingEvents.push(event); + } + + if (supportsUserTimingV3) { + markAndClear(`--schedule-state-update-${lane}-${componentName}`); } } diff --git a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js index 7d4cfa65ce030..fdd7bce2f8d9e 100644 --- a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js +++ b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js @@ -29,7 +29,10 @@ export function formatOwnerStackString(stack: string): string { // Pop the JSX frame. stack = stack.slice(idx + 1); } - idx = stack.indexOf('react-stack-bottom-frame'); + idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } if (idx !== -1) { idx = stack.lastIndexOf('\n', idx); } diff --git a/packages/react-devtools-shared/src/backend/utils/index.js b/packages/react-devtools-shared/src/backend/utils/index.js index 950161a020b21..b7e449869190e 100644 --- a/packages/react-devtools-shared/src/backend/utils/index.js +++ b/packages/react-devtools-shared/src/backend/utils/index.js @@ -358,7 +358,12 @@ function collectStackTrace( // We mirror how V8 serializes stack frames and how we later parse them. for (let i = 0; i < structuredStackTrace.length; i++) { const callSite = structuredStackTrace[i]; - if (callSite.getFunctionName() === 'react-stack-bottom-frame') { + const name = callSite.getFunctionName(); + if ( + name != null && + (name.includes('react_stack_bottom_frame') || + name.includes('react-stack-bottom-frame')) + ) { // We pick the last frame that matches before the bottom frame since // that will be immediately inside the component as opposed to some helper. // If we don't find a bottom frame then we bail to string parsing. @@ -376,7 +381,7 @@ function collectStackTrace( // $FlowFixMe[prop-missing] typeof callSite.getEnclosingColumnNumber === 'function' ? (callSite: any).getEnclosingColumnNumber() - : callSite.getLineNumber(); + : callSite.getColumnNumber(); if (!sourceURL || !line || !col) { // Skip eval etc. without source url. They don't have location. continue; @@ -407,12 +412,19 @@ export function parseSourceFromOwnerStack(error: Error): Source | null { let stack; try { stack = error.stack; + } catch (e) { + // $FlowFixMe[incompatible-type] It does accept undefined. + Error.prepareStackTrace = undefined; + stack = error.stack; } finally { Error.prepareStackTrace = previousPrepare; } if (collectedLocation !== null) { return collectedLocation; } + if (stack == null) { + return null; + } // Fallback to parsing the string form. const componentStack = formatOwnerStackString(stack); return parseSourceFromComponentStack(componentStack); diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 3895217053df1..97c3adc88f032 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -195,6 +195,10 @@ export default class Store extends EventEmitter<{ // Only used in browser extension for synchronization with built-in Elements panel. _lastSelectedHostInstanceElementId: Element['id'] | null = null; + // Maximum recorded node depth during the lifetime of this Store. + // Can only increase: not guaranteed to return maximal value for currently recorded elements. + _maximumRecordedDepth = 0; + constructor(bridge: FrontendBridge, config?: Config) { super(); @@ -698,6 +702,50 @@ export default class Store extends EventEmitter<{ return index; } + isDescendantOf(parentId: number, descendantId: number): boolean { + if (descendantId === 0) { + return false; + } + + const descendant = this.getElementByID(descendantId); + if (descendant === null) { + return false; + } + + if (descendant.parentID === parentId) { + return true; + } + + const parent = this.getElementByID(parentId); + if (!parent || parent.depth >= descendant.depth) { + return false; + } + + return this.isDescendantOf(parentId, descendant.parentID); + } + + /** + * Returns index of the lowest descendant element, if available. + * May not be the deepest element, the lowest is used in a sense of bottom-most from UI Tree representation perspective. + */ + getIndexOfLowestDescendantElement(element: Element): number | null { + let current: null | Element = element; + while (current !== null) { + if (current.isCollapsed || current.children.length === 0) { + if (current === element) { + return null; + } + + return this.getIndexOfElementID(current.id); + } else { + const lastChildID = current.children[current.children.length - 1]; + current = this.getElementByID(lastChildID); + } + } + + return null; + } + getOwnersListForElement(ownerID: number): Array { const list: Array = []; const element = this._idToElement.get(ownerID); @@ -1089,9 +1137,15 @@ export default class Store extends EventEmitter<{ compiledWithForget, } = parseElementDisplayNameFromBackend(displayName, type); + const elementDepth = parentElement.depth + 1; + this._maximumRecordedDepth = Math.max( + this._maximumRecordedDepth, + elementDepth, + ); + const element: Element = { children: [], - depth: parentElement.depth + 1, + depth: elementDepth, displayName: displayNameWithoutHOCs, hocDisplayNames, id, @@ -1536,6 +1590,14 @@ export default class Store extends EventEmitter<{ } }; + /** + * Maximum recorded node depth during the lifetime of this Store. + * Can only increase: not guaranteed to return maximal value for currently recorded elements. + */ + getMaximumRecordedDepth(): number { + return this._maximumRecordedDepth; + } + updateHookSettings: (settings: $ReadOnly) => void = settings => { this._hookSettings = settings; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index b7ce607685051..1d99317ba0909 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -176,7 +176,7 @@ function Components(_: {}) { const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; const VERTICAL_MODE_MAX_WIDTH = 600; -const MINIMUM_SIZE = 50; +const MINIMUM_SIZE = 100; function initResizeState(): ResizeState { let horizontalPercentage = 0.65; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.css b/packages/react-devtools-shared/src/devtools/views/Components/Element.css index b11e321e2e6d5..c25eddbdbb42c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.css @@ -1,7 +1,9 @@ .Element, +.HoveredElement, .InactiveSelectedElement, -.SelectedElement, -.HoveredElement { +.HighlightedElement, +.InactiveHighlightedElement, +.SelectedElement { color: var(--color-component-name); } .HoveredElement { @@ -10,8 +12,15 @@ .InactiveSelectedElement { background-color: var(--color-background-inactive); } +.HighlightedElement { + background-color: var(--color-selected-tree-highlight-active); +} +.InactiveHighlightedElement { + background-color: var(--color-selected-tree-highlight-inactive); +} .Wrapper { + position: relative; padding: 0 0.25rem; white-space: pre; height: var(--line-height-data); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index 71e0ebfbe9cbe..c3ddf1da07518 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -45,10 +45,6 @@ export default function Element({data, index, style}: Props): React.Node { const [isHovered, setIsHovered] = useState(false); - const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data; - const id = element === null ? null : element.id; - const isSelected = inspectedElementID === id; - const errorsAndWarningsSubscription = useMemo( () => ({ getCurrentValue: () => @@ -68,6 +64,15 @@ export default function Element({data, index, style}: Props): React.Node { }>(errorsAndWarningsSubscription); const changeOwnerAction = useChangeOwnerAction(); + + // Handle elements that are removed from the tree while an async render is in progress. + if (element == null) { + console.warn(` Could not find element at index ${index}`); + + // This return needs to happen after hooks, since hooks can't be conditional. + return null; + } + const handleDoubleClick = () => { if (id !== null) { changeOwnerAction(id); @@ -107,15 +112,8 @@ export default function Element({data, index, style}: Props): React.Node { event.preventDefault(); }; - // Handle elements that are removed from the tree while an async render is in progress. - if (element == null) { - console.warn(` Could not find element at index ${index}`); - - // This return needs to happen after hooks, since hooks can't be conditional. - return null; - } - const { + id, depth, displayName, hocDisplayNames, @@ -123,6 +121,19 @@ export default function Element({data, index, style}: Props): React.Node { key, compiledWithForget, } = element; + const { + isNavigatingWithKeyboard, + onElementMouseEnter, + treeFocused, + calculateElementOffset, + } = data; + + const isSelected = inspectedElementID === id; + const isDescendantOfSelected = + inspectedElementID !== null && + !isSelected && + store.isDescendantOf(inspectedElementID, id); + const elementOffset = calculateElementOffset(depth); // Only show strict mode non-compliance badges for top level elements. // Showing an inline badge for every element in the tree would be noisy. @@ -135,6 +146,10 @@ export default function Element({data, index, style}: Props): React.Node { : styles.InactiveSelectedElement; } else if (isHovered && !isNavigatingWithKeyboard) { className = styles.HoveredElement; + } else if (isDescendantOfSelected) { + className = treeFocused + ? styles.HighlightedElement + : styles.InactiveHighlightedElement; } return ( @@ -144,17 +159,13 @@ export default function Element({data, index, style}: Props): React.Node { onMouseLeave={handleMouseLeave} onMouseDown={handleClick} onDoubleClick={handleDoubleClick} - style={style} - data-testname="ComponentTreeListItem" - data-depth={depth}> + style={{ + ...style, + paddingLeft: elementOffset, + }} + data-testname="ComponentTreeListItem"> {/* This wrapper is used by Tree for measurement purposes. */} -
+
{ownerID === null && ( )} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css b/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css deleted file mode 100644 index 19b64a8ef4702..0000000000000 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.css +++ /dev/null @@ -1,16 +0,0 @@ -.Active, -.Inactive { - position: absolute; - left: 0; - width: 100%; - z-index: 0; - pointer-events: none; -} - -.Active { - background-color: var(--color-selected-tree-highlight-active); -} - -.Inactive { - background-color: var(--color-selected-tree-highlight-inactive); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js deleted file mode 100644 index 16035a13d65f9..0000000000000 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import type {Element} from 'react-devtools-shared/src/frontend/types'; - -import * as React from 'react'; -import {useContext, useMemo} from 'react'; -import {TreeStateContext} from './TreeContext'; -import {SettingsContext} from '../Settings/SettingsContext'; -import TreeFocusedContext from './TreeFocusedContext'; -import {StoreContext} from '../context'; -import {useSubscription} from '../hooks'; - -import styles from './SelectedTreeHighlight.css'; - -type Data = { - startIndex: number, - stopIndex: number, -}; - -export default function SelectedTreeHighlight(_: {}): React.Node { - const {lineHeight} = useContext(SettingsContext); - const store = useContext(StoreContext); - const treeFocused = useContext(TreeFocusedContext); - const {ownerID, inspectedElementID} = useContext(TreeStateContext); - - const subscription = useMemo( - () => ({ - getCurrentValue: () => { - if ( - inspectedElementID === null || - store.isInsideCollapsedSubTree(inspectedElementID) - ) { - return null; - } - - const element = store.getElementByID(inspectedElementID); - if ( - element === null || - element.isCollapsed || - element.children.length === 0 - ) { - return null; - } - - const startIndex = store.getIndexOfElementID(element.children[0]); - if (startIndex === null) { - return null; - } - - let stopIndex = null; - let current: null | Element = element; - while (current !== null) { - if (current.isCollapsed || current.children.length === 0) { - // We've found the last/deepest descendant. - stopIndex = store.getIndexOfElementID(current.id); - current = null; - } else { - const lastChildID = current.children[current.children.length - 1]; - current = store.getElementByID(lastChildID); - } - } - - if (stopIndex === null) { - return null; - } - - return { - startIndex, - stopIndex, - }; - }, - subscribe: (callback: Function) => { - store.addListener('mutated', callback); - return () => { - store.removeListener('mutated', callback); - }; - }, - }), - [inspectedElementID, store], - ); - const data = useSubscription(subscription); - - if (ownerID !== null) { - return null; - } - - if (data === null) { - return null; - } - - const {startIndex, stopIndex} = data; - - return ( -
- ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css index bf18f1d2e6019..a65cda45aac9f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css @@ -5,17 +5,16 @@ display: flex; flex-direction: column; border-top: 1px solid var(--color-border); - - /* Default size will be adjusted by Tree after scrolling */ - --indentation-size: 12px; } -.List { - overflow-x: hidden !important; +.InnerElementType { + position: relative; } -.InnerElementType { - overflow-x: hidden; +.VerticalDelimiter { + position: absolute; + width: 0.025rem; + background: #b0b0b0; } .SearchInput { @@ -97,4 +96,4 @@ .Link { color: var(--color-button-active); -} \ No newline at end of file +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 1ba61c52dd1a4..4e2d0f3551fee 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -29,7 +29,6 @@ import InspectHostNodesToggle from './InspectHostNodesToggle'; import OwnersStack from './OwnersStack'; import ComponentSearchInput from './ComponentSearchInput'; import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; -import SelectedTreeHighlight from './SelectedTreeHighlight'; import TreeFocusedContext from './TreeFocusedContext'; import {useHighlightHostInstance, useSubscription} from '../hooks'; import {clearErrorsAndWarnings as clearErrorsAndWarningsAPI} from 'react-devtools-shared/src/backendAPI'; @@ -40,13 +39,18 @@ import {logEvent} from 'react-devtools-shared/src/Logger'; import {useExtensionComponentsPanelVisibility} from 'react-devtools-shared/src/frontend/hooks/useExtensionComponentsPanelVisibility'; import {useChangeOwnerAction} from './OwnersListContext'; -// Never indent more than this number of pixels (even if we have the room). -const DEFAULT_INDENTATION_SIZE = 12; +// Indent for each node at level N, compared to node at level N - 1. +const INDENTATION_SIZE = 10; + +function calculateElementOffset(elementDepth: number): number { + return elementDepth * INDENTATION_SIZE; +} export type ItemData = { isNavigatingWithKeyboard: boolean, onElementMouseEnter: (id: number) => void, treeFocused: boolean, + calculateElementOffset: (depth: number) => number, }; function calculateInitialScrollOffset( @@ -90,16 +94,56 @@ export default function Tree(): React.Node { const treeRef = useRef(null); const focusTargetRef = useRef(null); const listRef = useRef(null); + const listDOMElementRef = useRef(null); useEffect(() => { - if (!componentsPanelVisible) { + if (!componentsPanelVisible || inspectedElementIndex == null) { + return; + } + + const listDOMElement = listDOMElementRef.current; + if (listDOMElement == null) { return; } - if (listRef.current != null && inspectedElementIndex !== null) { - listRef.current.scrollToItem(inspectedElementIndex, 'smart'); + const viewportHeight = listDOMElement.clientHeight; + const viewportLeft = listDOMElement.scrollLeft; + const viewportRight = viewportLeft + listDOMElement.clientWidth; + const viewportTop = listDOMElement.scrollTop; + const viewportBottom = viewportTop + viewportHeight; + + const element = store.getElementAtIndex(inspectedElementIndex); + if (element == null) { + return; + } + const elementLeft = calculateElementOffset(element.depth); + // Because of virtualization, this element might not be rendered yet; we can't look up its width. + // Assuming that it may take up to the half of the vieport. + const elementRight = elementLeft + listDOMElement.clientWidth / 2; + const elementTop = inspectedElementIndex * lineHeight; + const elementBottom = elementTop + lineHeight; + + const isElementFullyVisible = + elementTop >= viewportTop && + elementBottom <= viewportBottom && + elementLeft >= viewportLeft && + elementRight <= viewportRight; + + if (!isElementFullyVisible) { + const verticalDelta = + Math.min(0, elementTop - viewportTop) + + Math.max(0, elementBottom - viewportBottom); + const horizontalDelta = + Math.min(0, elementLeft - viewportLeft) + + Math.max(0, elementRight - viewportRight); + + listDOMElement.scrollBy({ + top: verticalDelta, + left: horizontalDelta, + behavior: treeFocused && ownerID == null ? 'smooth' : 'instant', + }); } - }, [inspectedElementIndex, componentsPanelVisible]); + }, [inspectedElementIndex, componentsPanelVisible, lineHeight]); // Picking an element in the inspector should put focus into the tree. // If possible, navigation works right after picking a node. @@ -291,8 +335,14 @@ export default function Tree(): React.Node { isNavigatingWithKeyboard, onElementMouseEnter: handleElementMouseEnter, treeFocused, + calculateElementOffset, }), - [isNavigatingWithKeyboard, handleElementMouseEnter, treeFocused], + [ + isNavigatingWithKeyboard, + handleElementMouseEnter, + treeFocused, + calculateElementOffset, + ], ); const itemKey = useCallback( @@ -422,6 +472,8 @@ export default function Tree(): React.Node { itemKey={itemKey} itemSize={lineHeight} ref={listRef} + outerRef={listDOMElementRef} + overscanCount={10} width={width}> {Element} @@ -434,153 +486,57 @@ export default function Tree(): React.Node { ); } -// Indentation size can be adjusted but child width is fixed. -// We need to adjust indentations so the widest child can fit without overflowing. -// Sometimes the widest child is also the deepest in the tree: -// ┏----------------------┓ -// ┆ ┆ -// ┆ •••• ┆ -// ┆ •••••••• ┆ -// ┗----------------------┛ -// -// But this is not always the case. -// Even with the above example, a change in indentation may change the overall widest child: -// ┏----------------------┓ -// ┆ ┆ -// ┆ •• ┆ -// ┆ •••• ┆ -// ┗----------------------┛ -// -// In extreme cases this difference can be important: -// ┏----------------------┓ -// ┆ ┆ -// ┆ •• ┆ -// ┆ •••• ┆ -// ┆ •••••• ┆ -// ┆ •••••••• ┆ -// ┗----------------------┛ -// -// In the above example, the current indentation is fine, -// but if we naively assumed that the widest element is also the deepest element, -// we would end up compressing the indentation unnecessarily: -// ┏----------------------┓ -// ┆ ┆ -// ┆ • ┆ -// ┆ •• ┆ -// ┆ ••• ┆ -// ┆ •••• ┆ -// ┗----------------------┛ -// -// The way we deal with this is to compute the max indentation size that can fit each child, -// given the child's fixed width and depth within the tree. -// Then we take the smallest of these indentation sizes... -function updateIndentationSizeVar( - innerDiv: HTMLDivElement, - cachedChildWidths: WeakMap, - indentationSizeRef: {current: number}, - prevListWidthRef: {current: number}, -): void { - const list = ((innerDiv.parentElement: any): HTMLDivElement); - const listWidth = list.clientWidth; - - // Skip measurements when the Components panel is hidden. - if (listWidth === 0) { - return; - } - - // Reset the max indentation size if the width of the tree has increased. - if (listWidth > prevListWidthRef.current) { - indentationSizeRef.current = DEFAULT_INDENTATION_SIZE; - } - prevListWidthRef.current = listWidth; - - let maxIndentationSize: number = indentationSizeRef.current; - - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const child of innerDiv.children) { - const depth = parseInt(child.getAttribute('data-depth'), 10) || 0; - - let childWidth: number = 0; - - const cachedChildWidth = cachedChildWidths.get(child); - if (cachedChildWidth != null) { - childWidth = cachedChildWidth; - } else { - const {firstElementChild} = child; - - // Skip over e.g. the guideline element - if (firstElementChild != null) { - childWidth = firstElementChild.clientWidth; - cachedChildWidths.set(child, childWidth); - } - } - - const remainingWidth = Math.max(0, listWidth - childWidth); +// $FlowFixMe[missing-local-annot] +function InnerElementType({children, style}) { + const store = useContext(StoreContext); - maxIndentationSize = Math.min(maxIndentationSize, remainingWidth / depth); - } + const {height} = style; + const maxDepth = store.getMaximumRecordedDepth(); + // Maximum possible indentation plus some arbitrary offset for the node content. + const width = calculateElementOffset(maxDepth) + 500; - indentationSizeRef.current = maxIndentationSize; + return ( +
+ {children} - list.style.setProperty('--indentation-size', `${maxIndentationSize}px`); + +
+ ); } -// $FlowFixMe[missing-local-annot] -function InnerElementType({children, style}) { - const {ownerID} = useContext(TreeStateContext); +function VerticalDelimiter() { + const store = useContext(StoreContext); + const {ownerID, inspectedElementIndex} = useContext(TreeStateContext); + const {lineHeight} = useContext(SettingsContext); - const cachedChildWidths = useMemo>( - () => new WeakMap(), - [], - ); + if (ownerID != null || inspectedElementIndex == null) { + return null; + } - // This ref tracks the current indentation size. - // We decrease indentation to fit wider/deeper trees. - // We intentionally do not increase it again afterward, to avoid the perception of content "jumping" - // e.g. clicking to toggle/collapse a row might otherwise jump horizontally beneath your cursor, - // e.g. scrolling a wide row off screen could cause narrower rows to jump to the right some. - // - // There are two exceptions for this: - // 1. The first is when the width of the tree increases. - // The user may have resized the window specifically to make more room for DevTools. - // In either case, this should reset our max indentation size logic. - // 2. The second is when the user enters or exits an owner tree. - const indentationSizeRef = useRef(DEFAULT_INDENTATION_SIZE); - const prevListWidthRef = useRef(0); - const prevOwnerIDRef = useRef(ownerID); - const divRef = useRef(null); - - // We shouldn't retain this width across different conceptual trees though, - // so when the user opens the "owners tree" view, we should discard the previous width. - if (ownerID !== prevOwnerIDRef.current) { - prevOwnerIDRef.current = ownerID; - indentationSizeRef.current = DEFAULT_INDENTATION_SIZE; + const element = store.getElementAtIndex(inspectedElementIndex); + if (element == null) { + return null; + } + const indexOfLowestDescendant = + store.getIndexOfLowestDescendantElement(element); + if (indexOfLowestDescendant == null) { + return null; } - // When we render new content, measure to see if we need to shrink indentation to fit it. - useEffect(() => { - if (divRef.current !== null) { - updateIndentationSizeVar( - divRef.current, - cachedChildWidths, - indentationSizeRef, - prevListWidthRef, - ); - } - }); + const delimiterLeft = calculateElementOffset(element.depth) + 12; + const delimiterTop = (inspectedElementIndex + 1) * lineHeight; + const delimiterHeight = + (indexOfLowestDescendant + 1) * lineHeight - delimiterTop; - // This style override enables the background color to fill the full visible width, - // when combined with the CSS tweaks in Element. - // A lot of options were considered; this seemed the one that requires the least code. - // See https://github.com/bvaughn/react-devtools-experimental/issues/9 return (
- - {children} -
+ className={styles.VerticalDelimiter} + style={{ + left: delimiterLeft, + top: delimiterTop, + height: delimiterHeight, + }} + /> ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index 4309b0e5fbe4d..106538cad3ab2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -340,30 +340,35 @@ export default function ComponentsSettings({ ); return ( -
- +
+
+ +
- +
+ +