diff --git a/.eslintrc.js b/.eslintrc.js index f5b98c6b4887f..ae108a37e3da1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -446,10 +446,7 @@ module.exports = { }, }, { - files: [ - 'scripts/eslint-rules/*.js', - 'packages/eslint-plugin-react-hooks/src/*.js', - ], + files: ['scripts/eslint-rules/*.js'], plugins: ['eslint-plugin'], rules: { 'eslint-plugin/prefer-object-rule': ERROR, @@ -500,6 +497,7 @@ module.exports = { 'packages/react-devtools-shared/src/hook.js', 'packages/react-devtools-shared/src/backend/console.js', 'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js', + 'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js', ], globals: { __IS_CHROME__: 'readonly', @@ -507,6 +505,7 @@ module.exports = { __IS_EDGE__: 'readonly', __IS_NATIVE__: 'readonly', __IS_INTERNAL_VERSION__: 'readonly', + chrome: 'readonly', }, }, { @@ -515,6 +514,26 @@ module.exports = { __IS_INTERNAL_VERSION__: 'readonly', }, }, + { + files: ['packages/eslint-plugin-react-hooks/src/**/*'], + extends: ['plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint', 'eslint-plugin'], + rules: { + '@typescript-eslint/no-explicit-any': OFF, + '@typescript-eslint/no-non-null-assertion': OFF, + '@typescript-eslint/array-type': [ERROR, {default: 'generic'}], + + 'es/no-optional-chaining': OFF, + + 'eslint-plugin/prefer-object-rule': ERROR, + 'eslint-plugin/require-meta-fixable': [ + ERROR, + {catchNoFixerButFixableProperty: true}, + ], + 'eslint-plugin/require-meta-has-suggestions': ERROR, + }, + }, ], env: { @@ -595,6 +614,7 @@ module.exports = { KeyframeAnimationOptions: 'readonly', GetAnimationsOptions: 'readonly', Animatable: 'readonly', + ScrollTimeline: 'readonly', spyOnDev: 'readonly', spyOnDevAndProd: 'readonly', diff --git a/.github/workflows/compiler_discord_notify.yml b/.github/workflows/compiler_discord_notify.yml index b4694e12be94d..f81f42b9cb7df 100644 --- a/.github/workflows/compiler_discord_notify.yml +++ b/.github/workflows/compiler_discord_notify.yml @@ -2,7 +2,7 @@ name: (Compiler) Discord Notify on: pull_request_target: - types: [labeled] + types: [opened, ready_for_review] paths: - compiler/** - .github/workflows/compiler_**.yml diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index 78cbac7fb536d..12e341a86fd96 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -476,9 +476,7 @@ jobs: fi - name: Playwright install deps working-directory: fixtures/flight - run: | - npx playwright install - sudo npx playwright install-deps + run: npx playwright install --with-deps chromium - name: Run tests working-directory: fixtures/flight run: yarn test @@ -501,6 +499,9 @@ jobs: name: Build DevTools and process artifacts needs: build_and_lint runs-on: ubuntu-latest + strategy: + matrix: + browser: [chrome, firefox, edge] steps: - uses: actions/checkout@v4 with: @@ -525,31 +526,32 @@ jobs: pattern: _build_* path: build merge-multiple: true - - run: ./scripts/ci/pack_and_store_devtools_artifacts.sh + - run: ./scripts/ci/pack_and_store_devtools_artifacts.sh ${{ matrix.browser }} env: RELEASE_CHANNEL: experimental - name: Display structure of build run: ls -R build - - name: Archive devtools build - uses: actions/upload-artifact@v4 - with: - name: react-devtools - path: build/devtools.tgz # Simplifies getting the extension for local testing - - name: Archive chrome extension + - name: Archive ${{ matrix.browser }} extension uses: actions/upload-artifact@v4 with: - name: react-devtools-chrome-extension - path: build/devtools/chrome-extension.zip - - name: Archive firefox extension - uses: actions/upload-artifact@v4 + name: react-devtools-${{ matrix.browser }}-extension + path: build/devtools/${{ matrix.browser }}-extension.zip + + merge_devtools_artifacts: + name: Merge DevTools artifacts + needs: build_devtools_and_process_artifacts + runs-on: ubuntu-latest + steps: + - name: Merge artifacts + uses: actions/upload-artifact/merge@v4 with: - name: react-devtools-firefox-extension - path: build/devtools/firefox-extension.zip + name: react-devtools + pattern: react-devtools-*-extension run_devtools_e2e_tests: name: Run DevTools e2e tests - needs: build_devtools_and_process_artifacts + needs: build_and_lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -583,9 +585,10 @@ jobs: RELEASE_CHANNEL: experimental # ----- SIZEBOT ----- - download_base_build_for_sizebot: - if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' }} - name: Download base build for sizebot + sizebot: + if: ${{ github.event_name == 'pull_request' && github.ref_name != 'main' && github.event.pull_request.base.ref == 'main' }} + name: Run sizebot + needs: [build_and_lint] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -601,16 +604,14 @@ jobs: id: node_modules with: path: "**/node_modules" - key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }} + key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Ensure clean build directory run: rm -rf build - - run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile working-directory: scripts/release - name: Download artifacts for base revision run: | - git fetch origin main - GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=$(git rev-parse origin/main) + GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=$(git rev-parse ${{ github.event.pull_request.base.sha }}) mv ./build ./base-build # TODO: The `download-experimental-build` script copies the npm # packages into the `node_modules` directory. This is a historical @@ -618,33 +619,8 @@ jobs: # don't exist. - name: Delete extraneous files run: rm -rf ./base-build/node_modules - - name: Display structure of base-build + - name: Display structure of base-build from origin/main run: ls -R base-build - - name: Archive base-build - uses: actions/upload-artifact@v4 - with: - name: base-build - path: base-build - - sizebot: - name: Run sizebot - needs: [build_and_lint, download_base_build_for_sizebot] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - cache: yarn - cache-dependency-path: yarn.lock - - name: Restore cached node_modules - uses: actions/cache@v4 - id: node_modules - with: - path: "**/node_modules" - key: runtime-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} - name: Ensure clean build directory run: rm -rf build - run: yarn install --frozen-lockfile @@ -660,13 +636,6 @@ jobs: node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js - name: Display structure of build for PR run: ls -R build - - name: Restore archived base-build from origin/main - uses: actions/download-artifact@v4 - with: - name: base-build - path: base-build - - name: Display structure of base-build from origin/main - run: ls -R base-build - run: echo ${{ github.sha }} >> build/COMMIT_SHA - run: node ./scripts/tasks/danger - name: Archive sizebot results diff --git a/.github/workflows/runtime_discord_notify.yml b/.github/workflows/runtime_discord_notify.yml index a29735ac4ed8e..e41b1c56405a7 100644 --- a/.github/workflows/runtime_discord_notify.yml +++ b/.github/workflows/runtime_discord_notify.yml @@ -2,7 +2,7 @@ name: (Runtime) Discord Notify on: pull_request_target: - types: [labeled] + types: [opened, ready_for_review] paths-ignore: - compiler/** - .github/workflows/compiler_**.yml diff --git a/.github/workflows/runtime_eslint_plugin_e2e.yml b/.github/workflows/runtime_eslint_plugin_e2e.yml new file mode 100644 index 0000000000000..edc188f38622e --- /dev/null +++ b/.github/workflows/runtime_eslint_plugin_e2e.yml @@ -0,0 +1,56 @@ +name: (Runtime) ESLint Plugin E2E + +on: + push: + branches: [main] + pull_request: + paths-ignore: + - compiler/** + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + +jobs: + # ----- TESTS ----- + test: + name: ESLint v${{ matrix.eslint_major }} + runs-on: ubuntu-latest + strategy: + matrix: + eslint_major: + - "6" + - "7" + - "8" + - "9" + continue-on-error: true + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: yarn + cache-dependency-path: yarn.lock + - name: Restore cached node_modules + uses: actions/cache@v4 + id: node_modules + with: + path: "node_modules" + key: runtime-eslint_e2e-node_modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + - name: Ensure clean build directory + run: rm -rf build + - run: yarn install --frozen-lockfile + - name: Build plugin + working-directory: fixtures/eslint-v${{ matrix.eslint_major }} + run: node build.mjs + - name: Install fixture dependencies + working-directory: ./fixtures/eslint-v${{ matrix.eslint_major }} + run: yarn --frozen-lockfile + - name: Run lint test + working-directory: ./fixtures/eslint-v${{ matrix.eslint_major }} + run: yarn lint diff --git a/.github/workflows/shared_close_direct_sync_branch_prs.yml b/.github/workflows/shared_close_direct_sync_branch_prs.yml new file mode 100644 index 0000000000000..7575c0e913c7e --- /dev/null +++ b/.github/workflows/shared_close_direct_sync_branch_prs.yml @@ -0,0 +1,37 @@ +name: (Shared) Close Direct Sync Branch PRs + +on: + pull_request: + branches: + - 'builds/facebook-**' + +env: + TZ: /usr/share/zoneinfo/America/Los_Angeles + # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout + SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 + +jobs: + close_pr: + runs-on: ubuntu-latest + steps: + - name: Close PR + uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const pullNumber = ${{ github.event.number }}; + + await github.rest.pulls.createReview({ + owner, + repo, + pull_number: pullNumber, + body: 'Do not land changes to `${{ github.event.pull_request.base.ref }}`. Please re-open your PR targeting `main` instead.', + event: 'REQUEST_CHANGES' + }); + await github.rest.pulls.update({ + owner, + repo, + pull_number: pullNumber, + state: 'closed' + }); diff --git a/babel.config-ts.js b/babel.config-ts.js new file mode 100644 index 0000000000000..20b35ec013e0f --- /dev/null +++ b/babel.config-ts.js @@ -0,0 +1,15 @@ +/** + * This file is purely being used for local jest runs, and doesn't participate in the build process. + */ +'use strict'; + +module.exports = { + plugins: [ + '@babel/plugin-syntax-jsx', + '@babel/plugin-transform-flow-strip-types', + ], + presets: [ + ['@babel/preset-env', {targets: {node: 'current'}}], + '@babel/preset-typescript', + ], +}; diff --git a/compiler/apps/playground/components/StoreContext.tsx b/compiler/apps/playground/components/StoreContext.tsx index 8529a3c9f84d2..10ad614b05554 100644 --- a/compiler/apps/playground/components/StoreContext.tsx +++ b/compiler/apps/playground/components/StoreContext.tsx @@ -6,11 +6,10 @@ */ import type {Dispatch, ReactNode} from 'react'; -import {useReducer} from 'react'; +import {useEffect, useReducer} from 'react'; import createContext from '../lib/createContext'; import {emptyStore} from '../lib/defaultStore'; -import type {Store} from '../lib/stores'; -import {saveStore} from '../lib/stores'; +import {saveStore, type Store} from '../lib/stores'; const StoreContext = createContext(); @@ -31,6 +30,11 @@ export const useStoreDispatch = StoreDispatchContext.useContext; */ export function StoreProvider({children}: {children: ReactNode}): JSX.Element { const [store, dispatch] = useReducer(storeReducer, emptyStore); + useEffect(() => { + if (store !== emptyStore) { + saveStore(store); + } + }, [store]); return ( @@ -59,19 +63,14 @@ function storeReducer(store: Store, action: ReducerAction): Store { switch (action.type) { case 'setStore': { const newStore = action.payload.store; - - saveStore(newStore); return newStore; } case 'updateFile': { const {source} = action.payload; - const newStore = { ...store, source, }; - - saveStore(newStore); return newStore; } } diff --git a/compiler/apps/playground/next-env.d.ts b/compiler/apps/playground/next-env.d.ts index 40c3d68096c27..1b3be0840f3f6 100644 --- a/compiler/apps/playground/next-env.d.ts +++ b/compiler/apps/playground/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/compiler/apps/playground/package.json b/compiler/apps/playground/package.json index 9b43f18f0e8c4..b8360ae06f5a7 100644 --- a/compiler/apps/playground/package.json +++ b/compiler/apps/playground/package.json @@ -34,7 +34,7 @@ "invariant": "^2.2.4", "lz-string": "^1.5.0", "monaco-editor": "^0.52.0", - "next": "^15.0.1", + "next": "^15.2.0-canary.64", "notistack": "^3.0.0-alpha.7", "prettier": "^3.3.3", "pretty-format": "^29.3.1", diff --git a/compiler/apps/playground/yarn.lock b/compiler/apps/playground/yarn.lock index dc5362548a7f7..ea0487b01c901 100644 --- a/compiler/apps/playground/yarn.lock +++ b/compiler/apps/playground/yarn.lock @@ -589,10 +589,10 @@ dependencies: "@monaco-editor/loader" "^1.4.0" -"@next/env@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-15.0.1.tgz#660fe9303e255cec112d3f4198d2897a24bc60b3" - integrity sha512-lc4HeDUKO9gxxlM5G2knTRifqhsY6yYpwuHspBZdboZe0Gp+rZHBNNSIjmQKDJIdRXiXGyVnSD6gafrbQPvILQ== +"@next/env@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.0-canary.66.tgz#c4ca0d502ad099c68927643df9c9b5d75c7b7fbb" + integrity sha512-/RxW1GJ7a6MJOQ7LOa2bcli7VTjqB7jPyzXwNJQflcYJH4gz1kP6uzg8+IptLJGFSRB58RBKHJk+q1cD8jongA== "@next/eslint-plugin-next@15.0.1": version "15.0.1" @@ -601,45 +601,45 @@ dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.1.tgz#b80a25f1569bd0ca03eca9473f7e93e64937e404" - integrity sha512-C9k/Xv4sxkQRTA37Z6MzNq3Yb1BJMmSqjmwowoWEpbXTkAdfOwnoKOpAb71ItSzoA26yUTIo6ZhN8rKGu4ExQw== - -"@next/swc-darwin-x64@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.1.tgz#00dcf79ec7c638a85c3b9ff2e2de2bfb09c1c250" - integrity sha512-uHl13HXOuq1G7ovWFxCACDJHTSDVbn/sbLv8V1p+7KIvTrYQ5HNoSmKBdYeEKRRCbEmd+OohOgg9YOp8Ux3MBg== - -"@next/swc-linux-arm64-gnu@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.1.tgz#faab5f7ffcc6d1a15e8dea1cb9953966658b39bf" - integrity sha512-LvyhvxHOihFTEIbb35KxOc3q8w8G4xAAAH/AQnsYDEnOvwawjL2eawsB59AX02ki6LJdgDaHoTEnC54Gw+82xw== - -"@next/swc-linux-arm64-musl@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.1.tgz#97abada9a782ab5b3cb42cf0d4799cbc2e733351" - integrity sha512-vFmCGUFNyk/A5/BYcQNhAQqPIw01RJaK6dRO+ZEhz0DncoW+hJW1kZ8aH2UvTX27zPq3m85zN5waMSbZEmANcQ== - -"@next/swc-linux-x64-gnu@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.1.tgz#548bd47c49fe6d819302139aff8766eb704322e2" - integrity sha512-5by7IYq0NCF8rouz6Qg9T97jYU68kaClHPfGpQG2lCZpSYHtSPQF1kjnqBTd34RIqPKMbCa4DqCufirgr8HM5w== - -"@next/swc-linux-x64-musl@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.1.tgz#84423fbd3a058dd6ae8322e530878f0ec7a1027a" - integrity sha512-lmYr6H3JyDNBJLzklGXLfbehU3ay78a+b6UmBGlHls4xhDXBNZfgb0aI67sflrX+cGBnv1LgmWzFlYrAYxS1Qw== - -"@next/swc-win32-arm64-msvc@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.1.tgz#723c2ced12a998fb40dc901b8faea9170e788c2f" - integrity sha512-DS8wQtl6diAj0eZTdH0sefykm4iXMbHT4MOvLwqZiIkeezKpkgPFcEdFlz3vKvXa2R/2UEgMh48z1nEpNhjeOQ== - -"@next/swc-win32-x64-msvc@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.1.tgz#ec7e3befc0bcc47527537b1eda2b3745beb15a09" - integrity sha512-4Ho2ggvDdMKlZ/0e9HNdZ9ngeaBwtc+2VS5oCeqrbXqOgutX6I4U2X/42VBw0o+M5evn4/7v3zKgGHo+9v/VjA== +"@next/swc-darwin-arm64@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0-canary.66.tgz#368438cf713c439b5b4c44d54b5c3b31ee5b772d" + integrity sha512-sVzNJWTekcLOdqkDMistBGr84AVh9eSu4o5JQNEOdxHry4jiF8lqixpOg0+Twj2RRuv4bx32h5xaRVvCSUpITQ== + +"@next/swc-darwin-x64@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0-canary.66.tgz#3ddc3f4f6e86e204727770e5984cabf52f852472" + integrity sha512-Avv6Nf/0j0WVqY72Q0mK2glGhvN7LT7iVF31iBYUe/Cbf2cXBjgpXUVmksJjg+2Fi6uTEpaMoZWSVEpJyPkjVQ== + +"@next/swc-linux-arm64-gnu@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0-canary.66.tgz#cd3683bf569c66444340b1e4d876913584e93aea" + integrity sha512-kUPejaStjKpF79fz4525DKQKADtUuE+T6j7IvLQsZuWrSX3a5Mix+i52fdTzMJ+sFGg3v147wopZt6L6JMIxxA== + +"@next/swc-linux-arm64-musl@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0-canary.66.tgz#453836b11efdb50b91cf8a6cfbce8779f6778dd9" + integrity sha512-U8l8jaZ+BAU5wn3bw7PRqq4vGTpObBt+7JbJLpbDqB1GfkZdCDc4nGtqAfLy3pY0O4lEfqal9jrsEVtUBCbfHg== + +"@next/swc-linux-x64-gnu@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0-canary.66.tgz#f02b295febaacf8d041f9f149c30c41aea16a81f" + integrity sha512-c+AV8ZN1znGBHu5BACGym+9FhV8+213XVHFI7i2J7TSsJ6T+Eofhwn0tSn1Vy/XpVmpyoEdkwepL+djbdQAGhw== + +"@next/swc-linux-x64-musl@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0-canary.66.tgz#f942c000ba3ffc0289520d25fa1067a75e72fa41" + integrity sha512-4mTIv86qyXuo8NfjigSQ7rk2cDwM8/f8R/kf3hNh8NF1Aaat2RrEet9a/SbsuNpdhhNnPI5RcRwpIJx2JQSPcQ== + +"@next/swc-win32-arm64-msvc@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0-canary.66.tgz#0ffdcf5c74b5aa6214307f2ae4aa84f1526e6bf9" + integrity sha512-NPnfsDQXk44h8VtncWq2AgLjHDbUMsc8Tpz7pcLe9qb8lZSxZ9jYbV7NwKzgd+qJbjy/58vgCWhL5PhyXDlWwQ== + +"@next/swc-win32-x64-msvc@15.2.0-canary.66": + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0-canary.66.tgz#458245850cf407d2551155e4662785c109f58bda" + integrity sha512-L/ef++GJqW+T3g2x6mrZ2vrBK+6QS9Ieam8EqK9dG7cFKv7Gqm9yrHvDuVse62hnNB11ZdxfDDKrs9vabuQLlw== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -716,12 +716,12 @@ resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/helpers@0.5.13": - version "0.5.13" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.13.tgz#33e63ff3cd0cade557672bd7888a39ce7d115a8c" - integrity sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w== +"@swc/helpers@0.5.15": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== dependencies: - tslib "^2.4.0" + tslib "^2.8.0" "@types/json5@^0.0.29": version "0.0.29" @@ -2682,27 +2682,27 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -next@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/next/-/next-15.0.1.tgz#a0e8eda35d803cb7f8092b2a2eb9d072e22bf21d" - integrity sha512-PSkFkr/w7UnFWm+EP8y/QpHrJXMqpZzAXpergB/EqLPOh4SGPJXv1wj4mslr2hUZBAS9pX7/9YLIdxTv6fwytw== +next@^15.2.0-canary.64: + version "15.2.0-canary.66" + resolved "https://registry.yarnpkg.com/next/-/next-15.2.0-canary.66.tgz#cb5ee4453c88f247b6e74fe33fd181eca58e7c86" + integrity sha512-S+gsEu8vxxejI7nKqtCLqZlTi9L40xelLRK/Fgtvm/XT8W8ziLp3KMtN4I9Si5nEMU5uv7bllIfd04kVX8+HIw== dependencies: - "@next/env" "15.0.1" + "@next/env" "15.2.0-canary.66" "@swc/counter" "0.1.3" - "@swc/helpers" "0.5.13" + "@swc/helpers" "0.5.15" busboy "1.6.0" caniuse-lite "^1.0.30001579" postcss "8.4.31" styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "15.0.1" - "@next/swc-darwin-x64" "15.0.1" - "@next/swc-linux-arm64-gnu" "15.0.1" - "@next/swc-linux-arm64-musl" "15.0.1" - "@next/swc-linux-x64-gnu" "15.0.1" - "@next/swc-linux-x64-musl" "15.0.1" - "@next/swc-win32-arm64-msvc" "15.0.1" - "@next/swc-win32-x64-msvc" "15.0.1" + "@next/swc-darwin-arm64" "15.2.0-canary.66" + "@next/swc-darwin-x64" "15.2.0-canary.66" + "@next/swc-linux-arm64-gnu" "15.2.0-canary.66" + "@next/swc-linux-arm64-musl" "15.2.0-canary.66" + "@next/swc-linux-x64-gnu" "15.2.0-canary.66" + "@next/swc-linux-x64-musl" "15.2.0-canary.66" + "@next/swc-win32-arm64-msvc" "15.2.0-canary.66" + "@next/swc-win32-x64-msvc" "15.2.0-canary.66" sharp "^0.33.5" node-releases@^2.0.18: @@ -3559,6 +3559,11 @@ tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" diff --git a/compiler/package.json b/compiler/package.json index b16756f0aa2c0..f1696b9b2a5c5 100644 --- a/compiler/package.json +++ b/compiler/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@tsconfig/strictest": "^2.0.5", "concurrently": "^7.4.0", - "esbuild": "^0.24.2", + "esbuild": "^0.25.0", "folder-hash": "^4.0.4", "npm-dts": "^1.3.13", "object-assign": "^4.1.1", diff --git a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts index c648c66043980..aa49bda22b27e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts @@ -6,12 +6,15 @@ */ import type * as BabelCore from '@babel/core'; -import {compileProgram, parsePluginOptions} from '../Entrypoint'; +import {compileProgram, Logger, parsePluginOptions} from '../Entrypoint'; import { injectReanimatedFlag, pipelineUsesReanimatedPlugin, } from '../Entrypoint/Reanimated'; +const ENABLE_REACT_COMPILER_TIMINGS = + process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1'; + /* * The React Forget Babel Plugin * @param {*} _babel @@ -28,35 +31,65 @@ export default function BabelPluginReactCompiler( * prior to B, if A does not have a Program visitor and B does, B will run first. We always * want Forget to run true to source as possible. */ - Program(prog, pass): void { - let opts = parsePluginOptions(pass.opts); - const isDev = - (typeof __DEV__ !== 'undefined' && __DEV__ === true) || - process.env['NODE_ENV'] === 'development'; - if ( - opts.enableReanimatedCheck === true && - pipelineUsesReanimatedPlugin(pass.file.opts.plugins) - ) { - opts = injectReanimatedFlag(opts); - } - if ( - opts.environment.enableResetCacheOnSourceFileChanges !== false && - isDev - ) { - opts = { - ...opts, - environment: { - ...opts.environment, - enableResetCacheOnSourceFileChanges: true, - }, - }; - } - compileProgram(prog, { - opts, - filename: pass.filename ?? null, - comments: pass.file.ast.comments ?? [], - code: pass.file.code, - }); + Program: { + enter(prog, pass): void { + const filename = pass.filename ?? 'unknown'; + if (ENABLE_REACT_COMPILER_TIMINGS === true) { + performance.mark(`${filename}:start`, { + detail: 'BabelPlugin:Program:start', + }); + } + let opts = parsePluginOptions(pass.opts); + const isDev = + (typeof __DEV__ !== 'undefined' && __DEV__ === true) || + process.env['NODE_ENV'] === 'development'; + if ( + opts.enableReanimatedCheck === true && + pipelineUsesReanimatedPlugin(pass.file.opts.plugins) + ) { + opts = injectReanimatedFlag(opts); + } + if ( + opts.environment.enableResetCacheOnSourceFileChanges !== false && + isDev + ) { + opts = { + ...opts, + environment: { + ...opts.environment, + enableResetCacheOnSourceFileChanges: true, + }, + }; + } + compileProgram(prog, { + opts, + filename: pass.filename ?? null, + comments: pass.file.ast.comments ?? [], + code: pass.file.code, + }); + if (ENABLE_REACT_COMPILER_TIMINGS === true) { + performance.mark(`${filename}:end`, { + detail: 'BabelPlugin:Program:end', + }); + } + }, + exit(_, pass): void { + if (ENABLE_REACT_COMPILER_TIMINGS === true) { + const filename = pass.filename ?? 'unknown'; + const measurement = performance.measure(filename, { + start: `${filename}:start`, + end: `${filename}:end`, + detail: 'BabelPlugin:Program', + }); + if ('logger' in pass.opts && pass.opts.logger != null) { + const logger: Logger = pass.opts.logger as Logger; + logger.logEvent(filename, { + kind: 'Timing', + measurement, + }); + } + } + }, }, }, }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index fb951d25c5398..35c2c4134eb44 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -206,6 +206,10 @@ export type LoggerEvent = kind: 'PipelineError'; fnLoc: t.SourceLocation | null; data: string; + } + | { + kind: 'Timing'; + measurement: PerformanceMeasure; }; export type Logger = { 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 a354acf69274e..0953289c1950e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -162,7 +162,8 @@ function runWithEnvironment( if ( !env.config.enablePreserveExistingManualUseMemo && !env.config.disableMemoizationForDebugging && - !env.config.enableChangeDetectionForDebugging + !env.config.enableChangeDetectionForDebugging && + !env.config.enableMinimalTransformsForRetry ) { dropManualMemoization(hir); log({kind: 'hir', name: 'DropManualMemoization', value: hir}); @@ -279,8 +280,10 @@ function runWithEnvironment( value: hir, }); - inferReactiveScopeVariables(hir); - log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); + if (!env.config.enableMinimalTransformsForRetry) { + inferReactiveScopeVariables(hir); + log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir}); + } const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir); log({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 6ab9ee79c7412..787d9e7047efe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -16,6 +16,7 @@ import { EnvironmentConfig, ExternalFunction, ReactFunctionType, + MINIMAL_RETRY_CONFIG, tryParseExternalFunction, } from '../HIR/Environment'; import {CodegenFunction} from '../ReactiveScopes'; @@ -382,66 +383,92 @@ export function compileProgram( ); } - let compiledFn: CodegenFunction; - try { - /** - * Note that Babel does not attach comment nodes to nodes; they are dangling off of the - * Program node itself. We need to figure out whether an eslint suppression range - * applies to this function first. - */ - const suppressionsInFunction = filterSuppressionsThatAffectFunction( - suppressions, - fn, - ); - if (suppressionsInFunction.length > 0) { - const lintError = suppressionsToCompilerError(suppressionsInFunction); - if (optOutDirectives.length > 0) { - logError(lintError, pass, fn.node.loc ?? null); - } else { - handleError(lintError, pass, fn.node.loc ?? null); - } - return null; + /** + * Note that Babel does not attach comment nodes to nodes; they are dangling off of the + * Program node itself. We need to figure out whether an eslint suppression range + * applies to this function first. + */ + const suppressionsInFunction = filterSuppressionsThatAffectFunction( + suppressions, + fn, + ); + let compileResult: + | {kind: 'compile'; compiledFn: CodegenFunction} + | {kind: 'error'; error: unknown}; + if (suppressionsInFunction.length > 0) { + compileResult = { + kind: 'error', + error: suppressionsToCompilerError(suppressionsInFunction), + }; + } else { + try { + compileResult = { + kind: 'compile', + compiledFn: compileFn( + fn, + environment, + fnType, + useMemoCacheIdentifier.name, + pass.opts.logger, + pass.filename, + pass.code, + ), + }; + } catch (err) { + compileResult = {kind: 'error', error: err}; } - - compiledFn = compileFn( - fn, - environment, - fnType, - useMemoCacheIdentifier.name, - pass.opts.logger, - pass.filename, - pass.code, - ); - pass.opts.logger?.logEvent(pass.filename, { - kind: 'CompileSuccess', - fnLoc: fn.node.loc ?? null, - fnName: compiledFn.id?.name ?? null, - memoSlots: compiledFn.memoSlotsUsed, - memoBlocks: compiledFn.memoBlocks, - memoValues: compiledFn.memoValues, - prunedMemoBlocks: compiledFn.prunedMemoBlocks, - prunedMemoValues: compiledFn.prunedMemoValues, - }); - } catch (err) { + } + // If non-memoization features are enabled, retry regardless of error kind + if (compileResult.kind === 'error' && environment.enableFire) { + try { + compileResult = { + kind: 'compile', + compiledFn: compileFn( + fn, + { + ...environment, + ...MINIMAL_RETRY_CONFIG, + }, + fnType, + useMemoCacheIdentifier.name, + pass.opts.logger, + pass.filename, + pass.code, + ), + }; + } catch (err) { + compileResult = {kind: 'error', error: err}; + } + } + if (compileResult.kind === 'error') { /** * If an opt out directive is present, log only instead of throwing and don't mark as * containing a critical error. */ - if (fn.node.body.type === 'BlockStatement') { - if (optOutDirectives.length > 0) { - logError(err, pass, fn.node.loc ?? null); - return null; - } + if (optOutDirectives.length > 0) { + logError(compileResult.error, pass, fn.node.loc ?? null); + } else { + handleError(compileResult.error, pass, fn.node.loc ?? null); } - handleError(err, pass, fn.node.loc ?? null); return null; } + pass.opts.logger?.logEvent(pass.filename, { + kind: 'CompileSuccess', + fnLoc: fn.node.loc ?? null, + fnName: compileResult.compiledFn.id?.name ?? null, + memoSlots: compileResult.compiledFn.memoSlotsUsed, + memoBlocks: compileResult.compiledFn.memoBlocks, + memoValues: compileResult.compiledFn.memoValues, + prunedMemoBlocks: compileResult.compiledFn.prunedMemoBlocks, + prunedMemoValues: compileResult.compiledFn.prunedMemoValues, + }); + /** * Always compile functions with opt in directives. */ if (optInDirectives.length > 0) { - return compiledFn; + return compileResult.compiledFn; } else if (pass.opts.compilationMode === 'annotation') { /** * No opt-in directive in annotation mode, so don't insert the compiled function. @@ -467,7 +494,7 @@ export function compileProgram( } if (!pass.opts.noEmit) { - return compiledFn; + return compileResult.compiledFn; } return null; }; 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 4d9ce6becc17a..c60bbd0e1ba3d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -7,7 +7,6 @@ import {NodePath, Scope} from '@babel/traverse'; import * as t from '@babel/types'; -import {Expression} from '@babel/types'; import invariant from 'invariant'; import { CompilerError, @@ -37,12 +36,14 @@ import { ObjectProperty, ObjectPropertyKey, Place, + PropertyLiteral, ReturnTerminal, SourceLocation, SpreadPattern, ThrowTerminal, Type, makeInstructionId, + makePropertyLiteral, makeType, promoteTemporary, } from './HIR'; @@ -75,7 +76,7 @@ export function lower( parent: NodePath | null = null, ): Result { const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs); - const context: Array = []; + const context: HIRFunction['context'] = []; for (const ref of capturedRefs ?? []) { context.push({ @@ -2018,11 +2019,11 @@ function lowerExpression( }); // Save the result back to the property - if (typeof property === 'string') { + if (typeof property === 'string' || typeof property === 'number') { return { kind: 'PropertyStore', object: {...object}, - property, + property: makePropertyLiteral(property), value: {...newValuePlace}, loc: leftExpr.node.loc ?? GeneratedSource, }; @@ -2317,11 +2318,11 @@ function lowerExpression( const argument = expr.get('argument'); if (argument.isMemberExpression()) { const {object, property} = lowerMemberExpression(builder, argument); - if (typeof property === 'string') { + if (typeof property === 'string' || typeof property === 'number') { return { kind: 'PropertyDelete', object, - property, + property: makePropertyLiteral(property), loc: exprLoc, }; } else { @@ -2428,11 +2429,11 @@ function lowerExpression( // Save the result back to the property let newValuePlace; - if (typeof property === 'string') { + if (typeof property === 'string' || typeof property === 'number') { newValuePlace = lowerValueToTemporary(builder, { kind: 'PropertyStore', object: {...object}, - property, + property: makePropertyLiteral(property), value: {...updatedValue}, loc: leftExpr.node.loc ?? GeneratedSource, }); @@ -2528,6 +2529,7 @@ function lowerExpression( loc: expr.node.loc ?? GeneratedSource, }; } + case 'TSInstantiationExpression': case 'TSNonNullExpression': { let expr = exprPath as NodePath; return lowerExpression(builder, expr.get('expression')); @@ -3057,7 +3059,7 @@ function lowerArguments( type LoweredMemberExpression = { object: Place; - property: Place | string; + property: Place | string | number; value: InstructionValue; }; function lowerMemberExpression( @@ -3072,8 +3074,13 @@ function lowerMemberExpression( const object = loweredObject ?? lowerExpressionToTemporary(builder, objectNode); - if (!expr.node.computed) { - if (!propertyNode.isIdentifier()) { + if (!expr.node.computed || expr.node.property.type === 'NumericLiteral') { + let property: PropertyLiteral; + if (propertyNode.isIdentifier()) { + property = makePropertyLiteral(propertyNode.node.name); + } else if (propertyNode.isNumericLiteral()) { + property = makePropertyLiteral(propertyNode.node.value); + } else { builder.errors.push({ reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`, severity: ErrorSeverity.Todo, @@ -3089,10 +3096,10 @@ function lowerMemberExpression( const value: InstructionValue = { kind: 'PropertyLoad', object: {...object}, - property: propertyNode.node.name, + property, loc: exprLoc, }; - return {object, property: propertyNode.node.name, value}; + return {object, property, value}; } else { if (!propertyNode.isExpression()) { builder.errors.push({ @@ -3210,7 +3217,7 @@ function lowerJsxMemberExpression( return lowerValueToTemporary(builder, { kind: 'PropertyLoad', object: objectPlace, - property, + property: makePropertyLiteral(property), loc, }); } @@ -3377,7 +3384,7 @@ function lowerFunction( >, ): LoweredFunction | null { const componentScope: Scope = builder.parentFunction.scope; - const captured = gatherCapturedDeps(builder, expr, componentScope); + const capturedContext = gatherCapturedContext(expr, componentScope); /* * TODO(gsn): In the future, we could only pass in the context identifiers @@ -3391,7 +3398,7 @@ function lowerFunction( expr, builder.environment, builder.bindings, - [...builder.context, ...captured.identifiers], + [...builder.context, ...capturedContext], builder.parentFunction, ); let loweredFunc: HIRFunction; @@ -3404,7 +3411,6 @@ function lowerFunction( loweredFunc = lowering.unwrap(); return { func: loweredFunc, - dependencies: captured.refs, }; } @@ -3627,8 +3633,25 @@ function lowerAssignment( const lvalue = lvaluePath as NodePath; const property = lvalue.get('property'); const object = lowerExpressionToTemporary(builder, lvalue.get('object')); - if (!lvalue.node.computed) { - if (!property.isIdentifier()) { + if (!lvalue.node.computed || lvalue.get('property').isNumericLiteral()) { + let temporary; + if (property.isIdentifier()) { + temporary = lowerValueToTemporary(builder, { + kind: 'PropertyStore', + object, + property: makePropertyLiteral(property.node.name), + value, + loc, + }); + } else if (property.isNumericLiteral()) { + temporary = lowerValueToTemporary(builder, { + kind: 'PropertyStore', + object, + property: makePropertyLiteral(property.node.value), + value, + loc, + }); + } else { builder.errors.push({ reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`, severity: ErrorSeverity.Todo, @@ -3637,13 +3660,6 @@ function lowerAssignment( }); return {kind: 'UnsupportedNode', node: lvalueNode, loc}; } - const temporary = lowerValueToTemporary(builder, { - kind: 'PropertyStore', - object, - property: property.node.name, - value, - loc, - }); return {kind: 'LoadLocal', place: temporary, loc: temporary.loc}; } else { if (!property.isExpression()) { @@ -4078,14 +4094,6 @@ function lowerAssignment( } } -function isValidDependency(path: NodePath): boolean { - const parent: NodePath = path.parentPath; - return ( - !path.node.computed && - !(parent.isCallExpression() && parent.get('callee') === path) - ); -} - function captureScopes({from, to}: {from: Scope; to: Scope}): Set { let scopes: Set = new Set(); while (from) { @@ -4100,8 +4108,7 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set { return scopes; } -function gatherCapturedDeps( - builder: HIRBuilder, +function gatherCapturedContext( fn: NodePath< | t.FunctionExpression | t.ArrowFunctionExpression @@ -4109,10 +4116,8 @@ function gatherCapturedDeps( | t.ObjectMethod >, componentScope: Scope, -): {identifiers: Array; refs: Array} { - const capturedIds: Map = new Map(); - const capturedRefs: Set = new Set(); - const seenPaths: Set = new Set(); +): Array { + const capturedIds = new Set(); /* * Capture all the scopes from the parent of this function up to and including @@ -4123,33 +4128,11 @@ function gatherCapturedDeps( to: componentScope, }); - function addCapturedId(bindingIdentifier: t.Identifier): number { - if (!capturedIds.has(bindingIdentifier)) { - const index = capturedIds.size; - capturedIds.set(bindingIdentifier, index); - return index; - } else { - return capturedIds.get(bindingIdentifier)!; - } - } - function handleMaybeDependency( - path: - | NodePath - | NodePath - | NodePath, + path: NodePath | NodePath, ): void { // Base context variable to depend on let baseIdentifier: NodePath | NodePath; - /* - * Base expression to depend on, which (for now) may contain non side-effectful - * member expressions - */ - let dependency: - | NodePath - | NodePath - | NodePath - | NodePath; if (path.isJSXOpeningElement()) { const name = path.get('name'); if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) { @@ -4165,115 +4148,20 @@ function gatherCapturedDeps( 'Invalid logic in gatherCapturedDeps', ); baseIdentifier = current; - - /* - * Get the expression to depend on, which may involve PropertyLoads - * for member expressions - */ - let currentDep: - | NodePath - | NodePath - | NodePath = baseIdentifier; - - while (true) { - const nextDep: null | NodePath = currentDep.parentPath; - if (nextDep && nextDep.isJSXMemberExpression()) { - currentDep = nextDep; - } else { - break; - } - } - dependency = currentDep; - } else if (path.isMemberExpression()) { - // Calculate baseIdentifier - let currentId: NodePath = path; - while (currentId.isMemberExpression()) { - currentId = currentId.get('object'); - } - if (!currentId.isIdentifier()) { - return; - } - baseIdentifier = currentId; - - /* - * Get the expression to depend on, which may involve PropertyLoads - * for member expressions - */ - let currentDep: - | NodePath - | NodePath - | NodePath = baseIdentifier; - - while (true) { - const nextDep: null | NodePath = currentDep.parentPath; - if ( - nextDep && - nextDep.isMemberExpression() && - isValidDependency(nextDep) - ) { - currentDep = nextDep; - } else { - break; - } - } - - dependency = currentDep; } else { baseIdentifier = path; - dependency = path; } /* * Skip dependency path, as we already tried to recursively add it (+ all subexpressions) * as a dependency. */ - dependency.skip(); + path.skip(); // Add the base identifier binding as a dependency. const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name); - if (binding === undefined || !pureScopes.has(binding.scope)) { - return; - } - const idKey = String(addCapturedId(binding.identifier)); - - // Add the expression (potentially a memberexpr path) as a dependency. - let exprKey = idKey; - if (dependency.isMemberExpression()) { - let pathTokens = []; - let current: NodePath = dependency; - while (current.isMemberExpression()) { - const property = current.get('property') as NodePath; - pathTokens.push(property.node.name); - current = current.get('object'); - } - - exprKey += '.' + pathTokens.reverse().join('.'); - } else if (dependency.isJSXMemberExpression()) { - let pathTokens = []; - let current: NodePath = - dependency; - while (current.isJSXMemberExpression()) { - const property = current.get('property'); - pathTokens.push(property.node.name); - current = current.get('object'); - } - } - - if (!seenPaths.has(exprKey)) { - let loweredDep: Place; - if (dependency.isJSXIdentifier()) { - loweredDep = lowerValueToTemporary(builder, { - kind: 'LoadLocal', - place: lowerIdentifier(builder, dependency), - loc: path.node.loc ?? GeneratedSource, - }); - } else if (dependency.isJSXMemberExpression()) { - loweredDep = lowerJsxMemberExpression(builder, dependency); - } else { - loweredDep = lowerExpressionToTemporary(builder, dependency); - } - capturedRefs.add(loweredDep); - seenPaths.add(exprKey); + if (binding !== undefined && pureScopes.has(binding.scope)) { + capturedIds.add(binding.identifier); } } @@ -4304,13 +4192,13 @@ function gatherCapturedDeps( return; } else if (path.isJSXElement()) { handleMaybeDependency(path.get('openingElement')); - } else if (path.isMemberExpression() || path.isIdentifier()) { + } else if (path.isIdentifier()) { handleMaybeDependency(path); } }, }); - return {identifiers: [...capturedIds.keys()], refs: [...capturedRefs]}; + return [...capturedIds.keys()]; } function notNull(value: T | null): value is T { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts index d3c919a6d8afe..7b352696860e8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -18,6 +18,7 @@ import { IdentifierId, InstructionId, InstructionValue, + PropertyLiteral, ReactiveScopeDependency, ScopeId, } from './HIR'; @@ -131,15 +132,7 @@ function collectHoistablePropertyLoadsImpl( fn: HIRFunction, context: CollectHoistablePropertyLoadsContext, ): ReadonlyMap { - const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn); - const actuallyEvaluatedTemporaries = new Map( - [...context.temporaries].filter(([id]) => !functionExpressionLoads.has(id)), - ); - - const nodes = collectNonNullsInBlocks(fn, { - ...context, - temporaries: actuallyEvaluatedTemporaries, - }); + const nodes = collectNonNullsInBlocks(fn, context); propagateNonNull(fn, nodes, context.registry); if (DEBUG_PRINT) { @@ -180,8 +173,8 @@ export type BlockInfo = { * and make computing sets intersections simpler. */ type RootNode = { - properties: Map; - optionalProperties: Map; + properties: Map; + optionalProperties: Map; parent: null; // Recorded to make later computations simpler fullPath: ReactiveScopeDependency; @@ -191,8 +184,8 @@ type RootNode = { type PropertyPathNode = | { - properties: Map; - optionalProperties: Map; + properties: Map; + optionalProperties: Map; parent: PropertyPathNode; fullPath: ReactiveScopeDependency; hasOptional: boolean; @@ -598,30 +591,3 @@ function reduceMaybeOptionalChains( } } while (changed); } - -function collectFunctionExpressionFakeLoads( - fn: HIRFunction, -): Set { - const sources = new Map(); - const functionExpressionReferences = new Set(); - - for (const [_, block] of fn.body.blocks) { - for (const {lvalue, value} of block.instructions) { - if ( - value.kind === 'FunctionExpression' || - value.kind === 'ObjectMethod' - ) { - for (const reference of value.loweredFunc.dependencies) { - let curr: IdentifierId | undefined = reference.identifier.id; - while (curr != null) { - functionExpressionReferences.add(curr); - curr = sources.get(curr); - } - } - } else if (value.kind === 'PropertyLoad') { - sources.set(lvalue.identifier.id, value.object.identifier.id); - } - } - } - return functionExpressionReferences; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts index 2b7c9f2134e71..02a28b9f35776 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts @@ -16,6 +16,7 @@ import { DependencyPathEntry, Instruction, Terminal, + PropertyLiteral, } from './HIR'; import {printIdentifier} from './PrintHIR'; @@ -157,7 +158,7 @@ function matchOptionalTestBlock( blocks: ReadonlyMap, ): { consequentId: IdentifierId; - property: string; + property: PropertyLiteral; propertyId: IdentifierId; storeLocalInstr: Instruction; consequentGoto: BlockId; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts index f5567b3e536d1..7f6fb9e88f817 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts @@ -10,10 +10,10 @@ import { DependencyPathEntry, GeneratedSource, Identifier, + PropertyLiteral, ReactiveScopeDependency, } from '../HIR'; import {printIdentifier} from '../HIR/PrintHIR'; -import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies'; /** * Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR @@ -91,7 +91,7 @@ export class ReactiveScopeDependencyTreeHIR { * dependency. This effectively truncates @param dep to its maximal * safe-to-evaluate subpath */ - addDependency(dep: ReactiveScopePropertyDependency): void { + addDependency(dep: ReactiveScopeDependency): void { const {identifier, path} = dep; let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot( identifier, @@ -287,7 +287,7 @@ function merge( } type TreeNode = { - properties: Map>; + properties: Map>; accessType: T; }; type HoistableNode = TreeNode<'Optional' | 'NonNull'>; @@ -344,7 +344,7 @@ function printSubtree( function makeOrMergeProperty( node: DependencyNode, - property: string, + property: PropertyLiteral, accessType: PropertyAccessType, ): DependencyNode { let child = node.properties.get(property); 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 9df34242ec089..8de9218a662e2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -245,8 +245,6 @@ const EnvironmentConfigSchema = z.object({ */ enableUseTypeAnnotations: z.boolean().default(false), - enableFunctionDependencyRewrite: 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 @@ -552,6 +550,8 @@ const EnvironmentConfigSchema = z.object({ */ disableMemoizationForDebugging: z.boolean().default(false), + enableMinimalTransformsForRetry: z.boolean().default(false), + /** * When true, rather using memoized values, the compiler will always re-compute * values, and then use a heuristic to compare the memoized value to the newly @@ -626,6 +626,17 @@ const EnvironmentConfigSchema = z.object({ export type EnvironmentConfig = z.infer; +export const MINIMAL_RETRY_CONFIG: PartialEnvironmentConfig = { + validateHooksUsage: false, + validateRefAccessDuringRender: false, + validateNoSetStateInRender: false, + validateNoSetStateInPassiveEffects: false, + validateNoJSXInTryStatements: false, + validateMemoizedEffectDependencies: false, + validateNoCapitalizedCalls: null, + validateBlocklistedImports: null, + enableMinimalTransformsForRetry: true, +}; /** * For test fixtures and playground only. * 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 a6a9cbcd48ec5..1ca00ef5877c4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -108,16 +108,7 @@ export type ReactiveValue = | ReactiveLogicalValue | ReactiveSequenceValue | ReactiveTernaryValue - | ReactiveOptionalCallValue - | ReactiveFunctionValue; - -export type ReactiveFunctionValue = { - kind: 'ReactiveFunctionValue'; - fn: ReactiveFunction; - dependencies: Array; - returnType: t.FlowType | t.TSType | null; - loc: SourceLocation; -}; + | ReactiveOptionalCallValue; export type ReactiveLogicalValue = { kind: 'LogicalExpression'; @@ -722,7 +713,6 @@ export type ObjectProperty = { }; export type LoweredFunction = { - dependencies: Array; func: HIRFunction; }; @@ -947,7 +937,7 @@ export type InstructionValue = | { kind: 'PropertyStore'; object: Place; - property: string; + property: PropertyLiteral; value: Place; loc: SourceLocation; } @@ -957,7 +947,7 @@ export type InstructionValue = | { kind: 'PropertyDelete'; object: Place; - property: string; + property: PropertyLiteral; loc: SourceLocation; } @@ -1131,7 +1121,7 @@ export type StoreLocal = { export type PropertyLoad = { kind: 'PropertyLoad'; object: Place; - property: string; + property: PropertyLiteral; loc: SourceLocation; }; @@ -1512,7 +1502,17 @@ export type ReactiveScopeDeclaration = { scope: ReactiveScope; // the scope in which the variable was originally declared }; -export type DependencyPathEntry = {property: string; optional: boolean}; +const opaquePropertyLiteral = Symbol(); +export type PropertyLiteral = (string | number) & { + [opaquePropertyLiteral]: 'PropertyLiteral'; +}; +export function makePropertyLiteral(value: string | number): PropertyLiteral { + return value as PropertyLiteral; +} +export type DependencyPathEntry = { + property: PropertyLiteral; + optional: boolean; +}; export type DependencyPath = Array; export type ReactiveScopeDependency = { identifier: Identifier; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts index a3740539b295b..5b286e917d0d3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/MergeOverlappingReactiveScopesHIR.ts @@ -148,6 +148,14 @@ function collectScopeInfo(fn: HIRFunction): ScopeInfo { const scope = place.identifier.scope; if (scope != null) { placeScopes.set(place, scope); + /** + * Record both mutating and non-mutating scopes to merge scopes with + * still-mutating values with inner scopes that alias those values + * (see `nonmutating-capture-in-unsplittable-memo-block`) + * + * Note that this isn't perfect, as it also leads to merging of mutating + * scopes with JSX single-instruction scopes (see `mutation-within-jsx`) + */ if (scope.range.start !== scope.range.end) { getOrInsertDefault(scopeStarts, scope.range.start, new Set()).add( scope, @@ -254,7 +262,7 @@ function visitPlace( * of the stack to the mutated outer scope. */ const placeScope = getPlaceScope(id, place); - if (placeScope != null && isMutable({id} as any, place)) { + if (placeScope != null && isMutable({id}, place)) { const placeScopeIdx = activeScopes.indexOf(placeScope); if (placeScopeIdx !== -1 && placeScopeIdx !== activeScopes.length - 1) { joined.union([placeScope, ...activeScopes.slice(placeScopeIdx + 1)]); @@ -275,6 +283,13 @@ function getOverlappingReactiveScopes( for (const instr of block.instructions) { visitInstructionId(instr.id, context, state); for (const place of eachInstructionOperand(instr)) { + if ( + (instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod') && + place.identifier.type.kind === 'Primitive' + ) { + continue; + } visitPlace(instr.id, place, state); } for (const place of eachInstructionLValue(instr)) { 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 a6f6c606e118d..19d3f707b9199 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts @@ -6,7 +6,6 @@ */ import generate from '@babel/generator'; -import {printReactiveFunction} from '..'; import {CompilerError} from '../CompilerError'; import {printReactiveScopeSummary} from '../ReactiveScopes/PrintReactiveFunction'; import DisjointSet from '../Utils/DisjointSet'; @@ -538,9 +537,6 @@ export function printInstructionValue(instrValue: ReactiveValue): string { .split('\n') .map(line => ` ${line}`) .join('\n'); - const deps = instrValue.loweredFunc.dependencies - .map(dep => printPlace(dep)) - .join(','); const context = instrValue.loweredFunc.func.context .map(dep => printPlace(dep)) .join(','); @@ -557,7 +553,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string { }) .join(', ') ?? ''; const type = printType(instrValue.loweredFunc.func.returnType).trim(); - value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`; + value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`; break; } case 'TaggedTemplateExpression': { @@ -704,10 +700,6 @@ export function printInstructionValue(instrValue: ReactiveValue): string { value = `FinishMemoize decl=${printPlace(instrValue.decl)}`; break; } - case 'ReactiveFunctionValue': { - value = `FunctionValue ${printReactiveFunction(instrValue.fn)}`; - break; - } default: { assertExhaustive( instrValue, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts index 08856e9143139..e12d10b40612b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -22,6 +22,7 @@ import { TInstruction, FunctionExpression, ObjectMethod, + PropertyLiteral, } from './HIR'; import { collectHoistablePropertyLoads, @@ -321,7 +322,7 @@ function collectTemporariesSidemapImpl( function getProperty( object: Place, - propertyName: string, + propertyName: PropertyLiteral, optional: boolean, temporaries: ReadonlyMap, ): ReactiveScopeDependency { @@ -519,7 +520,11 @@ class Context { ); } - visitProperty(object: Place, property: string, optional: boolean): void { + visitProperty( + object: Place, + property: PropertyLiteral, + optional: boolean, + ): void { const nextDependency = getProperty( object, property, @@ -738,9 +743,8 @@ function collectDependencies( } for (const instr of block.instructions) { if ( - fn.env.config.enableFunctionDependencyRewrite && - (instr.value.kind === 'FunctionExpression' || - instr.value.kind === 'ObjectMethod') + instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod' ) { context.declare(instr.lvalue.identifier, { id: instr.id, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts index 55cd1531aee93..a67b580a6728b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Types.ts @@ -6,6 +6,7 @@ */ import {CompilerError} from '../CompilerError'; +import {PropertyLiteral} from './HIR'; export type BuiltInType = PrimitiveType | FunctionType | ObjectType; @@ -59,7 +60,7 @@ export type PropType = { kind: 'Property'; objectType: Type; objectName: string; - propertyName: string; + propertyName: PropertyLiteral; }; export type ObjectMethod = { 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 c9ee803bfaffd..49ff3c256e016 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -193,7 +193,7 @@ export function* eachInstructionValueOperand( } case 'ObjectMethod': case 'FunctionExpression': { - yield* instrValue.loweredFunc.dependencies; + yield* instrValue.loweredFunc.func.context; break; } case 'TaggedTemplateExpression': { @@ -517,8 +517,9 @@ export function mapInstructionValueOperands( } case 'ObjectMethod': case 'FunctionExpression': { - instrValue.loweredFunc.dependencies = - instrValue.loweredFunc.dependencies.map(d => fn(d)); + instrValue.loweredFunc.func.context = + instrValue.loweredFunc.func.context.map(d => fn(d)); + break; } case 'TaggedTemplateExpression': { 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 75bd8f6811ffb..a439b4cd01232 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts @@ -10,91 +10,32 @@ import { Effect, HIRFunction, Identifier, - IdentifierName, LoweredFunction, - Place, isRefOrRefValue, makeInstructionId, } from '../HIR'; import {deadCodeElimination} from '../Optimization'; import {inferReactiveScopeVariables} from '../ReactiveScopes'; import {rewriteInstructionKindsBasedOnReassignment} from '../SSA'; -import {inferMutableContextVariables} from './InferMutableContextVariables'; import {inferMutableRanges} from './InferMutableRanges'; import inferReferenceEffects from './InferReferenceEffects'; -type Dependency = { - identifier: Identifier; - path: Array; -}; - -// Helper class to track indirections such as LoadLocal and PropertyLoad. -export class IdentifierState { - properties: Map = new Map(); - - resolve(identifier: Identifier): Identifier { - const resolved = this.properties.get(identifier); - if (resolved !== undefined) { - return resolved.identifier; - } - return identifier; - } - - declareProperty(lvalue: Place, object: Place, property: string): void { - const objectDependency = this.properties.get(object.identifier); - let nextDependency: Dependency; - if (objectDependency === undefined) { - nextDependency = {identifier: object.identifier, path: [property]}; - } else { - nextDependency = { - identifier: objectDependency.identifier, - path: [...objectDependency.path, property], - }; - } - this.properties.set(lvalue.identifier, nextDependency); - } - - declareTemporary(lvalue: Place, value: Place): void { - const resolved: Dependency = this.properties.get(value.identifier) ?? { - identifier: value.identifier, - path: [], - }; - this.properties.set(lvalue.identifier, resolved); - } -} - export default function analyseFunctions(func: HIRFunction): void { - const state = new IdentifierState(); - for (const [_, block] of func.body.blocks) { for (const instr of block.instructions) { switch (instr.value.kind) { case 'ObjectMethod': case 'FunctionExpression': { lower(instr.value.loweredFunc.func); - infer(instr.value.loweredFunc, state, func.context); - break; - } - case 'PropertyLoad': { - state.declareProperty( - instr.lvalue, - instr.value.object, - instr.value.property, - ); - break; - } - case 'ComputedLoad': { - /* - * The path is set to an empty string as the path doesn't really - * matter for a computed load. + infer(instr.value.loweredFunc); + + /** + * Reset mutable range for outer inferReferenceEffects */ - state.declareProperty(instr.lvalue, instr.value.object, ''); - break; - } - case 'LoadLocal': - case 'LoadContext': { - if (instr.lvalue.identifier.name === null) { - state.declareTemporary(instr.lvalue, instr.value.place); + for (const operand of instr.value.loweredFunc.func.context) { + operand.identifier.mutableRange.start = makeInstructionId(0); + operand.identifier.mutableRange.end = makeInstructionId(0); + operand.identifier.scope = null; } break; } @@ -110,7 +51,6 @@ function lower(func: HIRFunction): void { inferMutableRanges(func); rewriteInstructionKindsBasedOnReassignment(func); inferReactiveScopeVariables(func); - inferMutableContextVariables(func); func.env.logger?.debugLogIRs?.({ kind: 'hir', name: 'AnalyseFunction (inner)', @@ -118,32 +58,15 @@ function lower(func: HIRFunction): void { }); } -function infer( - loweredFunc: LoweredFunction, - state: IdentifierState, - context: Array, -): void { - const mutations = new Map(); +function infer(loweredFunc: LoweredFunction): void { for (const operand of loweredFunc.func.context) { - if ( - isMutatedOrReassigned(operand.identifier) && - operand.identifier.name !== null - ) { - mutations.set(operand.identifier.name.value, operand.effect); - } - } - - for (const dep of loweredFunc.dependencies) { - let name: IdentifierName | null = null; - - if (state.properties.has(dep.identifier)) { - const receiver = state.properties.get(dep.identifier)!; - name = receiver.identifier.name; - } else { - name = dep.identifier.name; - } - - if (isRefOrRefValue(dep.identifier)) { + const identifier = operand.identifier; + CompilerError.invariant(operand.effect === Effect.Unknown, { + reason: + '[AnalyseFunctions] Expected Function context effects to not have been set', + loc: operand.loc, + }); + if (isRefOrRefValue(identifier)) { /* * TODO: this is a hack to ensure we treat functions which reference refs * as having a capture and therefore being considered mutable. this ensures @@ -151,43 +74,17 @@ function infer( * could be called, and allows us to help ensure it isn't called during * render */ - dep.effect = Effect.Capture; - } else if (name !== null) { - const effect = mutations.get(name.value); - if (effect !== undefined) { - dep.effect = effect === Effect.Unknown ? Effect.Capture : effect; - } - } - } - - /* - * This could potentially add duplicate deps to mutatedDeps in the case of - * mutating a context ref in the child function and in this parent function. - * It might be useful to dedupe this. - * - * In practice this never really matters because the Component function has no - * context refs, so it will never have duplicate deps. - */ - for (const place of context) { - CompilerError.invariant(place.identifier.name !== null, { - reason: 'context refs should always have a name', - description: null, - loc: place.loc, - suggestions: null, - }); - - const effect = mutations.get(place.identifier.name.value); - if (effect !== undefined) { - place.effect = effect === Effect.Unknown ? Effect.Capture : effect; - loweredFunc.dependencies.push(place); + operand.effect = Effect.Capture; + } else if (isMutatedOrReassigned(identifier)) { + /** + * Reflects direct reassignments, PropertyStores, and ConditionallyMutate + * (directly or through maybe-aliases) + */ + operand.effect = Effect.Capture; + } else { + operand.effect = Effect.Read; } } - - for (const operand of loweredFunc.func.context) { - operand.identifier.mutableRange.start = makeInstructionId(0); - operand.identifier.mutableRange.end = makeInstructionId(0); - operand.identifier.scope = null; - } } function isMutatedOrReassigned(id: Identifier): boolean { 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 4dcdc21e15ac5..8d123845c3739 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts @@ -145,9 +145,10 @@ function collectTemporaries( } case 'PropertyLoad': { if (sidemap.react.has(value.object.identifier.id)) { - if (value.property === 'useMemo' || value.property === 'useCallback') { + const property = value.property; + if (property === 'useMemo' || property === 'useCallback') { sidemap.manualMemos.set(instr.lvalue.identifier.id, { - kind: value.property, + kind: property as 'useMemo' | 'useCallback', loadInstr: instr as TInstruction, }); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableContextVariables.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableContextVariables.ts deleted file mode 100644 index 67babf43db22f..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableContextVariables.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {Effect, HIRFunction, Identifier, Place} from '../HIR'; -import { - eachInstructionValueOperand, - eachTerminalOperand, -} from '../HIR/visitors'; -import {IdentifierState} from './AnalyseFunctions'; - -/* - * This pass infers which of the given function's context (free) variables - * are definitively mutated by the function. This analysis is *partial*, - * and only annotates provable mutations, and may miss mutations via indirections. - * The intent of this pass is to drive validations, rejecting known-bad code - * while avoiding false negatives, and the inference should *not* be used to - * drive changes in output. - * - * Note that a complete analysis is possible but would have too many false negatives. - * The approach would be to run LeaveSSA and InferReactiveScopeVariables in order to - * find all possible aliases of a context variable which may be mutated. However, this - * can lead to false negatives: - * - * ``` - * const [x, setX] = useState(null); // x is frozen - * const fn = () => { // context=[x] - * const z = {}; // z is mutable - * foo(z, x); // potentially mutate z and x - * z.a = true; // definitively mutate z - * } - * fn(); - * ``` - * - * When we analyze function expressions we assume that context variables are mutable, - * so we assume that `x` is mutable. We infer that `foo(z, x)` could be mutating the - * two variables to alias each other, such that `z.a = true` could be mutating `x`, - * and we would infer that `x` is definitively mutated. Then when we run - * InferReferenceEffects on the outer code we'd reject it, since there is a definitive - * mutation of a frozen value. - * - * Thus the actual implementation looks at only basic aliasing. The above example would - * pass, since z does not directly alias `x`. However, mutations through trivial aliases - * are detected: - * - * ``` - * const [x, setX] = useState(null); // x is frozen - * const fn = () => { // context=[x] - * const z = x; - * z.a = true; // ERROR: mutates x - * } - * fn(); - * ``` - */ -export function inferMutableContextVariables(fn: HIRFunction): void { - const state = new IdentifierState(); - const knownMutatedIdentifiers = new Set(); - for (const [, block] of fn.body.blocks) { - for (const instr of block.instructions) { - switch (instr.value.kind) { - case 'PropertyLoad': { - state.declareProperty( - instr.lvalue, - instr.value.object, - instr.value.property, - ); - break; - } - case 'ComputedLoad': { - /* - * The path is set to an empty string as the path doesn't really - * matter for a computed load. - */ - state.declareProperty(instr.lvalue, instr.value.object, ''); - break; - } - case 'LoadLocal': - case 'LoadContext': { - if (instr.lvalue.identifier.name === null) { - state.declareTemporary(instr.lvalue, instr.value.place); - } - break; - } - default: { - for (const operand of eachInstructionValueOperand(instr.value)) { - visitOperand(state, knownMutatedIdentifiers, operand); - } - } - } - } - for (const operand of eachTerminalOperand(block.terminal)) { - visitOperand(state, knownMutatedIdentifiers, operand); - } - } - for (const operand of fn.context) { - if (knownMutatedIdentifiers.has(operand.identifier)) { - operand.effect = Effect.Mutate; - } - } -} - -function visitOperand( - state: IdentifierState, - knownMutatedIdentifiers: Set, - operand: Place, -): void { - const resolved = state.resolve(operand.identifier); - if (operand.effect === Effect.Mutate || operand.effect === Effect.Store) { - knownMutatedIdentifiers.add(resolved); - } -} 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 70395e58d930e..e6883250705d4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -241,7 +241,7 @@ export default function inferReferenceEffects( if (options.isFunctionExpression) { fn.effects = functionEffects; - } else { + } else if (!fn.env.config.enableMinimalTransformsForRetry) { raiseFunctionEffectErrors(functionEffects); } } @@ -390,29 +390,31 @@ class InferenceState { freezeValues(values: Set, reason: Set): void { for (const value of values) { + if (value.kind === 'DeclareContext') { + /** + * Avoid freezing hoisted context declarations + * function Component() { + * const cb = useBar(() => foo(2)); // produces a hoisted context declaration + * const foo = useFoo(); // reassigns to the context variable + * return ; + * } + */ + continue; + } this.#values.set(value, { kind: ValueKind.Frozen, reason, context: new Set(), }); - if (value.kind === 'FunctionExpression') { - if ( - this.#env.config.enablePreserveExistingMemoizationGuarantees || - this.#env.config.enableTransitivelyFreezeFunctionExpressions - ) { - if (value.kind === 'FunctionExpression') { - /* - * We want to freeze the captured values, not mark the operands - * themselves as frozen. There could be mutations that occur - * before the freeze we are processing, and it would be invalid - * to overwrite those mutations as a freeze. - */ - for (const operand of eachInstructionValueOperand(value)) { - const operandValues = this.#variables.get(operand.identifier.id); - if (operandValues !== undefined) { - this.freezeValues(operandValues, reason); - } - } + if ( + value.kind === 'FunctionExpression' && + (this.#env.config.enablePreserveExistingMemoizationGuarantees || + this.#env.config.enableTransitivelyFreezeFunctionExpressions) + ) { + for (const operand of value.loweredFunc.func.context) { + const operandValues = this.#variables.get(operand.identifier.id); + if (operandValues !== undefined) { + this.freezeValues(operandValues, reason); } } } @@ -1143,17 +1145,17 @@ function inferBlock( case 'ObjectMethod': case 'FunctionExpression': { let hasMutableOperand = false; - const mutableOperands: Array = []; for (const operand of eachInstructionOperand(instr)) { + CompilerError.invariant(operand.effect !== Effect.Unknown, { + reason: 'Expected fn effects to be populated', + loc: operand.loc, + }); state.referenceAndRecordEffects( freezeActions, operand, - operand.effect === Effect.Unknown ? Effect.Read : operand.effect, + operand.effect, ValueReason.Other, ); - if (isMutableEffect(operand.effect, operand.loc)) { - mutableOperands.push(operand); - } hasMutableOperand ||= isMutableEffect(operand.effect, operand.loc); } /* @@ -1596,18 +1598,14 @@ function inferBlock( break; } case 'LoadLocal': { + /** + * Due to backedges in the CFG, we may revisit LoadLocal lvalues + * multiple times. Unlike StoreLocal which may reassign to existing + * identifiers, LoadLocal always evaluates to store a new temporary. + * This means that we should always model LoadLocal as a Capture effect + * on the rvalue. + */ const lvalue = instr.lvalue; - CompilerError.invariant( - !( - state.isDefined(lvalue) && - state.kind(lvalue).kind === ValueKind.Context - ), - { - reason: - '[InferReferenceEffects] Unexpected LoadLocal with context kind', - loc: lvalue.loc, - }, - ); state.referenceAndRecordEffects( freezeActions, instrValue.place, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts index a9f62c1986efa..07cd419230ac4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/ConstantPropagation.ts @@ -19,6 +19,7 @@ import { Primitive, assertConsistentIdentifiers, assertTerminalSuccessorsExist, + makePropertyLiteral, markInstructionIds, markPredecessors, mergeConsecutiveBlocks, @@ -238,13 +239,14 @@ function evaluateInstruction( if ( property !== null && property.kind === 'Primitive' && - typeof property.value === 'string' && - isValidIdentifier(property.value) + ((typeof property.value === 'string' && + isValidIdentifier(property.value)) || + typeof property.value === 'number') ) { const nextValue: InstructionValue = { kind: 'PropertyLoad', loc: value.loc, - property: property.value, + property: makePropertyLiteral(property.value), object: value.object, }; instr.value = nextValue; @@ -256,13 +258,14 @@ function evaluateInstruction( if ( property !== null && property.kind === 'Primitive' && - typeof property.value === 'string' && - isValidIdentifier(property.value) + ((typeof property.value === 'string' && + isValidIdentifier(property.value)) || + typeof property.value === 'number') ) { const nextValue: InstructionValue = { kind: 'PropertyStore', loc: value.loc, - property: property.value, + property: makePropertyLiteral(property.value), object: value.object, value: value.value, }; 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 9ffc64864f397..29c59c7b3644a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts @@ -21,6 +21,7 @@ import { InstructionKind, JsxAttribute, makeInstructionId, + makePropertyLiteral, ObjectProperty, Phi, Place, @@ -446,7 +447,7 @@ function createSymbolProperty( value: { kind: 'PropertyLoad', object: {...symbolInstruction.lvalue}, - property: 'for', + property: makePropertyLiteral('for'), loc: instr.value.loc, }, loc: instr.loc, 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 e27b8f952148a..b636c7b1718cc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts @@ -23,6 +23,7 @@ import { isUseContextHookType, makeBlockId, makeInstructionId, + makePropertyLiteral, makeType, markInstructionIds, promoteTemporary, @@ -195,7 +196,7 @@ function emitPropertyLoad( const loadProp: PropertyLoad = { kind: 'PropertyLoad', object, - property, + property: makePropertyLiteral(property), loc: GeneratedSource, }; const element: Place = createTemporaryPlace(env, GeneratedSource); @@ -270,7 +271,6 @@ function emitSelectorFn(env: Environment, keys: Array): Instruction { name: null, loweredFunc: { func: fn, - dependencies: [], }, type: 'ArrowFunctionExpression', loc: GeneratedSource, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts index 7a1473be40c87..0e6d1fd59201f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineFunctions.ts @@ -24,7 +24,6 @@ export function outlineFunctions( } if ( value.kind === 'FunctionExpression' && - value.loweredFunc.dependencies.length === 0 && value.loweredFunc.func.context.length === 0 && // TODO: handle outlining named functions value.loweredFunc.func.id === null && 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 cf40d86abc552..6c89aea45ea45 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1453,15 +1453,20 @@ function codegenDependency( if (dependency.path.length !== 0) { const hasOptional = dependency.path.some(path => path.optional); for (const path of dependency.path) { + const property = + typeof path.property === 'string' + ? t.identifier(path.property) + : t.numericLiteral(path.property); + const isComputed = typeof path.property !== 'string'; if (hasOptional) { object = t.optionalMemberExpression( object, - t.identifier(path.property), - false, + property, + isComputed, path.optional, ); } else { - object = t.memberExpression(object, t.identifier(path.property)); + object = t.memberExpression(object, property, isComputed); } } } @@ -1962,38 +1967,37 @@ function codegenInstructionValue( value = node; break; } - case 'PropertyStore': { - value = t.assignmentExpression( - '=', - t.memberExpression( - codegenPlaceToExpression(cx, instrValue.object), - t.identifier(instrValue.property), - ), - codegenPlaceToExpression(cx, instrValue.value), - ); - break; - } - case 'PropertyLoad': { - const object = codegenPlaceToExpression(cx, instrValue.object); + case 'PropertyStore': + case 'PropertyLoad': + case 'PropertyDelete': { + let memberExpr; /* * We currently only lower single chains of optional memberexpr. * (See BuildHIR.ts for more detail.) */ - value = t.memberExpression( - object, - t.identifier(instrValue.property), - undefined, - ); - break; - } - case 'PropertyDelete': { - value = t.unaryExpression( - 'delete', - t.memberExpression( + if (typeof instrValue.property === 'string') { + memberExpr = t.memberExpression( codegenPlaceToExpression(cx, instrValue.object), t.identifier(instrValue.property), - ), - ); + ); + } else { + memberExpr = t.memberExpression( + codegenPlaceToExpression(cx, instrValue.object), + t.numericLiteral(instrValue.property), + true, + ); + } + if (instrValue.kind === 'PropertyStore') { + value = t.assignmentExpression( + '=', + memberExpr, + codegenPlaceToExpression(cx, instrValue.value), + ); + } else if (instrValue.kind === 'PropertyLoad') { + value = memberExpr; + } else { + value = t.unaryExpression('delete', memberExpr); + } break; } case 'ComputedStore': { @@ -2231,7 +2235,6 @@ function codegenInstructionValue( ); break; } - case 'ReactiveFunctionValue': case 'StartMemoize': case 'FinishMemoize': case 'Debugger': diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts deleted file mode 100644 index c7e16cce7ac86..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts +++ /dev/null @@ -1,639 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import {CompilerError} from '../CompilerError'; -import {DependencyPath, Identifier, ReactiveScopeDependency} from '../HIR'; -import {printIdentifier} from '../HIR/PrintHIR'; -import {assertExhaustive} from '../Utils/utils'; - -/* - * We need to understand optional member expressions only when determining - * dependencies of a ReactiveScope (i.e. in {@link PropagateScopeDependencies}), - * hence why this type lives here (not in HIR.ts) - */ -export type ReactiveScopePropertyDependency = ReactiveScopeDependency; - -/* - * Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional - * dependencies, preserving granular accesses when possible. - * - * Correctness properties: - * - All dependencies to a ReactiveBlock must be tracked. - * We can always truncate a dependency's path to a subpath, due to Forget assuming - * deep immutability. If the value produced by a subpath has not changed, then - * dependency must have not changed. - * i.e. props.a === $[..] implies props.a.b === $[..] - * - * Note the inverse is not true, but this only means a false positive (we run the - * reactive block more than needed). - * i.e. props.a !== $[..] does not imply props.a.b !== $[..] - * - * - The dependencies of a finalized ReactiveBlock must be all safe to access - * unconditionally (i.e. preserve program semantics with respect to nullthrows). - * If a dependency is only accessed within a conditional, we must track the nearest - * unconditionally accessed subpath instead. - * @param initialDeps - * @returns - */ -export class ReactiveScopeDependencyTree { - #roots: Map = new Map(); - - #getOrCreateRoot(identifier: Identifier): DependencyNode { - // roots can always be accessed unconditionally in JS - let rootNode = this.#roots.get(identifier); - - if (rootNode === undefined) { - rootNode = { - properties: new Map(), - accessType: PropertyAccessType.UnconditionalAccess, - }; - this.#roots.set(identifier, rootNode); - } - return rootNode; - } - - add(dep: ReactiveScopePropertyDependency, inConditional: boolean): void { - const {path} = dep; - let currNode = this.#getOrCreateRoot(dep.identifier); - - for (const item of path) { - // all properties read 'on the way' to a dependency are marked as 'access' - let currChild = getOrMakeProperty(currNode, item.property); - const accessType = inConditional - ? PropertyAccessType.ConditionalAccess - : item.optional - ? PropertyAccessType.OptionalAccess - : PropertyAccessType.UnconditionalAccess; - currChild.accessType = merge(currChild.accessType, accessType); - currNode = currChild; - } - - /** - * The final property node should be marked as an conditional/unconditional - * `dependency` as based on control flow. - */ - const depType = inConditional - ? PropertyAccessType.ConditionalDependency - : isOptional(currNode.accessType) - ? PropertyAccessType.OptionalDependency - : PropertyAccessType.UnconditionalDependency; - - currNode.accessType = merge(currNode.accessType, depType); - } - - deriveMinimalDependencies(): Set { - const results = new Set(); - for (const [rootId, rootNode] of this.#roots.entries()) { - const deps = deriveMinimalDependenciesInSubtree(rootNode, null); - CompilerError.invariant( - deps.every( - dep => - dep.accessType === PropertyAccessType.UnconditionalDependency || - dep.accessType == PropertyAccessType.OptionalDependency, - ), - { - reason: - '[PropagateScopeDependencies] All dependencies must be reduced to unconditional dependencies.', - description: null, - loc: null, - suggestions: null, - }, - ); - - for (const dep of deps) { - results.add({ - identifier: rootId, - path: dep.relativePath, - }); - } - } - - return results; - } - - addDepsFromInnerScope( - depsFromInnerScope: ReactiveScopeDependencyTree, - innerScopeInConditionalWithinParent: boolean, - checkValidDepIdFn: (dep: ReactiveScopeDependency) => boolean, - ): void { - for (const [id, otherRoot] of depsFromInnerScope.#roots) { - if (!checkValidDepIdFn({identifier: id, path: []})) { - continue; - } - let currRoot = this.#getOrCreateRoot(id); - addSubtree(currRoot, otherRoot, innerScopeInConditionalWithinParent); - if (!isUnconditional(currRoot.accessType)) { - currRoot.accessType = isDependency(currRoot.accessType) - ? PropertyAccessType.UnconditionalDependency - : PropertyAccessType.UnconditionalAccess; - } - } - } - - promoteDepsFromExhaustiveConditionals( - trees: Array, - ): void { - CompilerError.invariant(trees.length > 1, { - reason: 'Expected trees to be at least 2 elements long.', - description: null, - loc: null, - suggestions: null, - }); - - for (const [id, root] of this.#roots) { - const nodesForRootId = mapNonNull(trees, tree => { - const node = tree.#roots.get(id); - if (node != null && isUnconditional(node.accessType)) { - return node; - } else { - return null; - } - }); - if (nodesForRootId) { - addSubtreeIntersection( - root.properties, - nodesForRootId.map(root => root.properties), - ); - } - } - } - - /* - * Prints dependency tree to string for debugging. - * @param includeAccesses - * @returns string representation of DependencyTree - */ - printDeps(includeAccesses: boolean): string { - let res = []; - - for (const [rootId, rootNode] of this.#roots.entries()) { - const rootResults = printSubtree(rootNode, includeAccesses).map( - result => `${printIdentifier(rootId)}.${result}`, - ); - res.push(rootResults); - } - return res.flat().join('\n'); - } - - debug(): string { - const buf: Array = [`tree() [`]; - for (const [rootId, rootNode] of this.#roots) { - buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`); - this.#debugImpl(buf, rootNode, 1); - } - buf.push(']'); - return buf.length > 2 ? buf.join('\n') : buf.join(''); - } - - #debugImpl( - buf: Array, - node: DependencyNode, - depth: number = 0, - ): void { - for (const [property, childNode] of node.properties) { - buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`); - this.#debugImpl(buf, childNode, depth + 1); - } - } -} - -/* - * Enum representing the access type of single property on a parent object. - * We distinguish on two independent axes: - * Conditional / Unconditional: - * - whether this property is accessed unconditionally (within the ReactiveBlock) - * Access / Dependency: - * - Access: this property is read on the path of a dependency. We do not - * need to track change variables for accessed properties. Tracking accesses - * helps Forget do more granular dependency tracking. - * - Dependency: this property is read as a dependency and we must track changes - * to it for correctness. - * - * ```javascript - * // props.a is a dependency here and must be tracked - * deps: {props.a, props.a.b} ---> minimalDeps: {props.a} - * // props.a is just an access here and does not need to be tracked - * deps: {props.a.b} ---> minimalDeps: {props.a.b} - * ``` - */ -enum PropertyAccessType { - ConditionalAccess = 'ConditionalAccess', - OptionalAccess = 'OptionalAccess', - UnconditionalAccess = 'UnconditionalAccess', - ConditionalDependency = 'ConditionalDependency', - OptionalDependency = 'OptionalDependency', - UnconditionalDependency = 'UnconditionalDependency', -} - -const MIN_ACCESS_TYPE = PropertyAccessType.ConditionalAccess; -function isUnconditional(access: PropertyAccessType): boolean { - return ( - access === PropertyAccessType.UnconditionalAccess || - access === PropertyAccessType.UnconditionalDependency - ); -} -function isDependency(access: PropertyAccessType): boolean { - return ( - access === PropertyAccessType.ConditionalDependency || - access === PropertyAccessType.OptionalDependency || - access === PropertyAccessType.UnconditionalDependency - ); -} -function isOptional(access: PropertyAccessType): boolean { - return ( - access === PropertyAccessType.OptionalAccess || - access === PropertyAccessType.OptionalDependency - ); -} - -function merge( - access1: PropertyAccessType, - access2: PropertyAccessType, -): PropertyAccessType { - const resultIsUnconditional = - isUnconditional(access1) || isUnconditional(access2); - const resultIsDependency = isDependency(access1) || isDependency(access2); - const resultIsOptional = isOptional(access1) || isOptional(access2); - - /* - * Straightforward merge. - * This can be represented as bitwise OR, but is written out for readability - * - * Observe that `UnconditionalAccess | ConditionalDependency` produces an - * unconditionally accessed conditional dependency. We currently use these - * as we use unconditional dependencies. (i.e. to codegen change variables) - */ - if (resultIsUnconditional) { - if (resultIsDependency) { - return PropertyAccessType.UnconditionalDependency; - } else { - return PropertyAccessType.UnconditionalAccess; - } - } else if (resultIsOptional) { - if (resultIsDependency) { - return PropertyAccessType.OptionalDependency; - } else { - return PropertyAccessType.OptionalAccess; - } - } else { - if (resultIsDependency) { - return PropertyAccessType.ConditionalDependency; - } else { - return PropertyAccessType.ConditionalAccess; - } - } -} - -type DependencyNode = { - properties: Map; - accessType: PropertyAccessType; -}; - -type ReduceResultNode = { - relativePath: DependencyPath; - accessType: PropertyAccessType; -}; - -function promoteResult( - accessType: PropertyAccessType, - path: {property: string; optional: boolean} | null, -): Array { - const result: ReduceResultNode = { - relativePath: [], - accessType, - }; - if (path !== null) { - result.relativePath.push(path); - } - return [result]; -} - -function prependPath( - results: Array, - path: {property: string; optional: boolean} | null, -): Array { - if (path === null) { - return results; - } - return results.map(result => { - return { - accessType: result.accessType, - relativePath: [path, ...result.relativePath], - }; - }); -} - -/* - * Recursively calculates minimal dependencies in a subtree. - * @param dep DependencyNode representing a dependency subtree. - * @returns a minimal list of dependencies in this subtree. - */ -function deriveMinimalDependenciesInSubtree( - dep: DependencyNode, - property: string | null, -): Array { - const results: Array = []; - for (const [childName, childNode] of dep.properties) { - const childResult = deriveMinimalDependenciesInSubtree( - childNode, - childName, - ); - results.push(...childResult); - } - - switch (dep.accessType) { - case PropertyAccessType.UnconditionalDependency: { - return promoteResult( - PropertyAccessType.UnconditionalDependency, - property !== null ? {property, optional: false} : null, - ); - } - case PropertyAccessType.UnconditionalAccess: { - if ( - results.every( - ({accessType}) => - accessType === PropertyAccessType.UnconditionalDependency || - accessType === PropertyAccessType.OptionalDependency, - ) - ) { - // all children are unconditional dependencies, return them to preserve granularity - return prependPath( - results, - property !== null ? {property, optional: false} : null, - ); - } else { - /* - * at least one child is accessed conditionally, so this node needs to be promoted to - * unconditional dependency - */ - return promoteResult( - PropertyAccessType.UnconditionalDependency, - property !== null ? {property, optional: false} : null, - ); - } - } - case PropertyAccessType.OptionalDependency: { - return promoteResult( - PropertyAccessType.OptionalDependency, - property !== null ? {property, optional: true} : null, - ); - } - case PropertyAccessType.OptionalAccess: { - if ( - results.every( - ({accessType}) => - accessType === PropertyAccessType.UnconditionalDependency || - accessType === PropertyAccessType.OptionalDependency, - ) - ) { - // all children are unconditional dependencies, return them to preserve granularity - return prependPath( - results, - property !== null ? {property, optional: true} : null, - ); - } else { - /* - * at least one child is accessed conditionally, so this node needs to be promoted to - * unconditional dependency - */ - return promoteResult( - PropertyAccessType.OptionalDependency, - property !== null ? {property, optional: true} : null, - ); - } - } - case PropertyAccessType.ConditionalAccess: - case PropertyAccessType.ConditionalDependency: { - if ( - results.every( - ({accessType}) => - accessType === PropertyAccessType.ConditionalDependency, - ) - ) { - /* - * No children are accessed unconditionally, so we cannot promote this node to - * unconditional access. - * Truncate results of child nodes here, since we shouldn't access them anyways - */ - return promoteResult( - PropertyAccessType.ConditionalDependency, - property !== null ? {property, optional: true} : null, - ); - } else { - /* - * at least one child is accessed unconditionally, so this node can be promoted to - * unconditional dependency - */ - return promoteResult( - PropertyAccessType.UnconditionalDependency, - property !== null ? {property, optional: true} : null, - ); - } - } - default: { - assertExhaustive( - dep.accessType, - '[PropgateScopeDependencies] Unhandled access type!', - ); - } - } -} - -/* - * Demote all unconditional accesses + dependencies in subtree to the - * conditional equivalent, mutating subtree in place. - * @param subtree unconditional node representing a subtree of dependencies - */ -function demoteSubtreeToConditional(subtree: DependencyNode): void { - const stack: Array = [subtree]; - - let node; - while ((node = stack.pop()) !== undefined) { - const {accessType, properties} = node; - if (!isUnconditional(accessType)) { - // A conditionally accessed node should not have unconditional children - continue; - } - node.accessType = isDependency(accessType) - ? PropertyAccessType.ConditionalDependency - : PropertyAccessType.ConditionalAccess; - - for (const childNode of properties.values()) { - if (isUnconditional(accessType)) { - /* - * No conditional node can have an unconditional node as a child, so - * we only process childNode if it is unconditional - */ - stack.push(childNode); - } - } - } -} - -/* - * Calculates currNode = union(currNode, otherNode), mutating currNode in place - * If demoteOtherNode is specified, we demote the subtree represented by - * otherNode to conditional access/deps before taking the union. - * - * This is a helper function used to join an inner scope to its parent scope. - * @param currNode (mutable) return by argument - * @param otherNode (move) {@link addSubtree} takes ownership of the subtree - * represented by otherNode, which may be mutated or moved to currNode. It is - * invalid to use otherNode after this call. - * - * Note that @param otherNode may contain both conditional and unconditional nodes, - * due to inner control flow and conditional member expressions - * - * @param demoteOtherNode - */ -function addSubtree( - currNode: DependencyNode, - otherNode: DependencyNode, - demoteOtherNode: boolean, -): void { - let otherType = otherNode.accessType; - if (demoteOtherNode) { - otherType = isDependency(otherType) - ? PropertyAccessType.ConditionalDependency - : PropertyAccessType.ConditionalAccess; - } - currNode.accessType = merge(currNode.accessType, otherType); - - for (const [propertyName, otherChild] of otherNode.properties) { - const currChild = currNode.properties.get(propertyName); - if (currChild) { - // recursively calculate currChild = union(currChild, otherChild) - addSubtree(currChild, otherChild, demoteOtherNode); - } else { - /* - * if currChild doesn't exist, we can just move otherChild - * currChild = otherChild. - */ - if (demoteOtherNode) { - demoteSubtreeToConditional(otherChild); - } - currNode.properties.set(propertyName, otherChild); - } - } -} - -/* - * Adds intersection(otherProperties) to currProperties, mutating - * currProperties in place. i.e. - * currProperties = union(currProperties, intersection(otherProperties)) - * - * Used to merge unconditional accesses from exhaustive conditional branches - * into the parent ReactiveDeps Tree. - * intersection(currProperties) is determined as such: - * - a node is present in the intersection iff it is present in all every - * branch - * - the type of an added node is `UnconditionalDependency` if it is a - * dependency in at least one branch (otherwise `UnconditionalAccess`) - * - * @param otherProperties (read-only) an array of node properties containing - * conditionally and unconditionally accessed nodes. Each element - * represents asubtree of reactive dependencies from a single CFG - * branch. - * otherProperties must represent all reachable branches. - * @param currProperties (mutable) return by argument properties of a node - * - * otherProperties and currProperties must be properties of disjoint nodes - * that represent the same dependency (identifier + path). - */ -function addSubtreeIntersection( - currProperties: Map, - otherProperties: Array>, -): void { - CompilerError.invariant(otherProperties.length > 1, { - reason: - '[DeriveMinimalDependencies] Expected otherProperties to be at least 2 elements long.', - description: null, - loc: null, - suggestions: null, - }); - - /* - * otherProperties here may contain unconditional nodes as the result of - * recursively merging exhaustively conditional children with unconditionally - * accessed nodes (e.g. in the test condition itself) - * See `reduce-reactive-cond-deps-cfg-nested-testifelse` fixture for example - */ - - for (const [propertyName, currNode] of currProperties) { - const otherNodes = mapNonNull(otherProperties, properties => { - const node = properties.get(propertyName); - if (node != null && isUnconditional(node.accessType)) { - return node; - } else { - return null; - } - }); - - /* - * intersection(otherNodes[propertyName]) only exists if each element in - * otherProperties accesses propertyName. - */ - if (otherNodes) { - addSubtreeIntersection( - currNode.properties, - otherNodes.map(node => node.properties), - ); - - const isDep = otherNodes.some(tree => isDependency(tree.accessType)); - const externalAccessType = isDep - ? PropertyAccessType.UnconditionalDependency - : PropertyAccessType.UnconditionalAccess; - currNode.accessType = merge(externalAccessType, currNode.accessType); - } - } -} - -function printSubtree( - node: DependencyNode, - includeAccesses: boolean, -): Array { - const results: Array = []; - for (const [propertyName, propertyNode] of node.properties) { - if (includeAccesses || isDependency(propertyNode.accessType)) { - results.push(`${propertyName} (${propertyNode.accessType})`); - } - const propertyResults = printSubtree(propertyNode, includeAccesses); - results.push(...propertyResults.map(result => `${propertyName}.${result}`)); - } - return results; -} - -function getOrMakeProperty( - node: DependencyNode, - property: string, -): DependencyNode { - let child = node.properties.get(property); - if (child == null) { - child = { - properties: new Map(), - accessType: MIN_ACCESS_TYPE, - }; - node.properties.set(property, child); - } - return child; -} - -function mapNonNull, V, U>( - arr: Array, - fn: (arg0: U) => T | undefined | null, -): Array | null { - const result = []; - for (let i = 0; i < arr.length; i++) { - const element = fn(arr[i]); - if (element) { - result.push(element); - } else { - return null; - } - } - return result; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts index 1108422f070d7..1f104d8592fab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -379,6 +379,14 @@ export function findDisjointMutableValues( */ operand.identifier.mutableRange.start > 0 ) { + if ( + instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod' + ) { + if (operand.identifier.type.kind === 'Primitive') { + continue; + } + } operands.push(operand.identifier); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts index b5aa44ead095d..259fc06486888 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction.ts @@ -394,7 +394,10 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void { break; } default: - assertExhaustive(terminal, `Unhandled terminal ${terminal}`); + assertExhaustive( + terminal, + `Unhandled terminal kind \`${(terminal as any).kind}\``, + ); } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts index b8ba19628451b..522aaf5a5df28 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateEarlyReturns.ts @@ -17,6 +17,7 @@ import { ReactiveStatement, ReactiveTerminalStatement, makeInstructionId, + makePropertyLiteral, promoteTemporary, } from '../HIR'; import {createTemporaryPlace} from '../HIR/HIRBuilder'; @@ -189,7 +190,7 @@ class Transform extends ReactiveFunctionTransform { value: { kind: 'PropertyLoad', object: {...symbolTemp}, - property: 'for', + property: makePropertyLiteral('for'), loc, }, }, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts index 2a9d0b9793d9f..9ef9d382c2834 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneInitializationDependencies.ts @@ -12,6 +12,7 @@ import { IdentifierId, InstructionId, Place, + PropertyLiteral, ReactiveBlock, ReactiveFunction, ReactiveInstruction, @@ -64,13 +65,13 @@ type KindMap = Map; class Visitor extends ReactiveFunctionVisitor { map: KindMap = new Map(); aliases: DisjointSet; - paths: Map>; + paths: Map>; env: Environment; constructor( env: Environment, aliases: DisjointSet, - paths: Map>, + paths: Map>, ) { super(); this.aliases = aliases; @@ -218,9 +219,9 @@ export default function pruneInitializationDependencies( } function update( - map: Map>, + map: Map>, key: IdentifierId, - path: string, + path: PropertyLiteral, value: IdentifierId, ): void { const inner = map.get(key) ?? new Map(); @@ -230,7 +231,7 @@ function update( class AliasVisitor extends ReactiveFunctionVisitor { scopeIdentifiers: DisjointSet = new DisjointSet(); - scopePaths: Map> = new Map(); + scopePaths: Map> = new Map(); override visitInstruction(instr: ReactiveInstruction): void { if ( @@ -271,11 +272,14 @@ class AliasVisitor extends ReactiveFunctionVisitor { function getAliases( fn: ReactiveFunction, -): [DisjointSet, Map>] { +): [ + DisjointSet, + Map>, +] { const visitor = new AliasVisitor(); visitReactiveFunction(fn, visitor, null); let disjoint = visitor.scopeIdentifiers; - let scopePaths = new Map>(); + let scopePaths = new Map>(); for (const [key, value] of visitor.scopePaths) { for (const [path, id] of value) { update( 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 5a9aa6b2a7368..441d529b39616 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -772,14 +772,6 @@ function computeMemoizationInputs( rvalues: operands, }; } - case 'ReactiveFunctionValue': { - CompilerError.invariant(false, { - reason: `Unexpected ReactiveFunctionValue node`, - description: null, - loc: value.loc, - suggestions: null, - }); - } case 'UnsupportedNode': { CompilerError.invariant(false, { reason: `Unexpected unsupported node`, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/visitors.ts index 487de2767a516..4ad05aa302ac1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/visitors.ts @@ -73,15 +73,6 @@ export class ReactiveFunctionVisitor { this.visitValue(value.id, value.value, state); break; } - case 'ReactiveFunctionValue': { - this.visitReactiveFunctionValue( - id, - value.dependencies, - value.fn, - state, - ); - break; - } default: { for (const place of eachInstructionValueOperand(value)) { this.visitPlace(id, place, state); @@ -434,18 +425,6 @@ export class ReactiveFunctionTransform< } break; } - case 'ReactiveFunctionValue': { - const nextValue = this.transformReactiveFunctionValue( - id, - value.dependencies, - value.fn, - state, - ); - if (nextValue.kind === 'replace') { - value.fn = nextValue.value; - } - break; - } default: { for (const place of eachInstructionValueOperand(value)) { this.visitPlace(id, place, state); @@ -619,10 +598,6 @@ export function* eachReactiveValueOperand( yield* eachReactiveValueOperand(instrValue.alternate); break; } - case 'ReactiveFunctionValue': { - yield* instrValue.dependencies; - break; - } default: { yield* eachInstructionValueOperand(instrValue); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/SSA/EliminateRedundantPhi.ts b/compiler/packages/babel-plugin-react-compiler/src/SSA/EliminateRedundantPhi.ts index bae038f9bd9df..12c8c0e2e6221 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/SSA/EliminateRedundantPhi.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/SSA/EliminateRedundantPhi.ts @@ -13,6 +13,8 @@ import { eachTerminalOperand, } from '../HIR/visitors'; +const DEBUG = false; + /* * Pass to eliminate redundant phi nodes: * - all operands are the same identifier, ie `x2 = phi(x1, x1, x1)`. @@ -141,6 +143,23 @@ export function eliminateRedundantPhi( * have already propagated forwards since we visit in reverse postorder. */ } while (rewrites.size > size && hasBackEdge); + + if (DEBUG) { + for (const [, block] of ir.blocks) { + for (const phi of block.phis) { + CompilerError.invariant(!rewrites.has(phi.place.identifier), { + reason: '[EliminateRedundantPhis]: rewrite not complete', + loc: phi.place.loc, + }); + for (const [, operand] of phi.operands) { + CompilerError.invariant(!rewrites.has(operand.identifier), { + reason: '[EliminateRedundantPhis]: rewrite not complete', + loc: phi.place.loc, + }); + } + } + } + } } function rewritePlace( diff --git a/compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts b/compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts index caba0d3c36992..820f7388dc41b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/SSA/EnterSSA.ts @@ -301,9 +301,6 @@ function enterSSAImpl( entry.preds.add(blockId); builder.defineFunction(loweredFunc); builder.enter(() => { - loweredFunc.context = loweredFunc.context.map(p => - builder.getPlace(p), - ); loweredFunc.params = loweredFunc.params.map(param => { if (param.kind === 'Identifier') { return builder.definePlace(param); 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 a35c4ddb0182c..a480b5d7c78de 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts @@ -319,51 +319,6 @@ function visitFunctionExpressionAndPropagateFireDependencies( replaceFireFunctions(fnExpr.loweredFunc.func, context), ); - /* - * Make a mapping from each dependency to the corresponding LoadLocal for it so that - * we can replace the loaded place with the generated fire function binding - */ - const loadLocalsToDepLoads = new Map(); - for (const dep of fnExpr.loweredFunc.dependencies) { - const loadLocal = context.getLoadLocalInstr(dep.identifier.id); - if (loadLocal != null) { - loadLocalsToDepLoads.set(loadLocal.place.identifier.id, loadLocal); - } - } - - const replacedCallees = new Map(); - for (const [ - calleeIdentifierId, - loadedFireFunctionBindingPlace, - ] of calleesCapturedByFnExpression.entries()) { - /* - * Given the ids of captured fire callees, look at the deps for loads of those identifiers - * and replace them with the new fire function binding - */ - const loadLocal = loadLocalsToDepLoads.get(calleeIdentifierId); - if (loadLocal == null) { - context.pushError({ - loc: fnExpr.loc, - description: null, - severity: ErrorSeverity.Invariant, - reason: - '[InsertFire] No loadLocal found for fire call argument for lambda', - suggestions: null, - }); - continue; - } - - const oldPlaceId = loadLocal.place.identifier.id; - loadLocal.place = { - ...loadedFireFunctionBindingPlace.fireFunctionBinding, - }; - - replacedCallees.set( - oldPlaceId, - loadedFireFunctionBindingPlace.fireFunctionBinding, - ); - } - // For each replaced callee, update the context of the function expression to track it for ( let contextIdx = 0; @@ -371,9 +326,13 @@ function visitFunctionExpressionAndPropagateFireDependencies( contextIdx++ ) { const contextItem = fnExpr.loweredFunc.func.context[contextIdx]; - const replacedCallee = replacedCallees.get(contextItem.identifier.id); + const replacedCallee = calleesCapturedByFnExpression.get( + contextItem.identifier.id, + ); if (replacedCallee != null) { - fnExpr.loweredFunc.func.context[contextIdx] = replacedCallee; + fnExpr.loweredFunc.func.context[contextIdx] = { + ...replacedCallee.fireFunctionBinding, + }; } } 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 0270723c3e863..3054a83c76138 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -14,6 +14,7 @@ import { Identifier, IdentifierId, Instruction, + makePropertyLiteral, makeType, PropType, Type, @@ -335,7 +336,7 @@ function* generateInstructionTypes( kind: 'Property', objectType: value.value.identifier.type, objectName: getName(names, value.value.identifier.id), - propertyName, + propertyName: makePropertyLiteral(propertyName), }); } else { break; @@ -352,7 +353,7 @@ function* generateInstructionTypes( kind: 'Property', objectType: value.value.identifier.type, objectName: getName(names, value.value.identifier.id), - propertyName: property.key.name, + propertyName: makePropertyLiteral(property.key.name), }); } } @@ -420,7 +421,10 @@ function* generateInstructionTypes( break; } default: - assertExhaustive(value, `Unhandled instruction value kind: ${value}`); + assertExhaustive( + value, + `Unhandled instruction value kind: ${(value as any).kind}`, + ); } } @@ -450,10 +454,12 @@ class Unifier { return; } const objectType = this.get(tB.objectType); - const propertyType = this.env.getPropertyType( - objectType, - tB.propertyName, - ); + let propertyType; + if (typeof tB.propertyName === 'number') { + propertyType = null; + } else { + propertyType = this.env.getPropertyType(objectType, tB.propertyName); + } if (propertyType !== null) { this.unify(tA, propertyType); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts index 53640da5020bd..ed23693f61439 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateHooksUsage.ts @@ -257,7 +257,9 @@ export function validateHooksUsage(fn: HIRFunction): void { } case 'PropertyLoad': { const objectKind = getKindForPlace(instr.value.object); - const isHookProperty = isHookName(instr.value.property); + const isHookProperty = + typeof instr.value.property === 'string' && + isHookName(instr.value.property); let kind: Kind; switch (objectKind) { case Kind.Error: { diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts index 2988d1cc02430..2f9b0a1c7e2ce 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoCapitalizedCalls.ts @@ -61,7 +61,10 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void { } case 'PropertyLoad': { // Start conservative and disallow all capitalized method calls - if (/^[A-Z]/.test(value.property)) { + if ( + typeof value.property === 'string' && + /^[A-Z]/.test(value.property) + ) { capitalizedProperties.set(lvalue.identifier.id, value.property); } break; diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts index 4db8c700f387f..779207b22e646 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts @@ -285,7 +285,7 @@ function validateNoRefAccessInRenderImpl( } case 'ComputedLoad': case 'PropertyLoad': { - if (typeof instr.value.property !== 'string') { + if (instr.value.kind === 'ComputedLoad') { validateNoDirectRefValueAccess(errors, instr.value.property, env); } const objType = env.get(instr.value.object.identifier.id); 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 e7615320c7b95..28be6c166d2e4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts @@ -327,13 +327,6 @@ class Visitor extends ReactiveFunctionVisitor { case 'OptionalExpression': { return this.recordDepsInValue(value.value, state); } - case 'ReactiveFunctionValue': { - CompilerError.throwTodo({ - reason: - 'Handle ReactiveFunctionValue in ValidatePreserveManualMemoization', - loc: value.loc, - }); - } case 'ConditionalExpression': { this.recordDepsInValue(value.test, state); this.recordDepsInValue(value.consequent, state); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.expect.md new file mode 100644 index 0000000000000..b30ee7e0d6817 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.expect.md @@ -0,0 +1,120 @@ + +## Input + +```javascript +// @enableTransitivelyFreezeFunctionExpressions:false +import { + Stringify, + mutate, + identity, + setPropertyByKey, + shallowCopy, +} from 'shared-runtime'; +/** + * Function expression version of `aliased-nested-scope-truncated-dep`. + * + * In this fixture, the output would be invalid if propagateScopeDeps did not + * avoid adding MemberExpression dependencies which would other evaluate during + * the mutable ranges of their base objects. + * This is different from `aliased-nested-scope-truncated-dep` which *does* + * produce correct output regardless of MemberExpression dependency truncation. + * + * Note while other expressions evaluate inline, function expressions *always* + * represent deferred evaluation. This means that + * (1) it's always safe to reorder function expression creation until its + * earliest potential invocation + * (2) it's invalid to eagerly evaluate function expression dependencies during + * their respective mutable ranges. + */ + +function Component({prop}) { + let obj = shallowCopy(prop); + + const aliasedObj = identity(obj); + + // When `obj` is mutable (either directly or through aliases), taking a + // dependency on `obj.id` is invalid as it may change before getId() is invoked + const getId = () => obj.id; + + mutate(aliasedObj); + setPropertyByKey(aliasedObj, 'id', prop.id + 1); + + // Calling getId() should return prop.id + 1, not the prev + 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"; // @enableTransitivelyFreezeFunctionExpressions:false +import { + Stringify, + mutate, + identity, + setPropertyByKey, + shallowCopy, +} from "shared-runtime"; +/** + * Function expression version of `aliased-nested-scope-truncated-dep`. + * + * In this fixture, the output would be invalid if propagateScopeDeps did not + * avoid adding MemberExpression dependencies which would other evaluate during + * the mutable ranges of their base objects. + * This is different from `aliased-nested-scope-truncated-dep` which *does* + * produce correct output regardless of MemberExpression dependency truncation. + * + * Note while other expressions evaluate inline, function expressions *always* + * represent deferred evaluation. This means that + * (1) it's always safe to reorder function expression creation until its + * earliest potential invocation + * (2) it's invalid to eagerly evaluate function expression dependencies during + * their respective mutable ranges. + */ + +function Component(t0) { + const $ = _c(2); + const { prop } = t0; + let t1; + if ($[0] !== prop) { + const obj = shallowCopy(prop); + + const aliasedObj = identity(obj); + + const getId = () => 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)
{"getId":{"kind":"Function","result":2},"shouldInvokeFns":true}
+
{"getId":{"kind":"Function","result":2},"shouldInvokeFns":true}
+
{"getId":{"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/aliased-nested-scope-fn-expr.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.tsx new file mode 100644 index 0000000000000..40022c6f651fd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-fn-expr.tsx @@ -0,0 +1,46 @@ +// @enableTransitivelyFreezeFunctionExpressions:false +import { + Stringify, + mutate, + identity, + setPropertyByKey, + shallowCopy, +} from 'shared-runtime'; +/** + * Function expression version of `aliased-nested-scope-truncated-dep`. + * + * In this fixture, the output would be invalid if propagateScopeDeps did not + * avoid adding MemberExpression dependencies which would other evaluate during + * the mutable ranges of their base objects. + * This is different from `aliased-nested-scope-truncated-dep` which *does* + * produce correct output regardless of MemberExpression dependency truncation. + * + * Note while other expressions evaluate inline, function expressions *always* + * represent deferred evaluation. This means that + * (1) it's always safe to reorder function expression creation until its + * earliest potential invocation + * (2) it's invalid to eagerly evaluate function expression dependencies during + * their respective mutable ranges. + */ + +function Component({prop}) { + let obj = shallowCopy(prop); + + const aliasedObj = identity(obj); + + // When `obj` is mutable (either directly or through aliases), taking a + // dependency on `obj.id` is invalid as it may change before getId() is invoked + const getId = () => obj.id; + + mutate(aliasedObj); + setPropertyByKey(aliasedObj, 'id', prop.id + 1); + + // Calling getId() should return prop.id + 1, not the prev + 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/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 new file mode 100644 index 0000000000000..933fafff5f1ba --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.expect.md @@ -0,0 +1,221 @@ + +## Input + +```javascript +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"; +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(4); + 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; + + 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/aliased-nested-scope-truncated-dep.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.tsx new file mode 100644 index 0000000000000..4d9d7e78fb309 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/aliased-nested-scope-truncated-dep.tsx @@ -0,0 +1,93 @@ +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/allow-mutate-global-in-effect-fixpoint.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md index 942daec1dd08c..76e4432fe90d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.expect.md @@ -19,9 +19,14 @@ function Component() { // capture into a separate variable that is not a context variable. const y = x; + /** + * Note that this fixture currently produces a stale effect closure if `y = x + * = someGlobal` changes between renders. Under current compiler assumptions, + * that would be a rule of react violation. + */ useEffect(() => { y.value = 'hello'; - }, []); + }); useEffect(() => { setState(someGlobal.value); @@ -46,57 +51,50 @@ import { useEffect, useState } from "react"; let someGlobal = { value: null }; function Component() { - const $ = _c(7); + const $ = _c(5); const [state, setState] = useState(someGlobal); + + let x = someGlobal; + while (x == null) { + x = someGlobal; + } + + const y = x; let t0; - let t1; - let t2; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - let x = someGlobal; - while (x == null) { - x = someGlobal; - } - - const y = x; - t0 = useEffect; - t1 = () => { + t0 = () => { y.value = "hello"; }; - t2 = []; $[0] = t0; + } else { + t0 = $[0]; + } + useEffect(t0); + let t1; + let t2; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + setState(someGlobal.value); + }; + t2 = [someGlobal]; $[1] = t1; $[2] = t2; } else { - t0 = $[0]; t1 = $[1]; t2 = $[2]; } - t0(t1, t2); - let t3; + useEffect(t1, t2); + + const t3 = String(state); let t4; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { - t3 = () => { - setState(someGlobal.value); - }; - t4 = [someGlobal]; + if ($[3] !== t3) { + t4 =
{t3}
; $[3] = t3; $[4] = t4; } else { - t3 = $[3]; t4 = $[4]; } - useEffect(t3, t4); - - const t5 = String(state); - let t6; - if ($[5] !== t5) { - t6 =
{t5}
; - $[5] = t5; - $[6] = t6; - } else { - t6 = $[6]; - } - return t6; + return t4; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.js index 84bd42aaefd18..6e44adf204101 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutate-global-in-effect-fixpoint.js @@ -15,9 +15,14 @@ function Component() { // capture into a separate variable that is not a context variable. const y = x; + /** + * Note that this fixture currently produces a stale effect closure if `y = x + * = someGlobal` changes between renders. Under current compiler assumptions, + * that would be a rule of react violation. + */ useEffect(() => { y.value = 'hello'; - }, []); + }); useEffect(() => { setState(someGlobal.value); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md index 37a510b8c290a..3584faf699f86 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md @@ -44,48 +44,44 @@ import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRen import { useEffect, useRef, useState } from "react"; function Component() { - const $ = _c(6); + const $ = _c(5); const ref = useRef(null); const [state, setState] = useState(false); let t0; - let t1; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = () => {}; - - t1 = []; + t0 = []; $[0] = t0; - $[1] = t1; } else { t0 = $[0]; - t1 = $[1]; } - useEffect(t0, t1); + useEffect(_temp, t0); + let t1; let t2; - let t3; - if ($[2] === Symbol.for("react.memo_cache_sentinel")) { - t2 = () => { + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { setState(true); }; - t3 = []; + t2 = []; + $[1] = t1; $[2] = t2; - $[3] = t3; } else { + t1 = $[1]; t2 = $[2]; - t3 = $[3]; } - useEffect(t2, t3); + useEffect(t1, t2); - const t4 = String(state); - let t5; - if ($[4] !== t4) { - t5 = ; + const t3 = String(state); + let t4; + if ($[3] !== t3) { + t4 = ; + $[3] = t3; $[4] = t4; - $[5] = t5; } else { - t5 = $[5]; + t4 = $[4]; } - return t5; + return t4; } +function _temp() {} function Child(t0) { const { ref } = t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.expect.md index 7a3ddf443f43f..2d7bc3f046e45 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.expect.md @@ -2,22 +2,31 @@ ## Input ```javascript +import {makeArray} from 'shared-runtime'; + function Component(props) { - const x = foo(...props.a, null, ...props.b); + const x = makeArray(...props.a, null, ...props.b); return x; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: [1, 2], b: [2, 3, 4]}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; +import { makeArray } from "shared-runtime"; + function Component(props) { const $ = _c(3); let t0; if ($[0] !== props.a || $[1] !== props.b) { - t0 = foo(...props.a, null, ...props.b); + t0 = makeArray(...props.a, null, ...props.b); $[0] = props.a; $[1] = props.b; $[2] = t0; @@ -28,5 +37,12 @@ function Component(props) { return x; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: [1, 2], b: [2, 3, 4] }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) [1,2,null,2,3,4] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.js index 5e73773bd1d24..8b7767c3bf890 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/call-spread.js @@ -1,4 +1,11 @@ +import {makeArray} from 'shared-runtime'; + function Component(props) { - const x = foo(...props.a, null, ...props.b); + const x = makeArray(...props.a, null, ...props.b); return x; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: [1, 2], b: [2, 3, 4]}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md index f9ce3f2e98cf5..061e64222eef0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(foo, bar) { +import {mutate} from 'shared-runtime'; + +function Component({foo, bar}) { let x = {foo}; let y = {bar}; const f0 = function () { @@ -15,14 +17,28 @@ function component(foo, bar) { return x; } +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"; -function component(foo, bar) { +import { mutate } from "shared-runtime"; + +function Component(t0) { const $ = _c(3); + const { foo, bar } = t0; let x; if ($[0] !== bar || $[1] !== foo) { x = { foo }; @@ -44,5 +60,21 @@ function component(foo, bar) { return x; } +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 }, + ], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"foo":2} +{"foo":2} +{"foo":2} +{"foo":3} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.js index a83acfcdb0e46..8579e8c4943f6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-2.js @@ -1,4 +1,6 @@ -function component(foo, bar) { +import {mutate} from 'shared-runtime'; + +function Component({foo, bar}) { let x = {foo}; let y = {bar}; const f0 = function () { @@ -10,3 +12,14 @@ function component(foo, bar) { mutate(y); return x; } + +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/capturing-fun-alias-captured-mutate-arr-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md index 38590d1559bb5..fb44f2c7b8f24 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(foo, bar) { +import {mutate} from 'shared-runtime'; + +function Component({foo, bar}) { let x = {foo}; let y = {bar}; const f0 = function () { @@ -15,14 +17,28 @@ function component(foo, bar) { return x; } +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"; -function component(foo, bar) { +import { mutate } from "shared-runtime"; + +function Component(t0) { const $ = _c(3); + const { foo, bar } = t0; let x; if ($[0] !== bar || $[1] !== foo) { x = { foo }; @@ -44,5 +60,21 @@ function component(foo, bar) { return x; } +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 }, + ], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"foo":2} +{"foo":2} +{"foo":2} +{"foo":3} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.js index 2a30bfbce197d..24dddd4910e08 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-fun-alias-captured-mutate-arr-2.js @@ -1,4 +1,6 @@ -function component(foo, bar) { +import {mutate} from 'shared-runtime'; + +function Component({foo, bar}) { let x = {foo}; let y = {bar}; const f0 = function () { @@ -10,3 +12,14 @@ function component(foo, bar) { mutate(y); return x; } + +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/capturing-func-alias-captured-mutate-arr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md index 7c94c33e495a8..e9cb3c4fd145b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.expect.md @@ -2,7 +2,8 @@ ## Input ```javascript -function component(foo, bar) { +import {mutate} from 'shared-runtime'; +function Component({foo, bar}) { let x = {foo}; let y = {bar}; const f0 = function () { @@ -15,14 +16,27 @@ function component(foo, bar) { return y; } +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"; -function component(foo, bar) { +import { mutate } from "shared-runtime"; +function Component(t0) { const $ = _c(3); + const { foo, bar } = t0; let y; if ($[0] !== bar || $[1] !== foo) { const x = { foo }; @@ -44,5 +58,21 @@ function component(foo, bar) { return y; } +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 }, + ], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"bar":3,"wat0":"joe"} +{"bar":3,"wat0":"joe"} +{"bar":4,"wat0":"joe"} +{"bar":4,"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.js index 5b7665498dfd6..f757078e73266 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-captured-mutate-arr.js @@ -1,4 +1,5 @@ -function component(foo, bar) { +import {mutate} from 'shared-runtime'; +function Component({foo, bar}) { let x = {foo}; let y = {bar}; const f0 = function () { @@ -10,3 +11,14 @@ function component(foo, bar) { mutate(y); return y; } + +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/capturing-func-alias-computed-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.expect.md index 1a561453a915e..513718ba8606b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.expect.md @@ -2,7 +2,8 @@ ## Input ```javascript -function component(a) { +import {mutate} from 'shared-runtime'; +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -13,14 +14,22 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a) { +import { mutate } from "shared-runtime"; +function Component(t0) { const $ = _c(2); + const { a } = t0; let y; if ($[0] !== a) { const x = { a }; @@ -39,5 +48,15 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 2 }, { a: 3 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"x":{"a":2},"wat0":"joe"} +{"x":{"a":2},"wat0":"joe"} +{"x":{"a":3},"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.js index a66039c9ad283..daa4fffb28df8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-computed-mutate.js @@ -1,4 +1,5 @@ -function component(a) { +import {mutate} from 'shared-runtime'; +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -8,3 +9,9 @@ function component(a) { mutate(y); return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.expect.md index 386cbec42db64..ae145f4d9e2eb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.expect.md @@ -2,7 +2,8 @@ ## Input ```javascript -function component(a) { +import {mutate} from 'shared-runtime'; +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -13,14 +14,22 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a) { +import { mutate } from "shared-runtime"; +function Component(t0) { const $ = _c(2); + const { a } = t0; let y; if ($[0] !== a) { const x = { a }; @@ -39,5 +48,15 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 2 }, { a: 3 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"x":{"a":2},"wat0":"joe"} +{"x":{"a":2},"wat0":"joe"} +{"x":{"a":3},"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.js index f528a8c02bc9a..524dc8af35601 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-mutate.js @@ -1,4 +1,5 @@ -function component(a) { +import {mutate} from 'shared-runtime'; +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -8,3 +9,9 @@ function component(a) { mutate(y); return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.expect.md index 90df65c6ae425..d65e9cca92740 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(a) { +import {mutate} from 'shared-runtime'; + +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -14,14 +16,23 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a) { +import { mutate } from "shared-runtime"; + +function Component(t0) { const $ = _c(2); + const { a } = t0; let y; if ($[0] !== a) { const x = { a }; @@ -41,5 +52,15 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 2 }, { a: 3 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"x":{"a":2},"wat0":"joe"} +{"x":{"a":2},"wat0":"joe"} +{"x":{"a":3},"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.js index d0a1f62320605..b4f645846c5be 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-computed-mutate.js @@ -1,4 +1,6 @@ -function component(a) { +import {mutate} from 'shared-runtime'; + +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -9,3 +11,9 @@ function component(a) { mutate(y); return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.expect.md index e26f048efba9a..deb78c79e08ed 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(a) { +import {mutate} from 'shared-runtime'; + +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -14,14 +16,23 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a) { +import { mutate } from "shared-runtime"; + +function Component(t0) { const $ = _c(2); + const { a } = t0; let y; if ($[0] !== a) { const x = { a }; @@ -41,5 +52,15 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 2 }, { a: 3 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"x":{"a":2},"wat0":"joe"} +{"x":{"a":2},"wat0":"joe"} +{"x":{"a":3},"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.js index e34a03ec87742..a82df6b27fff8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-alias-receiver-mutate.js @@ -1,4 +1,6 @@ -function component(a) { +import {mutate} from 'shared-runtime'; + +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -9,3 +11,9 @@ function component(a) { mutate(y); return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md index c071d5d20ed99..6836544c5d337 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md @@ -27,7 +27,6 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; function component(a, b) { const $ = _c(2); - const y = { b }; let z; if ($[0] !== a) { z = { a }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-no-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-no-mutate.expect.md index aa32b3260ef50..14bf94e770a6b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-no-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-no-mutate.expect.md @@ -31,12 +31,20 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(t0) { - const $ = _c(3); + const $ = _c(5); const { a, b } = t0; let z; if ($[0] !== a || $[1] !== b) { z = { a }; - const y = { b }; + let t1; + if ($[3] !== b) { + t1 = { b }; + $[3] = b; + $[4] = t1; + } else { + t1 = $[4]; + } + const y = t1; const x = function () { z.a = 2; return Math.max(y.b, 0); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias-iife.expect.md index 1b91bc1a11275..a071dddba6b4c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias-iife.expect.md @@ -34,7 +34,6 @@ function component(a) { const x = { a }; y = {}; - y; y = x; mutate(y); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.expect.md index bdee721b5820e..0e2d77023996a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(a) { +import {mutate} from 'shared-runtime'; + +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -13,14 +15,23 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a) { +import { mutate } from "shared-runtime"; + +function Component(t0) { const $ = _c(2); + const { a } = t0; let y; if ($[0] !== a) { const x = { a }; @@ -39,5 +50,15 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 2 }, { a: 3 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"a":2,"wat0":"joe"} +{"a":2,"wat0":"joe"} +{"a":3,"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.js index 39e87de8c089d..22764fec72e6e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-simple-alias.js @@ -1,4 +1,6 @@ -function component(a) { +import {mutate} from 'shared-runtime'; + +function Component({a}) { let x = {a}; let y = {}; const f0 = function () { @@ -8,3 +10,9 @@ function component(a) { mutate(y); return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; 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 f4721a507f31f..2afc5fd25dbac 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 @@ -31,7 +31,6 @@ function bar(a) { const x = [a]; y = {}; - y; y = x[0][1]; $[0] = a; $[1] = 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 5c0be290a6930..3e57b7dc7c255 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 @@ -37,8 +37,6 @@ function bar(a, b) { let t; t = {}; - y; - t; y = x[0][1]; t = x[1][0]; $[0] = a; 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 34b927d91e5f7..22728aaf4323d 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 @@ -31,7 +31,6 @@ function bar(a) { const x = [a]; y = {}; - y; y = x[0].a[1]; $[0] = a; $[1] = 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 0978be54acb46..60f829cdc4d66 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 @@ -30,7 +30,6 @@ function bar(a) { const x = [a]; y = {}; - y; y = x[0]; $[0] = a; $[1] = y; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.expect.md index 4087dc53c98f2..6364e83deb9a2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(a, b) { +import {mutate} from 'shared-runtime'; + +function Component({a, b}) { let z = {a}; (function () { mutate(z); @@ -17,14 +19,28 @@ function component(a, b) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2, b: 3}], + sequentialRenders: [ + {a: 2, b: 3}, + {a: 2, b: 3}, + {a: 2, b: 4}, + {a: 3, b: 4}, + ], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a, b) { +import { mutate } from "shared-runtime"; + +function Component(t0) { const $ = _c(7); + const { a, b } = t0; let z; if ($[0] !== a) { z = { a }; @@ -37,27 +53,43 @@ function component(a, b) { } let y = z; - let t0; + let t1; if ($[2] !== b) { - t0 = { b }; + t1 = { b }; $[2] = b; - $[3] = t0; + $[3] = t1; } else { - t0 = $[3]; + t1 = $[3]; } - const z_0 = t0; - let t1; + const z_0 = t1; + let t2; if ($[4] !== y || $[5] !== z_0) { - t1 = { y, z: z_0 }; + t2 = { y, z: z_0 }; $[4] = y; $[5] = z_0; - $[6] = t1; + $[6] = t2; } else { - t1 = $[6]; + t2 = $[6]; } - y = t1; + y = t2; return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2, b: 3 }], + sequentialRenders: [ + { a: 2, b: 3 }, + { a: 2, b: 3 }, + { a: 2, b: 4 }, + { a: 3, b: 4 }, + ], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"y":{"a":2,"wat0":"joe"},"z":{"b":3}} +{"y":{"a":2,"wat0":"joe"},"z":{"b":3}} +{"y":{"a":2,"wat0":"joe"},"z":{"b":4}} +{"y":{"a":3,"wat0":"joe"},"z":{"b":4}} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.js index ad1a1f03c105c..b13e563ea820f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-capture-ref-before-rename.js @@ -1,4 +1,6 @@ -function component(a, b) { +import {mutate} from 'shared-runtime'; + +function Component({a, b}) { let z = {a}; (function () { mutate(z); @@ -12,3 +14,14 @@ function component(a, b) { } return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2, b: 3}], + sequentialRenders: [ + {a: 2, b: 3}, + {a: 2, b: 3}, + {a: 2, b: 4}, + {a: 3, b: 4}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.expect.md index 2f827d699a90c..c42aa4c48c5fd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.expect.md @@ -2,8 +2,7 @@ ## Input ```javascript -// @debug -function component(a, b) { +function useHook(a, b) { let z = {a}; let y = b; let x = function () { @@ -22,8 +21,8 @@ function component(a, b) { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @debug -function component(a, b) { +import { c as _c } from "react/compiler-runtime"; +function useHook(a, b) { const $ = _c(5); let t0; if ($[0] !== a) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.js index 3a1a7d0d297a8..04d6ca29e1fea 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-conditional-capture-mutate.js @@ -1,5 +1,4 @@ -// @debug -function component(a, b) { +function useHook(a, b) { let z = {a}; let y = b; let x = function () { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.expect.md index edc6fb6d6cba4..034e9781b2356 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.expect.md @@ -2,7 +2,9 @@ ## Input ```javascript -function component(a, b) { +import {mutate} from 'shared-runtime'; + +function useHook({a, b}) { let z = {a}; { let z = {b}; @@ -13,23 +15,37 @@ function component(a, b) { return z; } +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{a: 2, b: 3}], + sequentialRenders: [ + {a: 2, b: 3}, + {a: 2, b: 3}, + {a: 2, b: 4}, + {a: 3, b: 4}, + ], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a, b) { +import { mutate } from "shared-runtime"; + +function useHook(t0) { const $ = _c(2); - let t0; + const { a, b } = t0; + let t1; if ($[0] !== a) { - t0 = { a }; + t1 = { a }; $[0] = a; - $[1] = t0; + $[1] = t1; } else { - t0 = $[1]; + t1 = $[1]; } - const z = t0; + const z = t1; const z_0 = { b }; @@ -37,5 +53,21 @@ function component(a, b) { return z; } +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ a: 2, b: 3 }], + sequentialRenders: [ + { a: 2, b: 3 }, + { a: 2, b: 3 }, + { a: 2, b: 4 }, + { a: 3, b: 4 }, + ], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"a":2} +{"a":2} +{"a":2} +{"a":3} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.js index 1022a8604ab9c..49595f19d7e6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-renamed-ref.js @@ -1,4 +1,6 @@ -function component(a, b) { +import {mutate} from 'shared-runtime'; + +function useHook({a, b}) { let z = {a}; { let z = {b}; @@ -8,3 +10,14 @@ function component(a, b) { } return z; } + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{a: 2, b: 3}], + sequentialRenders: [ + {a: 2, b: 3}, + {a: 2, b: 3}, + {a: 2, b: 4}, + {a: 3, b: 4}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md index 344b9439078ca..dc0961c61264c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.expect.md @@ -2,11 +2,17 @@ ## Input ```javascript -function component(a, b) { +import {Stringify} from 'shared-runtime'; +function Component({a, b}) { let z = {a}; - let p = () => {z}; + let p = () => {z}; return p(); } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1}], + sequentialRenders: [{a: 1}, {a: 1}, {a: 2}], +}; ``` @@ -14,36 +20,48 @@ function component(a, b) { ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a, b) { +import { Stringify } from "shared-runtime"; +function Component(t0) { const $ = _c(6); - let t0; + const { a } = t0; + let t1; if ($[0] !== a) { - t0 = { a }; + t1 = { a }; $[0] = a; - $[1] = t0; + $[1] = t1; } else { - t0 = $[1]; + t1 = $[1]; } - const z = t0; - let t1; + const z = t1; + let t2; if ($[2] !== z) { - t1 = () => {z}; + t2 = () => {z}; $[2] = z; - $[3] = t1; + $[3] = t2; } else { - t1 = $[3]; + t2 = $[3]; } - const p = t1; - let t2; + const p = t2; + let t3; if ($[4] !== p) { - t2 = p(); + t3 = p(); $[4] = p; - $[5] = t2; + $[5] = t3; } else { - t2 = $[5]; + t3 = $[5]; } - return t2; + return t3; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 1 }], + sequentialRenders: [{ a: 1 }, { a: 1 }, { a: 2 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok)
{"children":{"a":1}}
+
{"children":{"a":1}}
+
{"children":{"a":2}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.js index c4335e757feac..d5a4bb842f96c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-runs-inference.js @@ -1,5 +1,11 @@ -function component(a, b) { +import {Stringify} from 'shared-runtime'; +function Component({a, b}) { let z = {a}; - let p = () => {z}; + let p = () => {z}; return p(); } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1}], + sequentialRenders: [{a: 1}, {a: 1}, {a: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md index 82012899616cd..d409bd32965f9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.expect.md @@ -2,28 +2,58 @@ ## Input ```javascript -function component(a) { +import {mutate, Stringify} from 'shared-runtime'; +function Component({a}) { let z = {a}; let x = function () { let z; mutate(z); + return z; }; - return x; + return ; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1}], + sequentialRenders: [{a: 1}, {a: 1}, {a: 2}], +}; + ``` ## Code ```javascript -function component(a) { +import { c as _c } from "react/compiler-runtime"; +import { mutate, Stringify } from "shared-runtime"; +function Component(t0) { + const $ = _c(1); + const x = _temp; - return x; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ; + $[0] = t1; + } else { + t1 = $[0]; + } + return t1; } function _temp() { let z_0; mutate(z_0); + return z_0; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 1 }], + sequentialRenders: [{ a: 1 }, { a: 1 }, { a: 2 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok)
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
+
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
+
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.js index 0361ef08bb13e..a0ce67e4d00b6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-shadow-captured.js @@ -1,8 +1,16 @@ -function component(a) { +import {mutate, Stringify} from 'shared-runtime'; +function Component({a}) { let z = {a}; let x = function () { let z; mutate(z); + return z; }; - return x; + return ; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 1}], + sequentialRenders: [{a: 1}, {a: 1}, {a: 2}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.expect.md index 1bdc1c09a3483..f5d4b94ae5410 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.expect.md @@ -2,7 +2,8 @@ ## Input ```javascript -function component(a) { +import {mutate} from 'shared-runtime'; +function Component({a}) { let x = {a}; let y = 1; (function () { @@ -12,20 +13,27 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a) { +import { mutate } from "shared-runtime"; +function Component(t0) { const $ = _c(2); + const { a } = t0; let y; if ($[0] !== a) { const x = { a }; y = 1; - y; y = x; mutate(y); @@ -37,5 +45,15 @@ function component(a) { return y; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: 2 }], + sequentialRenders: [{ a: 2 }, { a: 2 }, { a: 3 }], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"a":2,"wat0":"joe"} +{"a":2,"wat0":"joe"} +{"a":3,"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.js index 0a454b19b45e7..2b4b1b47ecc31 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-reference-changes-type.js @@ -1,4 +1,5 @@ -function component(a) { +import {mutate} from 'shared-runtime'; +function Component({a}) { let x = {a}; let y = 1; (function () { @@ -7,3 +8,9 @@ function component(a) { mutate(y); return y; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{a: 2}], + sequentialRenders: [{a: 2}, {a: 2}, {a: 3}], +}; 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 d17c934b3b42c..cf85967682607 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 @@ -38,9 +38,8 @@ function useTest() { const t1 = (w = 42); const t2 = w; - - w; let t3; + w = 999; t3 = 2; t0 = makeArray(t1, t2, t3); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.expect.md index 6d8782f74b50c..9af46bbfd9889 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.expect.md @@ -2,7 +2,8 @@ ## Input ```javascript -function component(a, b) { +import {mutate} from 'shared-runtime'; +function useHook({a, b}) { let y = {a}; let x = {b}; x['y'] = y; @@ -10,14 +11,26 @@ function component(a, b) { return x; } +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{a: 2, b: 3}], + sequentialRenders: [ + {a: 2, b: 3}, + {a: 2, b: 3}, + {a: 3, b: 3}, + ], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; -function component(a, b) { +import { mutate } from "shared-runtime"; +function useHook(t0) { const $ = _c(3); + const { a, b } = t0; let x; if ($[0] !== a || $[1] !== b) { const y = { a }; @@ -33,5 +46,19 @@ function component(a, b) { return x; } +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{ a: 2, b: 3 }], + sequentialRenders: [ + { a: 2, b: 3 }, + { a: 2, b: 3 }, + { a: 3, b: 3 }, + ], +}; + ``` - \ No newline at end of file + +### Eval output +(kind: ok) {"b":3,"y":{"a":2},"wat0":"joe"} +{"b":3,"y":{"a":2},"wat0":"joe"} +{"b":3,"y":{"a":3},"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.js index e28a2c0928c04..85d0c398bcffc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/computed-store-alias.js @@ -1,7 +1,18 @@ -function component(a, b) { +import {mutate} from 'shared-runtime'; +function useHook({a, b}) { let y = {a}; let x = {b}; x['y'] = y; mutate(x); return x; } + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{a: 2, b: 3}], + sequentialRenders: [ + {a: 2, b: 3}, + {a: 2, b: 3}, + {a: 3, b: 3}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-global.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-global.expect.md index e42ea8ce933b8..04b6c4f17f41a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-global.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-global.expect.md @@ -19,10 +19,10 @@ function foo() { import { c as _c } from "react/compiler-runtime"; function foo() { const $ = _c(1); + + const getJSX = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const getJSX = () => ; - t0 = getJSX(); $[0] = t0; } else { @@ -31,6 +31,9 @@ function foo() { const result = t0; return result; } +function _temp() { + return ; +} ``` \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-primitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-primitive.expect.md index 6686c0b5301bf..60fe0808d922a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-primitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/const-propagation-into-function-expression-primitive.expect.md @@ -23,13 +23,14 @@ export const FIXTURE_ENTRYPOINT = { ```javascript function foo() { - const f = () => { - console.log(42); - }; + const f = _temp; f(); return 42; } +function _temp() { + console.log(42); +} export const FIXTURE_ENTRYPOINT = { fn: foo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.expect.md index 8ea2190480003..503ee6d716813 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.expect.md @@ -2,35 +2,55 @@ ## Input ```javascript +import {Stringify, identity} from 'shared-runtime'; + function Component(props) { const x = 42; const onEvent = () => { - console.log(x); + return identity(x); }; - return ; + return ; } +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; + ``` ## Code ```javascript import { c as _c } from "react/compiler-runtime"; +import { Stringify, identity } from "shared-runtime"; + function Component(props) { const $ = _c(1); + + const onEvent = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const onEvent = () => { - console.log(42); - }; - - t0 = ; + t0 = ; $[0] = t0; } else { t0 = $[0]; } return t0; } +function _temp() { + return identity(42); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; ``` - \ No newline at end of file + +### Eval output +(kind: ok)
{"onEvent":{"kind":"Function","result":42},"shouldInvokeFns":true}
+
{"onEvent":{"kind":"Function","result":42},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.js index c98bd9b670056..d1df503ad9736 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/constant-propagation-into-function-expressions.js @@ -1,7 +1,15 @@ +import {Stringify, identity} from 'shared-runtime'; + function Component(props) { const x = 42; const onEvent = () => { - console.log(x); + return identity(x); }; - return ; + return ; } + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], + sequentialRenders: [{}, {}], +}; 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 3dc0dba27c364..da3bb94ed5ed4 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,9 +34,8 @@ function Component(props) { let Component; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { Component = Stringify; - - Component; let t0; + t0 = Component; Component = t0; $[0] = Component; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md index 2045ee7901e96..1ba0d59e17265 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md @@ -24,13 +24,17 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` + 4 | } 5 | return baz(); // OK: FuncDecls are HoistableDeclarations that have both declaration and value hoisting - 6 | function baz() { +> 6 | function baz() { + | ^^^^^^^^^^^^^^^^ > 7 | return bar(); - | ^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (7:7) - 8 | } + | ^^^^^^^^^^^^^^^^^ +> 8 | } + | ^^^^ Todo: Support functions with unreachable code that may contain hoisted declarations (6:8) 9 | } 10 | + 11 | export const FIXTURE_ENTRYPOINT = { ``` \ No newline at end of file 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 new file mode 100644 index 0000000000000..7babe57b000e2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +// @enableTransitivelyFreezeFunctionExpressions +import {setPropertyByKey, Stringify, useIdentity} from 'shared-runtime'; + +function Foo({count}) { + const x = {value: 0}; + /** + * After this custom hook call, it's no longer valid to mutate x. + */ + const cb = useIdentity(() => { + setPropertyByKey(x, 'value', count); + }); + + x.value += count; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{count: 1}], +}; + +``` + + +## Error + +``` + 11 | }); + 12 | +> 13 | x.value += count; + | ^ InvalidReact: This mutates a variable that React considers immutable (13:13) + 14 | return ; + 15 | } + 16 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.tsx new file mode 100644 index 0000000000000..b71626a435b78 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-identifier.tsx @@ -0,0 +1,20 @@ +// @enableTransitivelyFreezeFunctionExpressions +import {setPropertyByKey, Stringify, useIdentity} from 'shared-runtime'; + +function Foo({count}) { + const x = {value: 0}; + /** + * After this custom hook call, it's no longer valid to mutate x. + */ + const cb = useIdentity(() => { + setPropertyByKey(x, 'value', count); + }); + + x.value += count; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{count: 1}], +}; 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 new file mode 100644 index 0000000000000..fcc47ddc2b14f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +// @enableTransitivelyFreezeFunctionExpressions +import {mutate, Stringify, useIdentity} from 'shared-runtime'; + +function Foo({count}) { + const x = {value: 0}; + /** + * After this custom hook call, it's no longer valid to mutate x. + */ + const cb = useIdentity(() => { + x.value++; + }); + + x.value += count; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{count: 1}], +}; + +``` + + +## Error + +``` + 11 | }); + 12 | +> 13 | x.value += count; + | ^ InvalidReact: This mutates a variable that React considers immutable (13:13) + 14 | return ; + 15 | } + 16 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.jsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.jsx new file mode 100644 index 0000000000000..2a94559c1f026 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-call-freezes-captured-memberexpr.jsx @@ -0,0 +1,20 @@ +// @enableTransitivelyFreezeFunctionExpressions +import {mutate, Stringify, useIdentity} from 'shared-runtime'; + +function Foo({count}) { + const x = {value: 0}; + /** + * After this custom hook call, it's no longer valid to mutate x. + */ + const cb = useIdentity(() => { + x.value++; + }); + + x.value += count; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{count: 1}], +}; 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 db3a192eaf604..f66b970f00dd4 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$16 (9:9) + | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9) 10 | } 11 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-functionexpr-conditional-dep.expect.md similarity index 95% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-functionexpr-conditional-dep.expect.md index d6331db4e7ea3..e99e9e1c80226 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-functionexpr-conditional-dep.expect.md @@ -90,4 +90,6 @@ export const FIXTURE_ENTRYPOINT = { }; ``` - \ No newline at end of file + +### Eval output +(kind: ok)
{"shouldInvokeFns":true,"callback":{"kind":"Function","result":null}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-functionexpr-conditional-dep.tsx similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.tsx rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-functionexpr-conditional-dep.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.expect.md new file mode 100644 index 0000000000000..55cab1e9f3bd1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.expect.md @@ -0,0 +1,81 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +function useFoo() { + const onClick = response => { + setState(DISABLED_FORM); + }; + + const [state, setState] = useState(); + const handleLogout = useCallback(() => { + setState(DISABLED_FORM); + }, [setState]); + const getComponent = () => { + return handleLogout()} />; + }; + + // this `getComponent` call should not be inferred as mutating setState + return [getComponent(), onClick]; // pass onClick to avoid dce +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +function useFoo() { + const $ = _c(9); + const onClick = (response) => { + setState(DISABLED_FORM); + }; + + const [, t0] = useState(); + const setState = t0; + let t1; + if ($[0] !== setState) { + t1 = () => { + setState(DISABLED_FORM); + }; + $[0] = setState; + $[1] = t1; + } else { + t1 = $[1]; + } + setState; + const handleLogout = t1; + let t2; + if ($[2] !== handleLogout) { + t2 = () => handleLogout()} />; + $[2] = handleLogout; + $[3] = t2; + } else { + t2 = $[3]; + } + const getComponent = t2; + let t3; + if ($[4] !== getComponent) { + t3 = getComponent(); + $[4] = getComponent; + $[5] = t3; + } else { + t3 = $[5]; + } + let t4; + if ($[6] !== onClick || $[7] !== t3) { + t4 = [t3, onClick]; + $[6] = onClick; + $[7] = t3; + $[8] = t4; + } else { + t4 = $[8]; + } + return t4; +} + +``` + +### 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/hoisting-setstate-captured-indirectly-jsx.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.js new file mode 100644 index 0000000000000..4a44679390a80 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate-captured-indirectly-jsx.js @@ -0,0 +1,17 @@ +// @validatePreserveExistingMemoizationGuarantees +function useFoo() { + const onClick = response => { + setState(DISABLED_FORM); + }; + + const [state, setState] = useState(); + const handleLogout = useCallback(() => { + setState(DISABLED_FORM); + }, [setState]); + const getComponent = () => { + return handleLogout()} />; + }; + + // this `getComponent` call should not be inferred as mutating setState + return [getComponent(), onClick]; // pass onClick to avoid dce +} 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/hoisting-setstate.expect.md new file mode 100644 index 0000000000000..483d9b1a8e2da --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.expect.md @@ -0,0 +1,77 @@ + +## Input + +```javascript +import {useEffect, useState} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Foo() { + /** + * Previously, this lowered to + * $1 = LoadContext capture setState + * $2 = FunctionExpression deps=$1 context=setState + * [[ at this point, we freeze the `LoadContext setState` instruction, but it will never be referenced again ]] + * + * Now, this function expression directly references `setState`, which freezes + * the source `DeclareContext HoistedConst setState`. Freezing source identifiers + * (instead of the one level removed `LoadContext`) is more semantically correct + * for everything *other* than hoisted context declarations. + * + * $2 = Function context=setState + */ + useEffect(() => setState(2), []); + + const [state, setState] = useState(0); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +## 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: [{}, {}], +}; + +``` + +### Eval output +(kind: ok)
{"state":2}
+
{"state":2}
\ 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/hoisting-setstate.js new file mode 100644 index 0000000000000..7b26c8d086491 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-setstate.js @@ -0,0 +1,28 @@ +import {useEffect, useState} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Foo() { + /** + * Previously, this lowered to + * $1 = LoadContext capture setState + * $2 = FunctionExpression deps=$1 context=setState + * [[ at this point, we freeze the `LoadContext setState` instruction, but it will never be referenced again ]] + * + * Now, this function expression directly references `setState`, which freezes + * the source `DeclareContext HoistedConst setState`. Freezing source identifiers + * (instead of the one level removed `LoadContext`) is more semantically correct + * for everything *other* than hoisted context declarations. + * + * $2 = Function context=setState + */ + useEffect(() => setState(2), []); + + const [state, setState] = useState(0); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], + sequentialRenders: [{}, {}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md new file mode 100644 index 0000000000000..957919516d09d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.expect.md @@ -0,0 +1,87 @@ + +## Input + +```javascript +import {useIdentity, Stringify, identity} from 'shared-runtime'; + +function Foo({val1}) { + // `x={inner: val1}` should be able to be memoized + const x = {inner: val1}; + + // Any references to `x` after this hook call should be read-only + const cb = useIdentity(() => x.inner); + + // With enableTransitivelyFreezeFunctionExpressions, it's invalid + // to write to `x` after it's been frozen. + // TODO: runtime validation for DX + const copy = identity(x); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{val1: 1}], + sequentialRenders: [{val1: 1}, {val1: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useIdentity, Stringify, identity } from "shared-runtime"; + +function Foo(t0) { + const $ = _c(9); + const { val1 } = t0; + let t1; + if ($[0] !== val1) { + t1 = { inner: val1 }; + $[0] = val1; + $[1] = t1; + } else { + t1 = $[1]; + } + const x = t1; + let t2; + if ($[2] !== x.inner) { + t2 = () => x.inner; + $[2] = x.inner; + $[3] = t2; + } else { + t2 = $[3]; + } + const cb = useIdentity(t2); + let t3; + if ($[4] !== x) { + t3 = identity(x); + $[4] = x; + $[5] = t3; + } else { + t3 = $[5]; + } + const copy = t3; + let t4; + if ($[6] !== cb || $[7] !== copy) { + t4 = ; + $[6] = cb; + $[7] = copy; + $[8] = t4; + } else { + t4 = $[8]; + } + return t4; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ val1: 1 }], + sequentialRenders: [{ val1: 1 }, { val1: 1 }], +}; + +``` + +### Eval output +(kind: ok)
{"copy":{"inner":1},"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}
+
{"copy":{"inner":1},"cb":{"kind":"Function","result":1},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.tsx new file mode 100644 index 0000000000000..68e8e034379e1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hook-call-freezes-captured-memberexpr.tsx @@ -0,0 +1,21 @@ +import {useIdentity, Stringify, identity} from 'shared-runtime'; + +function Foo({val1}) { + // `x={inner: val1}` should be able to be memoized + const x = {inner: val1}; + + // Any references to `x` after this hook call should be read-only + const cb = useIdentity(() => x.inner); + + // With enableTransitivelyFreezeFunctionExpressions, it's invalid + // to write to `x` after it's been frozen. + // TODO: runtime validation for DX + const copy = identity(x); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{val1: 1}], + sequentialRenders: [{val1: 1}, {val1: 1}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md index 74e01a72d52ba..a7d27bc38193f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-local-tag-in-lambda.expect.md @@ -25,10 +25,10 @@ import { c as _c } from "react/compiler-runtime"; import { Stringify } from "shared-runtime"; function useFoo() { const $ = _c(1); + + const callback = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const callback = () => ; - t0 = callback(); $[0] = t0; } else { @@ -36,6 +36,9 @@ function useFoo() { } return t0; } +function _temp() { + return ; +} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-memberexpr-tag-in-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-memberexpr-tag-in-lambda.expect.md index 22fa3b2e2a2e3..e5ead2479dd40 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-memberexpr-tag-in-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-memberexpr-tag-in-lambda.expect.md @@ -25,10 +25,10 @@ import { c as _c } from "react/compiler-runtime"; import * as SharedRuntime from "shared-runtime"; function useFoo() { const $ = _c(1); + + const callback = _temp; let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - const callback = () => ; - t0 = callback(); $[0] = t0; } else { @@ -36,6 +36,9 @@ function useFoo() { } return t0; } +function _temp() { + return ; +} export const FIXTURE_ENTRYPOINT = { fn: useFoo, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md index d34db46d6aa28..ed0ddda55b32f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-non-reactive-to-reactive.expect.md @@ -26,7 +26,6 @@ function f(a) { const $ = _c(4); let x; if ($[0] !== a) { - x; x = { a }; $[0] = a; $[1] = x; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-ref-non-reactive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-ref-non-reactive.expect.md index 2aa5d4d06dfb6..8dc4839085ee5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-ref-non-reactive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-mutated-ref-non-reactive.expect.md @@ -27,7 +27,6 @@ function f(a) { const $ = _c(2); let x; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - x; x = {}; $[0] = x; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mixedreadonly-mutating-map.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mixedreadonly-mutating-map.expect.md index 4867388a864d2..6dd05cbe7c619 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mixedreadonly-mutating-map.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mixedreadonly-mutating-map.expect.md @@ -125,22 +125,21 @@ function Component(t0) { } else { t1 = $[2]; } - const t2 = jsx[0]; - let t3; - if ($[3] !== t1 || $[4] !== t2) { - t3 = ( + let t2; + if ($[3] !== jsx[0] || $[4] !== t1) { + t2 = ( <> {t1} - {t2} + {jsx[0]} ); - $[3] = t1; - $[4] = t2; - $[5] = t3; + $[3] = jsx[0]; + $[4] = t1; + $[5] = t2; } else { - t3 = $[5]; + t2 = $[5]; } - return t3; + return t2; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-shadowed-identifiers.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-shadowed-identifiers.expect.md index 13ba6d17986bb..3c624de9ebe57 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-shadowed-identifiers.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-function-shadowed-identifiers.expect.md @@ -31,7 +31,7 @@ function Component(props) { let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = (e) => { - setX((currentX) => currentX + null); + setX(_temp); }; $[0] = t0; } else { @@ -48,6 +48,9 @@ function Component(props) { } return t1; } +function _temp(currentX) { + return currentX + null; +} export const FIXTURE_ENTRYPOINT = { fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md deleted file mode 100644 index bf22790a51c12..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.expect.md +++ /dev/null @@ -1,40 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees - -import {useMemo} from 'react'; -import {makeArray} from 'shared-runtime'; - -// We currently only recognize "hoistable" values (e.g. variable reads -// and property loads from named variables) in the source depslist. -// This makes validation logic simpler and follows the same constraints -// from the eslint react-hooks-deps plugin. -function Foo(props) { - const x = makeArray(props); - // react-hooks-deps lint would already fail here - return useMemo(() => [x[0]], [x[0]]); -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{val: 1}], -}; - -``` - - -## Error - -``` - 11 | const x = makeArray(props); - 12 | // react-hooks-deps lint would already fail here -> 13 | return useMemo(() => [x[0]], [x[0]]); - | ^^^^ InvalidReact: Expected the dependency list to be an array of simple expressions (e.g. `x`, `x.y.z`, `x?.y?.z`) (13:13) - 14 | } - 15 | - 16 | export const FIXTURE_ENTRYPOINT = { -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/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 new file mode 100644 index 0000000000000..dafe2b63e35ae --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees + +import {useMemo} from 'react'; +import {makeArray} from 'shared-runtime'; + +// We currently only recognize "hoistable" values (e.g. variable reads +// and property loads from named variables) in the source depslist. +// This makes validation logic simpler and follows the same constraints +// from the eslint react-hooks-deps plugin. +function Foo(props) { + const x = makeArray(props); + // react-hooks-deps lint would already fail here + return useMemo(() => [x[0]], [x[0]]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{val: 1}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees + +import { useMemo } from "react"; +import { makeArray } from "shared-runtime"; + +// We currently only recognize "hoistable" values (e.g. variable reads +// and property loads from named variables) in the source depslist. +// This makes validation logic simpler and follows the same constraints +// from the eslint react-hooks-deps plugin. +function Foo(props) { + const $ = _c(4); + let t0; + if ($[0] !== props) { + t0 = makeArray(props); + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const x = t0; + let t1; + let t2; + if ($[2] !== x[0]) { + t2 = [x[0]]; + $[2] = x[0]; + $[3] = t2; + } else { + t2 = $[3]; + } + t1 = t2; + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ val: 1 }], +}; + +``` + +### Eval output +(kind: ok) [{"val":1}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-dep-not-recognized.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-dep-array-literal-access.ts 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 e8a3e2d627c59..3fffec6a7dc20 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 @@ -35,7 +35,6 @@ function useFoo(arr1, arr2) { if ($[0] !== arr1 || $[1] !== arr2) { const x = [arr1]; - y; (y = x.concat(arr2)), y; $[0] = arr1; $[1] = arr2; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-array.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-array.expect.md index ee52047fbe982..b9ff24519e50f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-array.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-array.expect.md @@ -32,28 +32,20 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); - let x; + const $ = _c(2); + let t0; if ($[0] !== props.input) { - x = []; + const x = []; const y = x; y.push(props.input); - $[0] = props.input; - $[1] = x; - } else { - x = $[1]; - } - const t0 = x[0]; - let t1; - if ($[2] !== t0) { - t1 = [t0]; - $[2] = t0; - $[3] = t1; + t0 = [x[0]]; + $[0] = props.input; + $[1] = t0; } else { - t1 = $[3]; + t0 = $[1]; } - return t1; + return t0; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-lambda.expect.md index 3a338af5ab58f..ebf30d9d28560 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactivity-via-aliased-mutation-lambda.expect.md @@ -35,32 +35,24 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); - let x; + const $ = _c(2); + let t0; if ($[0] !== props.input) { - x = []; + const x = []; const f = (arg) => { const y = x; y.push(arg); }; f(props.input); - $[0] = props.input; - $[1] = x; - } else { - x = $[1]; - } - const t0 = x[0]; - let t1; - if ($[2] !== t0) { - t1 = [t0]; - $[2] = t0; - $[3] = t1; + t0 = [x[0]]; + $[0] = props.input; + $[1] = t0; } else { - t1 = $[3]; + t0 = $[1]; } - return t1; + return t0; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.expect.md index 8f808c94b3043..0a19a85428939 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-shared-value-writes.expect.md @@ -36,21 +36,22 @@ import { useSharedValue } from "react-native-reanimated"; * of render */ function SomeComponent() { - const $ = _c(3); + const $ = _c(2); const sharedVal = useSharedValue(0); - - const T0 = Button; - const t0 = () => (sharedVal.value = Math.random()); - let t1; - if ($[0] !== T0 || $[1] !== t0) { - t1 = ; - $[0] = T0; + let t0; + if ($[0] !== sharedVal) { + t0 = ( + 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 419435d6c470a..a521c5bea908f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -478,8 +478,8 @@ export default function ComponentsSettings({