Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78856eff76 | ||
|
|
254dc4d9f3 | ||
|
|
42a57ea802 | ||
|
|
04bf10e6a9 | ||
|
|
c61e75b76d | ||
|
|
7c908bcf4e | ||
|
|
a8e503dce0 | ||
|
|
45463ab3ac | ||
|
|
febc09b480 | ||
|
|
4a9df08157 | ||
|
|
da996a15be | ||
|
|
6b1a2c1d81 | ||
|
|
de4aad5ba6 | ||
|
|
156f0eca20 | ||
|
|
4f080e498c | ||
|
|
fe8c10695c | ||
|
|
daee08562c | ||
|
|
ab693a926f | ||
|
|
607615f4f6 | ||
|
|
e1e740717b | ||
|
|
ac799e569d | ||
|
|
0962f684a0 | ||
|
|
b888986054 | ||
|
|
addce2f9f2 | ||
|
|
74bcf3d0d2 | ||
|
|
b630219b13 | ||
|
|
7943da1e81 | ||
|
|
e3c06424ae | ||
|
|
5f4c5c920f | ||
|
|
112224d8d2 | ||
|
|
87d7e4c55b | ||
|
|
3bcf8c23de | ||
|
|
a4f9bd586b | ||
|
|
ff8f6f21f7 | ||
|
|
19176e3c08 | ||
|
|
d16c26da40 | ||
|
|
a8c155cab9 | ||
|
|
995410463a |
@@ -1,15 +1,20 @@
|
||||
name: (Compiler) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review]
|
||||
paths:
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_maintainer:
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
|
||||
15
.github/workflows/compiler_playground.yml
vendored
15
.github/workflows/compiler_playground.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_playground.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
@@ -40,7 +42,19 @@ jobs:
|
||||
**/node_modules
|
||||
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Check Playwright version
|
||||
id: playwright_version
|
||||
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
|
||||
id: cache_playwright_browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
|
||||
- run: npx playwright install --with-deps chromium
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
|
||||
- run: npx playwright install-deps
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
|
||||
- run: CI=true yarn test
|
||||
- run: ls -R test-results
|
||||
if: '!cancelled()'
|
||||
@@ -50,3 +64,4 @@ jobs:
|
||||
with:
|
||||
name: test-results
|
||||
path: compiler/apps/playground/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
3
.github/workflows/compiler_prereleases.yml
vendored
3
.github/workflows/compiler_prereleases.yml
vendored
@@ -20,11 +20,12 @@ on:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
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
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
defaults:
|
||||
|
||||
@@ -15,6 +15,8 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
|
||||
- cron: 10 16 * * 1,2,3,4,5
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
# At 10 minutes past 9:00 on Mon
|
||||
- cron: 10 9 * * 1
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
2
.github/workflows/compiler_typescript.yml
vendored
2
.github/workflows/compiler_typescript.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_typescript.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
10
.github/workflows/devtools_regression_tests.yml
vendored
10
.github/workflows/devtools_regression_tests.yml
vendored
@@ -9,6 +9,8 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
|
||||
@@ -18,6 +20,9 @@ jobs:
|
||||
download_build:
|
||||
name: Download base build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -47,6 +52,7 @@ jobs:
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
build_devtools_and_process_artifacts:
|
||||
name: Build DevTools and process artifacts
|
||||
@@ -84,17 +90,20 @@ jobs:
|
||||
with:
|
||||
name: react-devtools
|
||||
path: build/devtools.tgz
|
||||
if-no-files-found: error
|
||||
# Simplifies getting the extension for local testing
|
||||
- name: Archive chrome extension
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools-chrome-extension
|
||||
path: build/devtools/chrome-extension.zip
|
||||
if-no-files-found: error
|
||||
- name: Archive firefox extension
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: react-devtools-firefox-extension
|
||||
path: build/devtools/firefox-extension.zip
|
||||
if-no-files-found: error
|
||||
|
||||
run_devtools_tests_for_versions:
|
||||
name: Run DevTools tests for versions
|
||||
@@ -179,3 +188,4 @@ jobs:
|
||||
with:
|
||||
name: screenshots
|
||||
path: ./tmp/screenshots
|
||||
if-no-files-found: warn
|
||||
|
||||
109
.github/workflows/runtime_build_and_test.yml
vendored
109
.github/workflows/runtime_build_and_test.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
paths-ignore:
|
||||
- compiler/**
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
@@ -41,6 +43,16 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
- name: Warm with old cache
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Save cache
|
||||
@@ -74,6 +86,16 @@ jobs:
|
||||
cache-dependency-path: |
|
||||
yarn.lock
|
||||
compiler/yarn.lock
|
||||
- name: Warm with old cache
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd compiler install --frozen-lockfile
|
||||
@@ -127,6 +149,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -154,6 +179,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -241,6 +269,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -282,6 +313,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -302,6 +336,7 @@ jobs:
|
||||
with:
|
||||
name: _build_${{ matrix.worker_id }}_${{ matrix.release_channel }}
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
test_build:
|
||||
name: yarn test-build
|
||||
@@ -340,9 +375,16 @@ jobs:
|
||||
# TODO: Test more persistent configurations?
|
||||
]
|
||||
shard:
|
||||
- 1/3
|
||||
- 2/3
|
||||
- 3/3
|
||||
- 1/10
|
||||
- 2/10
|
||||
- 3/10
|
||||
- 4/10
|
||||
- 5/10
|
||||
- 6/10
|
||||
- 7/10
|
||||
- 8/10
|
||||
- 9/10
|
||||
- 10/10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -362,6 +404,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-and-compiler-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -398,6 +443,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -426,6 +474,7 @@ jobs:
|
||||
path: |
|
||||
./build.tgz
|
||||
./build2.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
check_error_codes:
|
||||
name: Search build artifacts for unminified errors
|
||||
@@ -447,6 +496,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -484,6 +536,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -517,7 +572,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: fixtures_dom-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
key: fixtures_dom-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'fixtures/dom/yarn.lock') }}
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn --cwd fixtures/dom install --frozen-lockfile
|
||||
@@ -561,11 +616,26 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: fixtures_flight-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
key: fixtures_flight-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'fixtures/flight/yarn.lock') }}
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd fixtures/flight install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Check Playwright version
|
||||
id: playwright_version
|
||||
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
|
||||
id: cache_playwright_browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
|
||||
- name: Playwright install deps
|
||||
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
|
||||
working-directory: fixtures/flight
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Restore archived build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -574,16 +644,6 @@ jobs:
|
||||
merge-multiple: true
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- name: Install fixture dependencies
|
||||
working-directory: fixtures/flight
|
||||
run: |
|
||||
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
if [ $? -ne 0 ]; then
|
||||
yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
||||
fi
|
||||
- name: Playwright install deps
|
||||
working-directory: fixtures/flight
|
||||
run: npx playwright install --with-deps chromium
|
||||
- name: Run tests
|
||||
working-directory: fixtures/flight
|
||||
run: yarn test
|
||||
@@ -595,11 +655,13 @@ jobs:
|
||||
with:
|
||||
name: flight-playwright-report
|
||||
path: fixtures/flight/playwright-report
|
||||
if-no-files-found: warn
|
||||
- name: Archive Flight fixture artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: flight-test-results
|
||||
path: fixtures/flight/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ----- DEVTOOLS -----
|
||||
build_devtools_and_process_artifacts:
|
||||
@@ -626,6 +688,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -647,6 +712,7 @@ jobs:
|
||||
with:
|
||||
name: react-devtools-${{ matrix.browser }}-extension
|
||||
path: build/devtools/${{ matrix.browser }}-extension.zip
|
||||
if-no-files-found: error
|
||||
|
||||
merge_devtools_artifacts:
|
||||
name: Merge DevTools artifacts
|
||||
@@ -679,6 +745,9 @@ jobs:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
runtime-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-
|
||||
runtime-node_modules-v6-
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
@@ -700,7 +769,10 @@ jobs:
|
||||
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, runtime_node_modules_cache]
|
||||
needs: [build_and_lint]
|
||||
permissions:
|
||||
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
|
||||
actions: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -712,7 +784,7 @@ jobs:
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache@v4 # note: this does not reuse centralized cache since it has unique cache key
|
||||
id: node_modules
|
||||
with:
|
||||
path: |
|
||||
@@ -738,8 +810,6 @@ jobs:
|
||||
run: ls -R base-build
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Restore archived build for PR
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -759,3 +829,4 @@ jobs:
|
||||
with:
|
||||
name: sizebot-message
|
||||
path: sizebot-message.md
|
||||
if-no-files-found: ignore
|
||||
|
||||
114
.github/workflows/runtime_commit_artifacts.yml
vendored
114
.github/workflows/runtime_commit_artifacts.yml
vendored
@@ -22,6 +22,8 @@ on:
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
|
||||
@@ -30,6 +32,40 @@ env:
|
||||
jobs:
|
||||
download_artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v4
|
||||
id: node_modules
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd scripts/release install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- name: Download artifacts for base revision
|
||||
run: |
|
||||
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- name: Archive build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: build/
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
process_artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [download_artifacts]
|
||||
outputs:
|
||||
www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
|
||||
fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
|
||||
@@ -69,25 +105,11 @@ jobs:
|
||||
run: |
|
||||
echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/setup-node@v4
|
||||
- name: Restore downloaded build
|
||||
uses: actions/download-artifact@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-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
|
||||
- name: Ensure clean build directory
|
||||
run: rm -rf build
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn --cwd scripts/release install --frozen-lockfile
|
||||
- name: Download artifacts for base revision
|
||||
run: |
|
||||
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
|
||||
name: build
|
||||
path: build
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- name: Strip @license from eslint plugin and react-refresh
|
||||
@@ -151,9 +173,9 @@ jobs:
|
||||
ls -R ./compiled-rn
|
||||
- name: Add REVISION files
|
||||
run: |
|
||||
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
|
||||
echo ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} >> ./compiled/facebook-www/REVISION
|
||||
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
|
||||
echo ${{ github.sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
|
||||
echo ${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
|
||||
- name: "Get current version string"
|
||||
id: get_current_version
|
||||
run: |
|
||||
@@ -170,15 +192,20 @@ jobs:
|
||||
with:
|
||||
name: compiled
|
||||
path: compiled/
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: compiled-rn
|
||||
path: compiled-rn/
|
||||
if-no-files-found: error
|
||||
|
||||
commit_www_artifacts:
|
||||
needs: download_artifacts
|
||||
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0')
|
||||
needs: [download_artifacts, process_artifacts]
|
||||
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.process_artifacts.outputs.www_branch_count == '0')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Used to push a commit to builds/facebook-www
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -190,12 +217,12 @@ jobs:
|
||||
name: compiled
|
||||
path: compiled/
|
||||
- name: Revert version changes
|
||||
if: needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
|
||||
if: needs.process_artifacts.outputs.last_version_classic != '' && needs.process_artifacts.outputs.last_version_modern != ''
|
||||
env:
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
|
||||
LAST_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.last_version_classic }}
|
||||
LAST_VERSION_MODERN: ${{ needs.download_artifacts.outputs.last_version_modern }}
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.process_artifacts.outputs.current_version_modern }}
|
||||
LAST_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.last_version_classic }}
|
||||
LAST_VERSION_MODERN: ${{ needs.process_artifacts.outputs.last_version_modern }}
|
||||
run: |
|
||||
echo "Reverting $CURRENT_VERSION_CLASSIC to $LAST_VERSION_CLASSIC"
|
||||
grep -rl "$CURRENT_VERSION_CLASSIC" ./compiled || echo "No files found with $CURRENT_VERSION_CLASSIC"
|
||||
@@ -225,12 +252,12 @@ jobs:
|
||||
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Re-apply version changes
|
||||
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '')
|
||||
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.process_artifacts.outputs.last_version_classic != '' && needs.process_artifacts.outputs.last_version_modern != '')
|
||||
env:
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
|
||||
LAST_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.last_version_classic }}
|
||||
LAST_VERSION_MODERN: ${{ needs.download_artifacts.outputs.last_version_modern }}
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.process_artifacts.outputs.current_version_modern }}
|
||||
LAST_VERSION_CLASSIC: ${{ needs.process_artifacts.outputs.last_version_classic }}
|
||||
LAST_VERSION_MODERN: ${{ needs.process_artifacts.outputs.last_version_modern }}
|
||||
run: |
|
||||
echo "Re-applying $LAST_VERSION_CLASSIC to $CURRENT_VERSION_CLASSIC"
|
||||
grep -rl "$LAST_VERSION_CLASSIC" ./compiled || echo "No files found with $LAST_VERSION_CLASSIC"
|
||||
@@ -244,8 +271,8 @@ jobs:
|
||||
- name: Will commit these changes
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
run: |
|
||||
echo ":"
|
||||
git status -u
|
||||
git add .
|
||||
git status
|
||||
- name: Check commit message
|
||||
if: inputs.dry_run
|
||||
run: |
|
||||
@@ -264,8 +291,11 @@ jobs:
|
||||
run: git push
|
||||
|
||||
commit_fbsource_artifacts:
|
||||
needs: download_artifacts
|
||||
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0')
|
||||
needs: [download_artifacts, process_artifacts]
|
||||
permissions:
|
||||
# Used to push a commit to builds/facebook-fbsource
|
||||
contents: write
|
||||
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.process_artifacts.outputs.fbsource_branch_count == '0')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -278,10 +308,10 @@ jobs:
|
||||
name: compiled-rn
|
||||
path: compiled-rn/
|
||||
- name: Revert version changes
|
||||
if: needs.download_artifacts.outputs.last_version_rn != ''
|
||||
if: needs.process_artifacts.outputs.last_version_rn != ''
|
||||
env:
|
||||
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
|
||||
CURRENT_VERSION: ${{ needs.process_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.process_artifacts.outputs.last_version_rn }}
|
||||
run: |
|
||||
echo "Reverting $CURRENT_VERSION to $LAST_VERSION"
|
||||
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "No files found with $CURRENT_VERSION"
|
||||
@@ -307,10 +337,10 @@ jobs:
|
||||
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Re-apply version changes
|
||||
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '')
|
||||
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.process_artifacts.outputs.last_version_rn != '')
|
||||
env:
|
||||
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
|
||||
CURRENT_VERSION: ${{ needs.process_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.process_artifacts.outputs.last_version_rn }}
|
||||
run: |
|
||||
echo "Re-applying $LAST_VERSION to $CURRENT_VERSION"
|
||||
grep -rl "$LAST_VERSION" ./compiled-rn || echo "No files found with $LAST_VERSION"
|
||||
|
||||
7
.github/workflows/runtime_discord_notify.yml
vendored
7
.github/workflows/runtime_discord_notify.yml
vendored
@@ -1,15 +1,20 @@
|
||||
name: (Runtime) Discord Notify
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types: [opened, ready_for_review]
|
||||
paths-ignore:
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_**.yml
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
check_maintainer:
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ on:
|
||||
paths-ignore:
|
||||
- compiler/**
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
2
.github/workflows/runtime_fuzz_tests.yml
vendored
2
.github/workflows/runtime_fuzz_tests.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
3
.github/workflows/runtime_prereleases.yml
vendored
3
.github/workflows/runtime_prereleases.yml
vendored
@@ -17,11 +17,12 @@ on:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
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
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
prerelease_commit_sha:
|
||||
required: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
|
||||
- cron: 10 16 * * 1,2,3,4,5
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
@@ -31,11 +31,12 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
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
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -14,6 +14,8 @@ on:
|
||||
is_core_team:
|
||||
value: ${{ jobs.check_maintainer.outputs.is_core_team }}
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
|
||||
@@ -22,6 +24,9 @@ env:
|
||||
jobs:
|
||||
check_maintainer:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# We fetch the contents of the MAINTAINERS file
|
||||
contents: read
|
||||
outputs:
|
||||
is_core_team: ${{ steps.check_if_actor_is_maintainer.outputs.result }}
|
||||
steps:
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
|
||||
|
||||
name: (Shared) Cleanup Branch Caches
|
||||
name: (Shared) Cleanup Merged Branch Caches
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
@@ -23,13 +29,13 @@ jobs:
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh cache delete $cacheKey
|
||||
echo "Deleting $cacheKey"
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
BRANCH: refs/pull/${{ inputs.pr_number || github.event.pull_request.number }}/merge
|
||||
35
.github/workflows/shared_cleanup_stale_branch_caches.yml
vendored
Normal file
35
.github/workflows/shared_cleanup_stale_branch_caches.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#force-deletion-of-caches-overriding-default-cache-eviction-policy
|
||||
|
||||
name: (Shared) Cleanup Stale Branch Caches
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 0 * * *
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# `actions:write` permission is required to delete caches
|
||||
# See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
|
||||
actions: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
echo "Fetching list of cache keys"
|
||||
cacheKeysForPR=$(gh cache list --limit 100 --json id,ref --jq '.[] | select(.ref != "refs/heads/main") | .id')
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh cache delete $cacheKey
|
||||
echo "Deleting $cacheKey"
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
@@ -5,6 +5,8 @@ on:
|
||||
branches:
|
||||
- 'builds/facebook-**'
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
|
||||
@@ -13,6 +15,9 @@ env:
|
||||
jobs:
|
||||
close_pr:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Used to create a review and close PRs
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Close PR
|
||||
uses: actions/github-script@v7
|
||||
|
||||
12
.github/workflows/shared_label_core_team_prs.yml
vendored
12
.github/workflows/shared_label_core_team_prs.yml
vendored
@@ -1,7 +1,9 @@
|
||||
name: (Shared) Label Core Team PRs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
@@ -11,6 +13,9 @@ env:
|
||||
jobs:
|
||||
check_maintainer:
|
||||
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
|
||||
permissions:
|
||||
# Used by check_maintainer
|
||||
contents: read
|
||||
with:
|
||||
actor: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
@@ -18,6 +23,11 @@ jobs:
|
||||
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_maintainer
|
||||
permissions:
|
||||
# Used to add labels on issues
|
||||
issues: write
|
||||
# Used to add labels on PRs
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Label PR as React Core Team
|
||||
uses: actions/github-script@v7
|
||||
|
||||
2
.github/workflows/shared_lint.yml
vendored
2
.github/workflows/shared_lint.yml
vendored
@@ -5,6 +5,8 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
2
.github/workflows/shared_stale.yml
vendored
2
.github/workflows/shared_stale.yml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
|
||||
@@ -7,7 +7,4 @@
|
||||
|
||||
const makeE2EConfig = require('../jest/makeE2EConfig');
|
||||
|
||||
const config = makeE2EConfig('e2e with forget', true);
|
||||
config.setupFilesAfterEnv = ['<rootDir>/../scripts/jest/setupEnvE2E.js'];
|
||||
|
||||
module.exports = config;
|
||||
module.exports = makeE2EConfig('e2e with forget', true);
|
||||
|
||||
@@ -5,19 +5,16 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {jsx} from '@babel/plugin-syntax-jsx';
|
||||
import babelJest from 'babel-jest';
|
||||
import {compile} from 'babel-plugin-react-compiler';
|
||||
import {
|
||||
validateEnvironmentConfig,
|
||||
EnvironmentConfig,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {execSync} from 'child_process';
|
||||
|
||||
import type {NodePath, Visitor} from '@babel/traverse';
|
||||
import type {CallExpression, FunctionDeclaration} from '@babel/types';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
validateEnvironmentConfig,
|
||||
} from 'babel-plugin-react-compiler';
|
||||
import {basename} from 'path';
|
||||
import type {CallExpression} from '@babel/types';
|
||||
import BabelPluginReactCompiler from 'babel-plugin-react-compiler';
|
||||
|
||||
/**
|
||||
* -- IMPORTANT --
|
||||
@@ -28,10 +25,19 @@ import {basename} from 'path';
|
||||
const e2eTransformerCacheKey = 1;
|
||||
const forgetOptions: EnvironmentConfig = validateEnvironmentConfig({
|
||||
enableAssumeHooksFollowRulesOfReact: true,
|
||||
enableFunctionOutlining: false,
|
||||
});
|
||||
const debugMode = process.env['DEBUG_FORGET_COMPILER'] != null;
|
||||
|
||||
const compilerCacheKey = execSync(
|
||||
'yarn --silent --cwd ../.. hash packages/babel-plugin-react-compiler/dist',
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
if (debugMode) {
|
||||
console.log('cachebreaker', compilerCacheKey);
|
||||
}
|
||||
|
||||
module.exports = (useForget: boolean) => {
|
||||
function createTransformer() {
|
||||
return babelJest.createTransformer({
|
||||
@@ -42,15 +48,14 @@ module.exports = (useForget: boolean) => {
|
||||
plugins: [
|
||||
useForget
|
||||
? [
|
||||
ReactForgetFunctionTransform,
|
||||
BabelPluginReactCompiler,
|
||||
{
|
||||
environment: forgetOptions,
|
||||
/*
|
||||
* Jest hashes the babel config as a cache breaker.
|
||||
* (see https://github.com/jestjs/jest/blob/v29.6.2/packages/babel-jest/src/index.ts#L84)
|
||||
*/
|
||||
compilerCacheKey: execSync(
|
||||
'yarn --silent --cwd ../.. hash packages/babel-plugin-react-compiler/dist',
|
||||
).toString(),
|
||||
compilerCacheKey,
|
||||
transformOptionsCacheKey: forgetOptions,
|
||||
e2eTransformerCacheKey,
|
||||
},
|
||||
@@ -105,104 +110,3 @@ module.exports = (useForget: boolean) => {
|
||||
createTransformer,
|
||||
};
|
||||
};
|
||||
|
||||
// Mostly copied from react/scripts/babel/transform-forget.js
|
||||
function isReactComponentLike(fn: NodePath<FunctionDeclaration>): boolean {
|
||||
let isReactComponent = false;
|
||||
let hasNoUseForgetDirective = false;
|
||||
|
||||
/*
|
||||
* React components start with an upper case letter,
|
||||
* React hooks start with `use`
|
||||
*/
|
||||
if (
|
||||
fn.node.id == null ||
|
||||
(fn.node.id.name[0].toUpperCase() !== fn.node.id.name[0] &&
|
||||
!/^use[A-Z0-9]/.test(fn.node.id.name))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn.traverse({
|
||||
DirectiveLiteral(path) {
|
||||
if (path.node.value === 'use no forget') {
|
||||
hasNoUseForgetDirective = true;
|
||||
}
|
||||
},
|
||||
|
||||
JSX(path) {
|
||||
// Is there is a JSX node created in the current function context?
|
||||
if (path.scope.getFunctionParent()?.path.node === fn.node) {
|
||||
isReactComponent = true;
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression(path) {
|
||||
// Is there hook usage?
|
||||
if (
|
||||
path.node.callee.type === 'Identifier' &&
|
||||
!/^use[A-Z0-9]/.test(path.node.callee.name)
|
||||
) {
|
||||
isReactComponent = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (hasNoUseForgetDirective) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isReactComponent;
|
||||
}
|
||||
|
||||
function ReactForgetFunctionTransform() {
|
||||
const compiledFns = new Set();
|
||||
const visitor = {
|
||||
FunctionDeclaration(fn: NodePath<FunctionDeclaration>, state: any): void {
|
||||
if (compiledFns.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReactComponentLike(fn)) {
|
||||
return;
|
||||
}
|
||||
if (debugMode) {
|
||||
const filename = basename(state.file.opts.filename);
|
||||
if (fn.node.loc && fn.node.id) {
|
||||
console.log(
|
||||
` Compiling ${filename}:${fn.node.loc.start.line}:${fn.node.loc.start.column} ${fn.node.id.name}`,
|
||||
);
|
||||
} else {
|
||||
console.log(` Compiling ${filename} ${fn.node.id?.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const compiled = compile(
|
||||
fn,
|
||||
forgetOptions,
|
||||
'Other',
|
||||
'all_features',
|
||||
'_c',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
compiledFns.add(compiled);
|
||||
|
||||
const fun = t.functionDeclaration(
|
||||
compiled.id,
|
||||
compiled.params,
|
||||
compiled.body,
|
||||
compiled.generator,
|
||||
compiled.async,
|
||||
);
|
||||
fn.replaceWith(fun);
|
||||
fn.skip();
|
||||
},
|
||||
};
|
||||
return {
|
||||
name: 'react-forget-e2e',
|
||||
inherits: jsx,
|
||||
visitor,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +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.
|
||||
*/
|
||||
|
||||
const ReactCompilerRuntime = require('react/compiler-runtime');
|
||||
|
||||
/*
|
||||
* Our e2e babel transform currently only compiles functions, not programs.
|
||||
* As a result, our e2e transpiled code does not contain an import for the
|
||||
* memo cache function. As a temporary hack, we add a `_c` global, which is
|
||||
* the name that is used for the import by default.
|
||||
*/
|
||||
globalThis._c = ReactCompilerRuntime.c;
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import type {SourceLocation} from './HIR';
|
||||
import {Err, Ok, Result} from './Utils/Result';
|
||||
import {assertExhaustive} from './Utils/utils';
|
||||
|
||||
export enum ErrorSeverity {
|
||||
@@ -224,6 +225,10 @@ export class CompilerError extends Error {
|
||||
return this.details.length > 0;
|
||||
}
|
||||
|
||||
asResult(): Result<void, CompilerError> {
|
||||
return this.hasErrors() ? Err(this) : Ok(undefined);
|
||||
}
|
||||
|
||||
/*
|
||||
* An error is critical if it means the compiler has entered into a broken state and cannot
|
||||
* continue safely. Other expected errors such as Todos mean that we can skip over that component
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {PluginOptions} from './Options';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {ProgramContext} from './Imports';
|
||||
import {ExternalFunction} from '..';
|
||||
|
||||
/**
|
||||
* Gating rewrite for function declarations which are referenced before their
|
||||
@@ -34,7 +35,8 @@ import {CompilerError} from '../CompilerError';
|
||||
function insertAdditionalFunctionDeclaration(
|
||||
fnPath: NodePath<t.FunctionDeclaration>,
|
||||
compiled: t.FunctionDeclaration,
|
||||
gating: NonNullable<PluginOptions['gating']>,
|
||||
programContext: ProgramContext,
|
||||
gatingFunctionIdentifierName: string,
|
||||
): void {
|
||||
const originalFnName = fnPath.node.id;
|
||||
const originalFnParams = fnPath.node.params;
|
||||
@@ -57,14 +59,14 @@ function insertAdditionalFunctionDeclaration(
|
||||
loc: fnPath.node.loc ?? null,
|
||||
});
|
||||
|
||||
const gatingCondition = fnPath.scope.generateUidIdentifier(
|
||||
`${gating.importSpecifierName}_result`,
|
||||
const gatingCondition = t.identifier(
|
||||
programContext.newUid(`${gatingFunctionIdentifierName}_result`),
|
||||
);
|
||||
const unoptimizedFnName = fnPath.scope.generateUidIdentifier(
|
||||
`${originalFnName.name}_unoptimized`,
|
||||
const unoptimizedFnName = t.identifier(
|
||||
programContext.newUid(`${originalFnName.name}_unoptimized`),
|
||||
);
|
||||
const optimizedFnName = fnPath.scope.generateUidIdentifier(
|
||||
`${originalFnName.name}_optimized`,
|
||||
const optimizedFnName = t.identifier(
|
||||
programContext.newUid(`${originalFnName.name}_optimized`),
|
||||
);
|
||||
/**
|
||||
* Step 1: rename existing functions
|
||||
@@ -115,7 +117,7 @@ function insertAdditionalFunctionDeclaration(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
gatingCondition,
|
||||
t.callExpression(t.identifier(gating.importSpecifierName), []),
|
||||
t.callExpression(t.identifier(gatingFunctionIdentifierName), []),
|
||||
),
|
||||
]),
|
||||
);
|
||||
@@ -129,19 +131,26 @@ export function insertGatedFunctionDeclaration(
|
||||
| t.FunctionDeclaration
|
||||
| t.ArrowFunctionExpression
|
||||
| t.FunctionExpression,
|
||||
gating: NonNullable<PluginOptions['gating']>,
|
||||
programContext: ProgramContext,
|
||||
gating: ExternalFunction,
|
||||
referencedBeforeDeclaration: boolean,
|
||||
): void {
|
||||
const gatingImportedName = programContext.addImportSpecifier(gating).name;
|
||||
if (referencedBeforeDeclaration && fnPath.isFunctionDeclaration()) {
|
||||
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
|
||||
reason: 'Expected compiled node type to match input type',
|
||||
description: `Got ${compiled.type} but expected FunctionDeclaration`,
|
||||
loc: fnPath.node.loc ?? null,
|
||||
});
|
||||
insertAdditionalFunctionDeclaration(fnPath, compiled, gating);
|
||||
insertAdditionalFunctionDeclaration(
|
||||
fnPath,
|
||||
compiled,
|
||||
programContext,
|
||||
gatingImportedName,
|
||||
);
|
||||
} else {
|
||||
const gatingExpression = t.conditionalExpression(
|
||||
t.callExpression(t.identifier(gating.importSpecifierName), []),
|
||||
t.callExpression(t.identifier(gatingImportedName), []),
|
||||
buildFunctionExpression(compiled),
|
||||
buildFunctionExpression(fnPath.node),
|
||||
);
|
||||
|
||||
@@ -7,9 +7,19 @@
|
||||
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {EnvironmentConfig, ExternalFunction, GeneratedSource} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
GeneratedSource,
|
||||
NonLocalImportSpecifier,
|
||||
} from '../HIR';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {CompilerReactTarget} from './Options';
|
||||
import {getReactCompilerRuntimeModule} from './Program';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
path: NodePath<t.Program>,
|
||||
@@ -42,50 +52,209 @@ export function validateRestrictedImports(
|
||||
}
|
||||
}
|
||||
|
||||
export function addImportsToProgram(
|
||||
path: NodePath<t.Program>,
|
||||
importList: Array<ExternalFunction>,
|
||||
): void {
|
||||
const identifiers: Set<string> = new Set();
|
||||
const sortedImports: Map<string, Array<string>> = new Map();
|
||||
for (const {importSpecifierName, source} of importList) {
|
||||
/*
|
||||
* Codegen currently does not rename import specifiers, so we do additional
|
||||
* validation here
|
||||
*/
|
||||
CompilerError.invariant(identifiers.has(importSpecifierName) === false, {
|
||||
reason: `Encountered conflicting import specifier for ${importSpecifierName} in Forget config.`,
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
CompilerError.invariant(
|
||||
path.scope.hasBinding(importSpecifierName) === false,
|
||||
{
|
||||
reason: `Encountered conflicting import specifiers for ${importSpecifierName} in generated program.`,
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
identifiers.add(importSpecifierName);
|
||||
export class ProgramContext {
|
||||
/* Program and environment context */
|
||||
scope: BabelScope;
|
||||
reactRuntimeModule: string;
|
||||
hookPattern: string | null;
|
||||
|
||||
const importSpecifierNameList = getOrInsertDefault(
|
||||
sortedImports,
|
||||
source,
|
||||
[],
|
||||
);
|
||||
importSpecifierNameList.push(importSpecifierName);
|
||||
// known generated or referenced identifiers in the program
|
||||
knownReferencedNames: Set<string> = new Set();
|
||||
// generated imports
|
||||
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
|
||||
|
||||
constructor(
|
||||
program: NodePath<t.Program>,
|
||||
reactRuntimeModule: CompilerReactTarget,
|
||||
hookPattern: string | null,
|
||||
) {
|
||||
this.hookPattern = hookPattern;
|
||||
this.scope = program.scope;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
|
||||
}
|
||||
|
||||
isHookName(name: string): boolean {
|
||||
if (this.hookPattern == null) {
|
||||
return isHookName(name);
|
||||
} else {
|
||||
const match = new RegExp(this.hookPattern).exec(name);
|
||||
return (
|
||||
match != null && typeof match[1] === 'string' && isHookName(match[1])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
hasReference(name: string): boolean {
|
||||
return (
|
||||
this.knownReferencedNames.has(name) ||
|
||||
this.scope.hasBinding(name) ||
|
||||
this.scope.hasGlobal(name) ||
|
||||
this.scope.hasReference(name)
|
||||
);
|
||||
}
|
||||
|
||||
newUid(name: string): string {
|
||||
/**
|
||||
* Don't call babel's generateUid for known hook imports, as
|
||||
* InferTypes might eventually type `HookKind` based on callee naming
|
||||
* convention and `_useFoo` is not named as a hook.
|
||||
*
|
||||
* Local uid generation is susceptible to check-before-use bugs since we're
|
||||
* checking for naming conflicts / references long before we actually insert
|
||||
* the import. (see similar logic in HIRBuilder:resolveBinding)
|
||||
*/
|
||||
let uid;
|
||||
if (this.isHookName(name)) {
|
||||
uid = name;
|
||||
let i = 0;
|
||||
while (this.hasReference(uid)) {
|
||||
this.knownReferencedNames.add(uid);
|
||||
uid = `${name}_${i++}`;
|
||||
}
|
||||
} else if (!this.hasReference(name)) {
|
||||
uid = name;
|
||||
} else {
|
||||
uid = this.scope.generateUid(name);
|
||||
}
|
||||
this.knownReferencedNames.add(uid);
|
||||
return uid;
|
||||
}
|
||||
|
||||
addMemoCacheImport(): NonLocalImportSpecifier {
|
||||
return this.addImportSpecifier(
|
||||
{
|
||||
source: this.reactRuntimeModule,
|
||||
importSpecifierName: 'c',
|
||||
},
|
||||
'_c',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param externalFunction
|
||||
* @param nameHint if defined, will be used as the name of the import specifier
|
||||
* @returns
|
||||
*/
|
||||
addImportSpecifier(
|
||||
{source: module, importSpecifierName: specifier}: ExternalFunction,
|
||||
nameHint?: string,
|
||||
): NonLocalImportSpecifier {
|
||||
const maybeBinding = this.imports.get(module)?.get(specifier);
|
||||
if (maybeBinding != null) {
|
||||
return {...maybeBinding};
|
||||
}
|
||||
|
||||
const binding: NonLocalImportSpecifier = {
|
||||
kind: 'ImportSpecifier',
|
||||
name: this.newUid(nameHint ?? specifier),
|
||||
module,
|
||||
imported: specifier,
|
||||
};
|
||||
getOrInsertWith(this.imports, module, () => new Map()).set(specifier, {
|
||||
...binding,
|
||||
});
|
||||
return binding;
|
||||
}
|
||||
|
||||
addNewReference(name: string): void {
|
||||
this.knownReferencedNames.add(name);
|
||||
}
|
||||
|
||||
assertGlobalBinding(
|
||||
name: string,
|
||||
localScope?: BabelScope,
|
||||
): Result<void, CompilerError> {
|
||||
const scope = localScope ?? this.scope;
|
||||
if (!scope.hasReference(name) && !scope.hasBinding(name)) {
|
||||
return Ok(undefined);
|
||||
}
|
||||
const error = new CompilerError();
|
||||
error.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Encountered conflicting global in generated program',
|
||||
description: `Conflict from local binding ${name}`,
|
||||
loc: scope.getBinding(name)?.path.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingImports(
|
||||
program: NodePath<t.Program>,
|
||||
): Map<string, NodePath<t.ImportDeclaration>> {
|
||||
const existingImports = new Map<string, NodePath<t.ImportDeclaration>>();
|
||||
program.traverse({
|
||||
ImportDeclaration(path) {
|
||||
if (isNonNamespacedImport(path)) {
|
||||
existingImports.set(path.node.source.value, path);
|
||||
}
|
||||
},
|
||||
});
|
||||
return existingImports;
|
||||
}
|
||||
|
||||
export function addImportsToProgram(
|
||||
path: NodePath<t.Program>,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const existingImports = getExistingImports(path);
|
||||
const stmts: Array<t.ImportDeclaration> = [];
|
||||
for (const [source, importSpecifierNameList] of sortedImports) {
|
||||
const importSpecifiers = importSpecifierNameList.map(name => {
|
||||
const id = t.identifier(name);
|
||||
return t.importSpecifier(id, id);
|
||||
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
for (const [moduleName, importsMap] of sortedModules) {
|
||||
for (const [specifierName, loweredImport] of importsMap) {
|
||||
/**
|
||||
* Assert that the import identifier hasn't already be declared in the program.
|
||||
* Note: we use getBinding here since `Scope.hasBinding` pessimistically returns true
|
||||
* for all allocated uids (from `Scope.getUid`)
|
||||
*/
|
||||
CompilerError.invariant(
|
||||
path.scope.getBinding(loweredImport.name) == null,
|
||||
{
|
||||
reason:
|
||||
'Encountered conflicting import specifiers in generated program',
|
||||
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
CompilerError.invariant(
|
||||
loweredImport.module === moduleName &&
|
||||
loweredImport.imported === specifierName,
|
||||
{
|
||||
reason:
|
||||
'Found inconsistent import specifier. This is an internal bug.',
|
||||
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
}
|
||||
const sortedImport: Array<NonLocalImportSpecifier> = [
|
||||
...importsMap.values(),
|
||||
].sort(({imported: a}, {imported: b}) => a.localeCompare(b));
|
||||
const importSpecifiers = sortedImport.map(specifier => {
|
||||
return t.importSpecifier(
|
||||
t.identifier(specifier.name),
|
||||
t.identifier(specifier.imported),
|
||||
);
|
||||
});
|
||||
|
||||
stmts.push(t.importDeclaration(importSpecifiers, t.stringLiteral(source)));
|
||||
/**
|
||||
* If an existing import of this module exists (ie `import { ... } from
|
||||
* '<moduleName>'`), inject new imported specifiers into the list of
|
||||
* destructured variables.
|
||||
*/
|
||||
const maybeExistingImports = existingImports.get(moduleName);
|
||||
if (maybeExistingImports != null) {
|
||||
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
|
||||
} else {
|
||||
stmts.push(
|
||||
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
|
||||
);
|
||||
}
|
||||
}
|
||||
path.unshiftContainer('body', stmts);
|
||||
}
|
||||
@@ -93,13 +262,12 @@ export function addImportsToProgram(
|
||||
/*
|
||||
* Matches `import { ... } from <moduleName>;`
|
||||
* but not `import * as React from <moduleName>;`
|
||||
* `import type { Foo } from <moduleName>;`
|
||||
*/
|
||||
function isNonNamespacedImport(
|
||||
importDeclPath: NodePath<t.ImportDeclaration>,
|
||||
moduleName: string,
|
||||
): boolean {
|
||||
return (
|
||||
importDeclPath.get('source').node.value === moduleName &&
|
||||
importDeclPath
|
||||
.get('specifiers')
|
||||
.every(specifier => specifier.isImportSpecifier()) &&
|
||||
@@ -107,94 +275,3 @@ function isNonNamespacedImport(
|
||||
importDeclPath.node.importKind !== 'typeof'
|
||||
);
|
||||
}
|
||||
|
||||
function hasExistingNonNamespacedImportOfModule(
|
||||
program: NodePath<t.Program>,
|
||||
moduleName: string,
|
||||
): boolean {
|
||||
let hasExistingImport = false;
|
||||
program.traverse({
|
||||
ImportDeclaration(importDeclPath) {
|
||||
if (isNonNamespacedImport(importDeclPath, moduleName)) {
|
||||
hasExistingImport = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return hasExistingImport;
|
||||
}
|
||||
|
||||
/*
|
||||
* If an existing import of React exists (ie `import { ... } from '<moduleName>'`), inject useMemoCache
|
||||
* into the list of destructured variables.
|
||||
*/
|
||||
function addMemoCacheFunctionSpecifierToExistingImport(
|
||||
program: NodePath<t.Program>,
|
||||
moduleName: string,
|
||||
identifierName: string,
|
||||
): boolean {
|
||||
let didInsertUseMemoCache = false;
|
||||
program.traverse({
|
||||
ImportDeclaration(importDeclPath) {
|
||||
if (
|
||||
!didInsertUseMemoCache &&
|
||||
isNonNamespacedImport(importDeclPath, moduleName)
|
||||
) {
|
||||
importDeclPath.pushContainer(
|
||||
'specifiers',
|
||||
t.importSpecifier(t.identifier(identifierName), t.identifier('c')),
|
||||
);
|
||||
didInsertUseMemoCache = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
return didInsertUseMemoCache;
|
||||
}
|
||||
|
||||
export function updateMemoCacheFunctionImport(
|
||||
program: NodePath<t.Program>,
|
||||
moduleName: string,
|
||||
useMemoCacheIdentifier: string,
|
||||
): void {
|
||||
/*
|
||||
* If there isn't already an import of * as React, insert it so useMemoCache doesn't
|
||||
* throw
|
||||
*/
|
||||
const hasExistingImport = hasExistingNonNamespacedImportOfModule(
|
||||
program,
|
||||
moduleName,
|
||||
);
|
||||
|
||||
if (hasExistingImport) {
|
||||
const didUpdateImport = addMemoCacheFunctionSpecifierToExistingImport(
|
||||
program,
|
||||
moduleName,
|
||||
useMemoCacheIdentifier,
|
||||
);
|
||||
if (!didUpdateImport) {
|
||||
throw new Error(
|
||||
`Expected an ImportDeclaration of \`${moduleName}\` in order to update ImportSpecifiers with useMemoCache`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
addMemoCacheFunctionImportDeclaration(
|
||||
program,
|
||||
moduleName,
|
||||
useMemoCacheIdentifier,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addMemoCacheFunctionImportDeclaration(
|
||||
program: NodePath<t.Program>,
|
||||
moduleName: string,
|
||||
localName: string,
|
||||
): void {
|
||||
program.unshiftContainer(
|
||||
'body',
|
||||
t.importDeclaration(
|
||||
[t.importSpecifier(t.identifier(localName), t.identifier('c'))],
|
||||
t.stringLiteral(moduleName),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import prettyFormat from 'pretty-format';
|
||||
import {Logger} from '.';
|
||||
import {Logger, ProgramContext} from '.';
|
||||
import {
|
||||
HIRFunction,
|
||||
ReactiveFunction,
|
||||
@@ -100,8 +100,9 @@ import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHI
|
||||
import {outlineJSX} from '../Optimization/OutlineJsx';
|
||||
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
|
||||
import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValiateNoImpureFunctionsInRender';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -116,7 +117,7 @@ function run(
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
mode: CompilerMode,
|
||||
useMemoCacheIdentifier: string,
|
||||
programContext: ProgramContext,
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
@@ -131,7 +132,7 @@ function run(
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
useMemoCacheIdentifier,
|
||||
programContext,
|
||||
);
|
||||
env.logger?.debugLogIRs?.({
|
||||
kind: 'debug',
|
||||
@@ -161,7 +162,7 @@ function runWithEnvironment(
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
validateContextVariableLValues(hir);
|
||||
validateUseMemo(hir);
|
||||
validateUseMemo(hir).unwrap();
|
||||
|
||||
if (
|
||||
env.isInferredMemoEnabled &&
|
||||
@@ -202,10 +203,10 @@ function runWithEnvironment(
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.validateHooksUsage) {
|
||||
validateHooksUsage(hir);
|
||||
validateHooksUsage(hir).unwrap();
|
||||
}
|
||||
if (env.config.validateNoCapitalizedCalls) {
|
||||
validateNoCapitalizedCalls(hir);
|
||||
validateNoCapitalizedCalls(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,23 +256,23 @@ function runWithEnvironment(
|
||||
}
|
||||
|
||||
if (env.config.validateRefAccessDuringRender) {
|
||||
validateNoRefAccessInRender(hir);
|
||||
validateNoRefAccessInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInRender) {
|
||||
validateNoSetStateInRender(hir);
|
||||
validateNoSetStateInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInPassiveEffects) {
|
||||
validateNoSetStateInPassiveEffects(hir);
|
||||
env.logErrors(validateNoSetStateInPassiveEffects(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
validateNoJSXInTryStatement(hir);
|
||||
env.logErrors(validateNoJSXInTryStatement(hir));
|
||||
}
|
||||
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir);
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +294,10 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.validateStaticComponents) {
|
||||
env.logErrors(validateStaticComponents(hir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only create reactive scopes (which directly map to generated memo blocks)
|
||||
* if inferred memoization is enabled. This makes all later passes which
|
||||
@@ -509,14 +514,14 @@ function runWithEnvironment(
|
||||
});
|
||||
|
||||
if (env.config.validateMemoizedEffectDependencies) {
|
||||
validateMemoizedEffectDependencies(reactiveFunction);
|
||||
validateMemoizedEffectDependencies(reactiveFunction).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
env.config.validatePreserveExistingMemoizationGuarantees
|
||||
) {
|
||||
validatePreservedManualMemoization(reactiveFunction);
|
||||
validatePreservedManualMemoization(reactiveFunction).unwrap();
|
||||
}
|
||||
|
||||
const ast = codegenFunction(reactiveFunction, {
|
||||
@@ -547,7 +552,7 @@ export function compileFn(
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
mode: CompilerMode,
|
||||
useMemoCacheIdentifier: string,
|
||||
programContext: ProgramContext,
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
@@ -557,7 +562,7 @@ export function compileFn(
|
||||
config,
|
||||
fnType,
|
||||
mode,
|
||||
useMemoCacheIdentifier,
|
||||
programContext,
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
|
||||
@@ -12,11 +12,7 @@ import {
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
ExternalFunction,
|
||||
ReactFunctionType,
|
||||
} from '../HIR/Environment';
|
||||
import {EnvironmentConfig, ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
|
||||
import {isHookDeclaration} from '../Utils/HookDeclaration';
|
||||
@@ -24,10 +20,10 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
import {insertGatedFunctionDeclaration} from './Gating';
|
||||
import {
|
||||
addImportsToProgram,
|
||||
updateMemoCacheFunctionImport,
|
||||
ProgramContext,
|
||||
validateRestrictedImports,
|
||||
} from './Imports';
|
||||
import {PluginOptions} from './Options';
|
||||
import {CompilerReactTarget, PluginOptions} from './Options';
|
||||
import {compileFn} from './Pipeline';
|
||||
import {
|
||||
filterSuppressionsThatAffectFunction,
|
||||
@@ -299,8 +295,12 @@ export function compileProgram(
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return null;
|
||||
}
|
||||
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
|
||||
|
||||
const programContext = new ProgramContext(
|
||||
program,
|
||||
pass.opts.target,
|
||||
environment.hookPattern,
|
||||
);
|
||||
/*
|
||||
* Record lint errors and critical errors as depending on Forget's config,
|
||||
* we may still need to run Forget's analysis on every function (even if we
|
||||
@@ -410,7 +410,7 @@ export function compileProgram(
|
||||
environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
useMemoCacheIdentifier.name,
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
@@ -445,12 +445,18 @@ export function compileProgram(
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
useMemoCacheIdentifier.name,
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
),
|
||||
};
|
||||
if (
|
||||
!compileResult.compiledFn.hasFireRewrite &&
|
||||
!compileResult.compiledFn.hasInferredEffect
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: we might want to log error here, but this will also result in duplicate logging
|
||||
if (err instanceof CompilerError) {
|
||||
@@ -548,79 +554,29 @@ export function compileProgram(
|
||||
if (moduleScopeOptOutDirectives.length > 0) {
|
||||
return null;
|
||||
}
|
||||
let gating: null | {
|
||||
gatingFn: ExternalFunction;
|
||||
referencedBeforeDeclared: Set<CompileResult>;
|
||||
} = null;
|
||||
if (pass.opts.gating != null) {
|
||||
gating = {
|
||||
gatingFn: pass.opts.gating,
|
||||
referencedBeforeDeclared:
|
||||
getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns),
|
||||
};
|
||||
}
|
||||
|
||||
const hasLoweredContextAccess = compiledFns.some(
|
||||
c => c.compiledFn.hasLoweredContextAccess,
|
||||
);
|
||||
const externalFunctions: Array<ExternalFunction> = [];
|
||||
try {
|
||||
// TODO: check for duplicate import specifiers
|
||||
if (gating != null) {
|
||||
externalFunctions.push(gating.gatingFn);
|
||||
}
|
||||
|
||||
const lowerContextAccess = environment.lowerContextAccess;
|
||||
if (lowerContextAccess && hasLoweredContextAccess) {
|
||||
externalFunctions.push(lowerContextAccess);
|
||||
}
|
||||
|
||||
const enableEmitInstrumentForget = environment.enableEmitInstrumentForget;
|
||||
if (enableEmitInstrumentForget != null) {
|
||||
externalFunctions.push(enableEmitInstrumentForget.fn);
|
||||
if (enableEmitInstrumentForget.gating != null) {
|
||||
externalFunctions.push(enableEmitInstrumentForget.gating);
|
||||
}
|
||||
}
|
||||
|
||||
if (environment.enableEmitFreeze != null) {
|
||||
externalFunctions.push(environment.enableEmitFreeze);
|
||||
}
|
||||
|
||||
if (environment.enableEmitHookGuards != null) {
|
||||
externalFunctions.push(environment.enableEmitHookGuards);
|
||||
}
|
||||
|
||||
if (environment.enableChangeDetectionForDebugging != null) {
|
||||
externalFunctions.push(environment.enableChangeDetectionForDebugging);
|
||||
}
|
||||
|
||||
const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite);
|
||||
if (environment.enableFire && hasFireRewrite) {
|
||||
externalFunctions.push({
|
||||
source: getReactCompilerRuntimeModule(pass.opts),
|
||||
importSpecifierName: 'useFire',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err, pass, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Only insert Forget-ified functions if we have not encountered a critical
|
||||
* error elsewhere in the file, regardless of bailout mode.
|
||||
*/
|
||||
const referencedBeforeDeclared =
|
||||
pass.opts.gating != null
|
||||
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
|
||||
: null;
|
||||
for (const result of compiledFns) {
|
||||
const {kind, originalFn, compiledFn} = result;
|
||||
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
|
||||
|
||||
if (gating != null && kind === 'original') {
|
||||
if (referencedBeforeDeclared != null && kind === 'original') {
|
||||
CompilerError.invariant(pass.opts.gating != null, {
|
||||
reason: "Expected 'gating' import to be present",
|
||||
loc: null,
|
||||
});
|
||||
insertGatedFunctionDeclaration(
|
||||
originalFn,
|
||||
transformedFn,
|
||||
gating.gatingFn,
|
||||
gating.referencedBeforeDeclared.has(result),
|
||||
programContext,
|
||||
pass.opts.gating,
|
||||
referencedBeforeDeclared.has(result),
|
||||
);
|
||||
} else {
|
||||
originalFn.replaceWith(transformedFn);
|
||||
@@ -629,22 +585,7 @@ export function compileProgram(
|
||||
|
||||
// Forget compiled the component, we need to update existing imports of useMemoCache
|
||||
if (compiledFns.length > 0) {
|
||||
let needsMemoCacheFunctionImport = false;
|
||||
for (const fn of compiledFns) {
|
||||
if (fn.compiledFn.memoSlotsUsed > 0) {
|
||||
needsMemoCacheFunctionImport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMemoCacheFunctionImport) {
|
||||
updateMemoCacheFunctionImport(
|
||||
program,
|
||||
getReactCompilerRuntimeModule(pass.opts),
|
||||
useMemoCacheIdentifier.name,
|
||||
);
|
||||
}
|
||||
addImportsToProgram(program, externalFunctions);
|
||||
addImportsToProgram(program, programContext);
|
||||
}
|
||||
return {retryErrors};
|
||||
}
|
||||
@@ -677,7 +618,7 @@ function shouldSkipCompilation(
|
||||
if (
|
||||
hasMemoCacheFunctionImport(
|
||||
program,
|
||||
getReactCompilerRuntimeModule(pass.opts),
|
||||
getReactCompilerRuntimeModule(pass.opts.target),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
@@ -1002,31 +943,39 @@ function callsHooksOrCreatesJsx(
|
||||
return invokesHooks || createsJsx;
|
||||
}
|
||||
|
||||
function isNonNode(node?: t.Expression | null): boolean {
|
||||
if (!node) {
|
||||
return true;
|
||||
}
|
||||
switch (node.type) {
|
||||
case 'ObjectExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'FunctionExpression':
|
||||
case 'BigIntLiteral':
|
||||
case 'ClassExpression':
|
||||
case 'NewExpression': // technically `new Array()` is legit, but unlikely
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function returnsNonNode(
|
||||
node: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
): boolean {
|
||||
let hasReturn = false;
|
||||
let returnsNonNode = false;
|
||||
if (
|
||||
// node.traverse#ArrowFunctionExpression isn't called for the root node
|
||||
node.type === 'ArrowFunctionExpression' &&
|
||||
node.node.body.type !== 'BlockStatement'
|
||||
) {
|
||||
returnsNonNode = isNonNode(node.node.body);
|
||||
}
|
||||
|
||||
node.traverse({
|
||||
ReturnStatement(ret) {
|
||||
hasReturn = true;
|
||||
const argument = ret.node.argument;
|
||||
if (argument == null) {
|
||||
returnsNonNode = true;
|
||||
} else {
|
||||
switch (argument.type) {
|
||||
case 'ObjectExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'FunctionExpression':
|
||||
case 'BigIntLiteral':
|
||||
case 'ClassExpression':
|
||||
case 'NewExpression': // technically `new Array()` is legit, but unlikely
|
||||
returnsNonNode = true;
|
||||
}
|
||||
}
|
||||
returnsNonNode = isNonNode(ret.node.argument);
|
||||
},
|
||||
// Skip traversing all nested functions and their return statements
|
||||
ArrowFunctionExpression: skipNestedFunctions(node),
|
||||
@@ -1035,7 +984,7 @@ function returnsNonNode(
|
||||
ObjectMethod: node => node.skip(),
|
||||
});
|
||||
|
||||
return !hasReturn || returnsNonNode;
|
||||
return returnsNonNode;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1163,16 +1112,18 @@ function getFunctionReferencedBeforeDeclarationAtTopLevel(
|
||||
return referencedBeforeDeclaration;
|
||||
}
|
||||
|
||||
function getReactCompilerRuntimeModule(opts: PluginOptions): string {
|
||||
if (opts.target === '19') {
|
||||
export function getReactCompilerRuntimeModule(
|
||||
target: CompilerReactTarget,
|
||||
): string {
|
||||
if (target === '19') {
|
||||
return 'react/compiler-runtime'; // from react namespace
|
||||
} else if (opts.target === '17' || opts.target === '18') {
|
||||
} else if (target === '17' || target === '18') {
|
||||
return 'react-compiler-runtime'; // npm package
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
opts.target != null &&
|
||||
opts.target.kind === 'donotuse_meta_internal' &&
|
||||
typeof opts.target.runtimeModule === 'string',
|
||||
target != null &&
|
||||
target.kind === 'donotuse_meta_internal' &&
|
||||
typeof target.runtimeModule === 'string',
|
||||
{
|
||||
reason: 'Expected target to already be validated',
|
||||
description: null,
|
||||
@@ -1180,6 +1131,6 @@ function getReactCompilerRuntimeModule(opts: PluginOptions): string {
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
return opts.target.runtimeModule;
|
||||
return target.runtimeModule;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
PanicThresholdOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
ProgramContext,
|
||||
} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
@@ -84,6 +85,8 @@ export const InstrumentationSchema = z
|
||||
);
|
||||
|
||||
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
export const USE_FIRE_FUNCTION_NAME = 'useFire';
|
||||
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
|
||||
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
@@ -330,6 +333,11 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoJSXInTryStatements: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against dynamically creating components during render.
|
||||
*/
|
||||
validateStaticComponents: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that the dependencies of all effect hooks are memoized. This helps ensure
|
||||
* that Forget does not introduce infinite renders caused by a dependency changing,
|
||||
@@ -841,9 +849,9 @@ export class Environment {
|
||||
config: EnvironmentConfig;
|
||||
fnType: ReactFunctionType;
|
||||
compilerMode: CompilerMode;
|
||||
useMemoCacheIdentifier: string;
|
||||
hasLoweredContextAccess: boolean;
|
||||
programContext: ProgramContext;
|
||||
hasFireRewrite: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
@@ -857,7 +865,7 @@ export class Environment {
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
useMemoCacheIdentifier: string,
|
||||
programContext: ProgramContext,
|
||||
) {
|
||||
this.#scope = scope;
|
||||
this.fnType = fnType;
|
||||
@@ -866,11 +874,11 @@ export class Environment {
|
||||
this.filename = filename;
|
||||
this.code = code;
|
||||
this.logger = logger;
|
||||
this.useMemoCacheIdentifier = useMemoCacheIdentifier;
|
||||
this.programContext = programContext;
|
||||
this.#shapes = new Map(DEFAULT_SHAPES);
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
this.hasLoweredContextAccess = false;
|
||||
this.hasFireRewrite = false;
|
||||
this.hasInferredEffect = false;
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
@@ -932,6 +940,23 @@ export class Environment {
|
||||
return makeScopeId(this.#nextScope++);
|
||||
}
|
||||
|
||||
get scope(): BabelScope {
|
||||
return this.#scope;
|
||||
}
|
||||
|
||||
logErrors(errors: Result<void, CompilerError>): void {
|
||||
if (errors.isOk() || this.logger == null) {
|
||||
return;
|
||||
}
|
||||
for (const error of errors.unwrapErr().details) {
|
||||
this.logger.logEvent(this.filename, {
|
||||
kind: 'CompileError',
|
||||
detail: error,
|
||||
fnLoc: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isContextIdentifier(node: t.Identifier): boolean {
|
||||
return this.#contextIdentifiers.has(node);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInFireId,
|
||||
BuiltInMapId,
|
||||
BuiltInMixedReadonlyId,
|
||||
BuiltInObjectId,
|
||||
BuiltInSetId,
|
||||
BuiltInUseActionStateId,
|
||||
BuiltInUseContextHookId,
|
||||
BuiltInUseEffectHookId,
|
||||
@@ -45,29 +48,23 @@ export const DEFAULT_SHAPES: ShapeRegistry = new Map(BUILTIN_SHAPES);
|
||||
|
||||
// Hack until we add ObjectShapes for all globals
|
||||
const UNTYPED_GLOBALS: Set<string> = new Set([
|
||||
'String',
|
||||
'Object',
|
||||
'Function',
|
||||
'Number',
|
||||
'RegExp',
|
||||
'Date',
|
||||
'Error',
|
||||
'Function',
|
||||
'TypeError',
|
||||
'RangeError',
|
||||
'ReferenceError',
|
||||
'SyntaxError',
|
||||
'URIError',
|
||||
'EvalError',
|
||||
'Boolean',
|
||||
'DataView',
|
||||
'Float32Array',
|
||||
'Float64Array',
|
||||
'Int8Array',
|
||||
'Int16Array',
|
||||
'Int32Array',
|
||||
'Map',
|
||||
'Set',
|
||||
'WeakMap',
|
||||
'Uint8Array',
|
||||
'Uint8ClampedArray',
|
||||
@@ -75,16 +72,8 @@ const UNTYPED_GLOBALS: Set<string> = new Set([
|
||||
'Uint32Array',
|
||||
'ArrayBuffer',
|
||||
'JSON',
|
||||
'parseFloat',
|
||||
'parseInt',
|
||||
'console',
|
||||
'isNaN',
|
||||
'eval',
|
||||
'isFinite',
|
||||
'encodeURI',
|
||||
'decodeURI',
|
||||
'encodeURIComponent',
|
||||
'decodeURIComponent',
|
||||
]);
|
||||
|
||||
const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
@@ -101,6 +90,23 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* Object.fromEntries(iterable)
|
||||
* iterable: An iterable, such as an Array or Map, containing a list of
|
||||
* objects. Each object should have two properties.
|
||||
* Returns a new object whose properties are given by the entries of the
|
||||
* iterable.
|
||||
*/
|
||||
'fromEntries',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [Effect.ConditionallyMutate],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInObjectId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
@@ -132,7 +138,7 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
'from',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [
|
||||
Effect.ConditionallyMutate,
|
||||
Effect.ConditionallyMutateIterator,
|
||||
Effect.ConditionallyMutate,
|
||||
Effect.ConditionallyMutate,
|
||||
],
|
||||
@@ -372,6 +378,118 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'parseInt',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'parseFloat',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'isNaN',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'isFinite',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'encodeURI',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'encodeURIComponent',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'decodeURI',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'decodeURIComponent',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'Map',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [Effect.ConditionallyMutateIterator],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInMapId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
null,
|
||||
true,
|
||||
),
|
||||
],
|
||||
[
|
||||
'Set',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [Effect.ConditionallyMutateIterator],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInSetId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
null,
|
||||
true,
|
||||
),
|
||||
],
|
||||
// TODO: rest of Global objects
|
||||
];
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as t from '@babel/types';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import {HookKind} from './ObjectShape';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
|
||||
@@ -746,6 +746,27 @@ export enum InstructionKind {
|
||||
Function = 'Function',
|
||||
}
|
||||
|
||||
export function convertHoistedLValueKind(
|
||||
kind: InstructionKind,
|
||||
): InstructionKind | null {
|
||||
switch (kind) {
|
||||
case InstructionKind.HoistedLet:
|
||||
return InstructionKind.Let;
|
||||
case InstructionKind.HoistedConst:
|
||||
return InstructionKind.Const;
|
||||
case InstructionKind.HoistedFunction:
|
||||
return InstructionKind.Function;
|
||||
case InstructionKind.Let:
|
||||
case InstructionKind.Const:
|
||||
case InstructionKind.Function:
|
||||
case InstructionKind.Reassign:
|
||||
case InstructionKind.Catch:
|
||||
return null;
|
||||
default:
|
||||
assertExhaustive(kind, 'Unexpected lvalue kind');
|
||||
}
|
||||
}
|
||||
|
||||
function _staticInvariantInstructionValueHasLocation(
|
||||
value: InstructionValue,
|
||||
): SourceLocation {
|
||||
@@ -829,6 +850,13 @@ export type CallExpression = {
|
||||
typeArguments?: Array<t.FlowType>;
|
||||
};
|
||||
|
||||
export type NewExpression = {
|
||||
kind: 'NewExpression';
|
||||
callee: Place;
|
||||
args: Array<Place | SpreadPattern>;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
export type LoadLocal = {
|
||||
kind: 'LoadLocal';
|
||||
place: Place;
|
||||
@@ -894,12 +922,7 @@ export type InstructionValue =
|
||||
right: Place;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {
|
||||
kind: 'NewExpression';
|
||||
callee: Place;
|
||||
args: Array<Place | SpreadPattern>;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| NewExpression
|
||||
| CallExpression
|
||||
| MethodCall
|
||||
| {
|
||||
@@ -1165,18 +1188,21 @@ export type VariableBinding =
|
||||
// bindings declard outside the current component/hook
|
||||
| NonLocalBinding;
|
||||
|
||||
// `import {bar as baz} from 'foo'`: name=baz, module=foo, imported=bar
|
||||
export type NonLocalImportSpecifier = {
|
||||
kind: 'ImportSpecifier';
|
||||
name: string;
|
||||
module: string;
|
||||
imported: string;
|
||||
};
|
||||
|
||||
export type NonLocalBinding =
|
||||
// `import Foo from 'foo'`: name=Foo, module=foo
|
||||
| {kind: 'ImportDefault'; name: string; module: string}
|
||||
// `import * as Foo from 'foo'`: name=Foo, module=foo
|
||||
| {kind: 'ImportNamespace'; name: string; module: string}
|
||||
// `import {bar as baz} from 'foo'`: name=baz, module=foo, imported=bar
|
||||
| {
|
||||
kind: 'ImportSpecifier';
|
||||
name: string;
|
||||
module: string;
|
||||
imported: string;
|
||||
}
|
||||
// `import {bar as baz} from 'foo'`
|
||||
| NonLocalImportSpecifier
|
||||
// let, const, function, etc declared in the module but outside the current component/hook
|
||||
| {kind: 'ModuleLocal'; name: string}
|
||||
// an unresolved binding
|
||||
@@ -1394,6 +1420,7 @@ export enum Effect {
|
||||
Read = 'read',
|
||||
// This reference reads and stores the value
|
||||
Capture = 'capture',
|
||||
ConditionallyMutateIterator = 'mutate-iterator?',
|
||||
/*
|
||||
* This reference *may* write to (mutate) the value. This covers two similar cases:
|
||||
* - The compiler is being conservative and assuming that a value *may* be mutated
|
||||
@@ -1412,11 +1439,11 @@ export enum Effect {
|
||||
// This reference may alias to (mutate) the value
|
||||
Store = 'store',
|
||||
}
|
||||
|
||||
export const EffectSchema = z.enum([
|
||||
Effect.Read,
|
||||
Effect.Mutate,
|
||||
Effect.ConditionallyMutate,
|
||||
Effect.ConditionallyMutateIterator,
|
||||
Effect.Capture,
|
||||
Effect.Store,
|
||||
Effect.Freeze,
|
||||
@@ -1430,6 +1457,7 @@ export function isMutableEffect(
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
@@ -1649,6 +1677,14 @@ export function isArrayType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
|
||||
}
|
||||
|
||||
export function isMapType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInMap';
|
||||
}
|
||||
|
||||
export function isSetType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInSet';
|
||||
}
|
||||
|
||||
export function isPropsType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInProps';
|
||||
}
|
||||
|
||||
@@ -331,6 +331,7 @@ export default class HIRBuilder {
|
||||
type: makeType(),
|
||||
loc: node.loc ?? GeneratedSource,
|
||||
};
|
||||
this.#env.programContext.addNewReference(name);
|
||||
this.#bindings.set(name, {node, identifier});
|
||||
return identifier;
|
||||
} else if (mapping.node === node) {
|
||||
|
||||
@@ -44,6 +44,7 @@ export function addFunction(
|
||||
properties: Iterable<[string, BuiltInType | PolyType]>,
|
||||
fn: Omit<FunctionSignature, 'hookKind'>,
|
||||
id: string | null = null,
|
||||
isConstructor: boolean = false,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
addShape(registry, shapeId, properties, {
|
||||
@@ -54,6 +55,7 @@ export function addFunction(
|
||||
kind: 'Function',
|
||||
return: fn.returnType,
|
||||
shapeId,
|
||||
isConstructor,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ export function addHook(
|
||||
kind: 'Function',
|
||||
return: fn.returnType,
|
||||
shapeId,
|
||||
isConstructor: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,6 +201,8 @@ export type ObjectShape = {
|
||||
export type ShapeRegistry = Map<string, ObjectShape>;
|
||||
export const BuiltInPropsId = 'BuiltInProps';
|
||||
export const BuiltInArrayId = 'BuiltInArray';
|
||||
export const BuiltInSetId = 'BuiltInSet';
|
||||
export const BuiltInMapId = 'BuiltInMap';
|
||||
export const BuiltInFunctionId = 'BuiltInFunction';
|
||||
export const BuiltInJsxId = 'BuiltInJsx';
|
||||
export const BuiltInObjectId = 'BuiltInObject';
|
||||
@@ -451,6 +456,313 @@ addObject(BUILTIN_SHAPES, BuiltInObjectId, [
|
||||
*/
|
||||
]);
|
||||
|
||||
/* Built-in Set shape */
|
||||
addObject(BUILTIN_SHAPES, BuiltInSetId, [
|
||||
[
|
||||
/**
|
||||
* add(value)
|
||||
* Parameters
|
||||
* value: the value of the element to add to the Set object.
|
||||
* Returns the Set object with added value.
|
||||
*/
|
||||
'add',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInSetId},
|
||||
calleeEffect: Effect.Store,
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* clear()
|
||||
* Parameters none
|
||||
* Returns undefined
|
||||
*/
|
||||
'clear',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* setInstance.delete(value)
|
||||
* Returns true if value was already in Set; otherwise false.
|
||||
*/
|
||||
'delete',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'has',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
['size', PRIMITIVE_TYPE],
|
||||
[
|
||||
/**
|
||||
* difference(other)
|
||||
* Parameters
|
||||
* other: A Set object, or set-like object.
|
||||
* Returns a new Set object containing elements in this set but not in the other set.
|
||||
*/
|
||||
'difference',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInSetId},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* union(other)
|
||||
* Parameters
|
||||
* other: A Set object, or set-like object.
|
||||
* Returns a new Set object containing elements in either this set or the other set.
|
||||
*/
|
||||
'union',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInSetId},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* symmetricalDifference(other)
|
||||
* Parameters
|
||||
* other: A Set object, or set-like object.
|
||||
* A new Set object containing elements which are in either this set or the other set, but not in both.
|
||||
*/
|
||||
'symmetricalDifference',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInSetId},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* isSubsetOf(other)
|
||||
* Parameters
|
||||
* other: A Set object, or set-like object.
|
||||
* Returns true if all elements in this set are also in the other set, and false otherwise.
|
||||
*/
|
||||
'isSubsetOf',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* isSupersetOf(other)
|
||||
* Parameters
|
||||
* other: A Set object, or set-like object.
|
||||
* Returns true if all elements in the other set are also in this set, and false otherwise.
|
||||
*/
|
||||
'isSupersetOf',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* forEach(callbackFn)
|
||||
* forEach(callbackFn, thisArg)
|
||||
*/
|
||||
'forEach',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
/**
|
||||
* see Array.map explanation for why arguments are marked `ConditionallyMutate`
|
||||
*/
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
}),
|
||||
],
|
||||
/**
|
||||
* Iterators
|
||||
*/
|
||||
[
|
||||
'entries',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'keys',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'values',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
addObject(BUILTIN_SHAPES, BuiltInMapId, [
|
||||
[
|
||||
/**
|
||||
* clear()
|
||||
* Parameters none
|
||||
* Returns undefined
|
||||
*/
|
||||
'clear',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'delete',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'get',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'has',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Read],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* Params
|
||||
* key: the key of the element to add to the Map object. The key may be
|
||||
* any JavaScript type (any primitive value or any type of JavaScript
|
||||
* object).
|
||||
* value: the value of the element to add to the Map object.
|
||||
* Returns the Map object.
|
||||
*/
|
||||
'set',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [Effect.Capture, Effect.Capture],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInMapId},
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
['size', PRIMITIVE_TYPE],
|
||||
[
|
||||
'forEach',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
/**
|
||||
* see Array.map explanation for why arguments are marked `ConditionallyMutate`
|
||||
*/
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
}),
|
||||
],
|
||||
/**
|
||||
* Iterators
|
||||
*/
|
||||
[
|
||||
'entries',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'keys',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'values',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Capture,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
|
||||
['0', {kind: 'Poly'}],
|
||||
[
|
||||
|
||||
@@ -23,7 +23,9 @@ import {
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
} from './HIR';
|
||||
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
keyByScopeId,
|
||||
@@ -464,6 +466,9 @@ class Context {
|
||||
}
|
||||
this.#reassignments.set(identifier, decl);
|
||||
}
|
||||
hasDeclared(identifier: Identifier): boolean {
|
||||
return this.#declarations.has(identifier.declarationId);
|
||||
}
|
||||
|
||||
// Checks if identifier is a valid dependency in the current scope
|
||||
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
|
||||
@@ -662,21 +667,21 @@ function handleInstruction(instr: Instruction, context: Context): void {
|
||||
});
|
||||
} else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') {
|
||||
/*
|
||||
* Some variables may be declared and never initialized. We need
|
||||
* to retain (and hoist) these declarations if they are included
|
||||
* in a reactive scope. One approach is to simply add all `DeclareLocal`s
|
||||
* as scope declarations.
|
||||
* Some variables may be declared and never initialized. We need to retain
|
||||
* (and hoist) these declarations if they are included in a reactive scope.
|
||||
* One approach is to simply add all `DeclareLocal`s as scope declarations.
|
||||
*
|
||||
* Context variables with hoisted declarations only become live after their
|
||||
* first assignment. We only declare real DeclareLocal / DeclareContext
|
||||
* instructions (not hoisted ones) to avoid generating dependencies on
|
||||
* hoisted declarations.
|
||||
*/
|
||||
|
||||
/*
|
||||
* We add context variable declarations here, not at `StoreContext`, since
|
||||
* context Store / Loads are modeled as reads and mutates to the underlying
|
||||
* variable reference (instead of through intermediate / inlined temporaries)
|
||||
*/
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
if (convertHoistedLValueKind(value.lvalue.kind) === null) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'Destructure') {
|
||||
context.visitOperand(value.value);
|
||||
for (const place of eachPatternOperand(value.lvalue.pattern)) {
|
||||
@@ -688,6 +693,23 @@ function handleInstruction(instr: Instruction, context: Context): void {
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else if (value.kind === 'StoreContext') {
|
||||
/**
|
||||
* Some StoreContext variables have hoisted declarations. If we're storing
|
||||
* to a context variable that hasn't yet been declared, the StoreContext is
|
||||
* the declaration.
|
||||
* (see corresponding logic in PruneHoistedContext)
|
||||
*/
|
||||
if (!context.hasDeclared(value.lvalue.place.identifier)) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
|
||||
@@ -38,6 +38,7 @@ export type FunctionType = {
|
||||
kind: 'Function';
|
||||
shapeId: string | null;
|
||||
return: Type;
|
||||
isConstructor: boolean;
|
||||
};
|
||||
|
||||
export type ObjectType = {
|
||||
@@ -111,6 +112,7 @@ export function duplicateType(type: Type): Type {
|
||||
kind: 'Function',
|
||||
return: duplicateType(type.return),
|
||||
shapeId: type.shapeId,
|
||||
isConstructor: type.isConstructor,
|
||||
};
|
||||
}
|
||||
case 'Object': {
|
||||
|
||||
@@ -249,6 +249,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
fn.env.hasInferredEffect = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isRefOrRefValue,
|
||||
isSetType,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
@@ -90,6 +93,17 @@ function inferPlace(
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
const identifier = place.identifier;
|
||||
if (
|
||||
!isArrayType(identifier) &&
|
||||
!isSetType(identifier) &&
|
||||
!isMapType(identifier)
|
||||
) {
|
||||
infer(place, instrId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.Mutate: {
|
||||
infer(place, instrId);
|
||||
|
||||
@@ -230,6 +230,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
case Effect.Capture:
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
if (isMutable(instruction, operand)) {
|
||||
reactiveIdentifiers.markReactive(operand);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
CallExpression,
|
||||
NewExpression,
|
||||
Effect,
|
||||
FunctionEffect,
|
||||
GeneratedSource,
|
||||
@@ -23,12 +24,15 @@ import {
|
||||
Phi,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
TInstruction,
|
||||
Type,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
isArrayType,
|
||||
isMapType,
|
||||
isMutableEffect,
|
||||
isObjectType,
|
||||
isSetType,
|
||||
} from '../HIR/HIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {
|
||||
@@ -38,7 +42,6 @@ import {
|
||||
printSourceLocation,
|
||||
} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachCallArgument,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachPatternOperand,
|
||||
@@ -251,7 +254,7 @@ type FreezeAction = {values: Set<InstructionValue>; reason: Set<ValueReason>};
|
||||
|
||||
// Maintains a mapping of top-level variables to the kind of value they hold
|
||||
class InferenceState {
|
||||
#env: Environment;
|
||||
env: Environment;
|
||||
|
||||
// The kind of each value, based on its allocation site
|
||||
#values: Map<InstructionValue, AbstractValue>;
|
||||
@@ -267,7 +270,7 @@ class InferenceState {
|
||||
values: Map<InstructionValue, AbstractValue>,
|
||||
variables: Map<IdentifierId, Set<InstructionValue>>,
|
||||
) {
|
||||
this.#env = env;
|
||||
this.env = env;
|
||||
this.#values = values;
|
||||
this.#variables = variables;
|
||||
}
|
||||
@@ -409,8 +412,8 @@ class InferenceState {
|
||||
});
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
(this.#env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
this.#env.config.enableTransitivelyFreezeFunctionExpressions)
|
||||
(this.env.config.enablePreserveExistingMemoizationGuarantees ||
|
||||
this.env.config.enableTransitivelyFreezeFunctionExpressions)
|
||||
) {
|
||||
for (const operand of value.loweredFunc.func.context) {
|
||||
const operandValues = this.#variables.get(operand.identifier.id);
|
||||
@@ -468,6 +471,25 @@ class InferenceState {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.ConditionallyMutateIterator: {
|
||||
if (
|
||||
valueKind.kind === ValueKind.Mutable ||
|
||||
valueKind.kind === ValueKind.Context
|
||||
) {
|
||||
if (
|
||||
isArrayType(place.identifier) ||
|
||||
isSetType(place.identifier) ||
|
||||
isMapType(place.identifier)
|
||||
) {
|
||||
effect = Effect.Capture;
|
||||
} else {
|
||||
effect = Effect.ConditionallyMutate;
|
||||
}
|
||||
} else {
|
||||
effect = Effect.Read;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Effect.Mutate: {
|
||||
effect = Effect.Mutate;
|
||||
break;
|
||||
@@ -590,7 +612,7 @@ class InferenceState {
|
||||
return null;
|
||||
} else {
|
||||
return new InferenceState(
|
||||
this.#env,
|
||||
this.env,
|
||||
nextValues ?? new Map(this.#values),
|
||||
nextVariables ?? new Map(this.#variables),
|
||||
);
|
||||
@@ -604,7 +626,7 @@ class InferenceState {
|
||||
*/
|
||||
clone(): InferenceState {
|
||||
return new InferenceState(
|
||||
this.#env,
|
||||
this.env,
|
||||
new Map(this.#values),
|
||||
new Map(this.#variables),
|
||||
);
|
||||
@@ -879,9 +901,7 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
element.place,
|
||||
isArrayType(element.place.identifier)
|
||||
? Effect.Capture
|
||||
: Effect.ConditionallyMutate,
|
||||
Effect.ConditionallyMutateIterator,
|
||||
ValueReason.Other,
|
||||
);
|
||||
} else if (element.kind === 'Identifier') {
|
||||
@@ -904,43 +924,12 @@ function inferBlock(
|
||||
break;
|
||||
}
|
||||
case 'NewExpression': {
|
||||
/**
|
||||
* For new expressions, we infer a `read` effect on the Class / Function type
|
||||
* to avoid extending mutable ranges of locally created classes, e.g.
|
||||
* ```js
|
||||
* const MyClass = getClass();
|
||||
* const value = new MyClass(val1, val2)
|
||||
* ^ (read) ^ (conditionally mutate)
|
||||
* ```
|
||||
*
|
||||
* Risks:
|
||||
* Classes / functions created during render could technically capture and
|
||||
* mutate their enclosing scope, which we currently do not detect.
|
||||
*/
|
||||
const valueKind: AbstractValue = {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
state.referenceAndRecordEffects(
|
||||
inferCallEffects(
|
||||
state,
|
||||
instr as TInstruction<NewExpression>,
|
||||
freezeActions,
|
||||
instrValue.callee,
|
||||
Effect.Read,
|
||||
ValueReason.Other,
|
||||
getFunctionCallSignature(env, instrValue.callee.identifier.type),
|
||||
);
|
||||
|
||||
for (const operand of eachCallArgument(instrValue.args)) {
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
operand,
|
||||
Effect.ConditionallyMutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
}
|
||||
|
||||
state.initialize(instrValue, valueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect = Effect.ConditionallyMutate;
|
||||
continuation = {kind: 'funeffects'};
|
||||
break;
|
||||
}
|
||||
@@ -1238,62 +1227,12 @@ function inferBlock(
|
||||
break;
|
||||
}
|
||||
case 'CallExpression': {
|
||||
const signature = getFunctionCallSignature(
|
||||
env,
|
||||
instrValue.callee.identifier.type,
|
||||
inferCallEffects(
|
||||
state,
|
||||
instr as TInstruction<CallExpression>,
|
||||
freezeActions,
|
||||
getFunctionCallSignature(env, instrValue.callee.identifier.type),
|
||||
);
|
||||
|
||||
const effects =
|
||||
signature !== null ? getFunctionEffects(instrValue, signature) : null;
|
||||
const returnValueKind: AbstractValue =
|
||||
signature !== null
|
||||
? {
|
||||
kind: signature.returnValueKind,
|
||||
reason: new Set([
|
||||
signature.returnValueReason ??
|
||||
ValueReason.KnownReturnSignature,
|
||||
]),
|
||||
context: new Set(),
|
||||
}
|
||||
: {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
let hasCaptureArgument = false;
|
||||
for (let i = 0; i < instrValue.args.length; i++) {
|
||||
const arg = instrValue.args[i];
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
place,
|
||||
getArgumentEffect(effects != null ? effects[i] : null, arg),
|
||||
ValueReason.Other,
|
||||
);
|
||||
hasCaptureArgument ||= place.effect === Effect.Capture;
|
||||
}
|
||||
if (signature !== null) {
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.callee,
|
||||
signature.calleeEffect,
|
||||
ValueReason.Other,
|
||||
);
|
||||
} else {
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.callee,
|
||||
Effect.ConditionallyMutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
}
|
||||
hasCaptureArgument ||= instrValue.callee.effect === Effect.Capture;
|
||||
|
||||
state.initialize(instrValue, returnValueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect = hasCaptureArgument
|
||||
? Effect.Store
|
||||
: Effect.ConditionallyMutate;
|
||||
continuation = {kind: 'funeffects'};
|
||||
break;
|
||||
}
|
||||
@@ -1311,102 +1250,12 @@ function inferBlock(
|
||||
Effect.Read,
|
||||
ValueReason.Other,
|
||||
);
|
||||
|
||||
const signature = getFunctionCallSignature(
|
||||
env,
|
||||
instrValue.property.identifier.type,
|
||||
inferCallEffects(
|
||||
state,
|
||||
instr as TInstruction<MethodCall>,
|
||||
freezeActions,
|
||||
getFunctionCallSignature(env, instrValue.property.identifier.type),
|
||||
);
|
||||
|
||||
const returnValueKind: AbstractValue =
|
||||
signature !== null
|
||||
? {
|
||||
kind: signature.returnValueKind,
|
||||
reason: new Set([
|
||||
signature.returnValueReason ??
|
||||
ValueReason.KnownReturnSignature,
|
||||
]),
|
||||
context: new Set(),
|
||||
}
|
||||
: {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
|
||||
if (
|
||||
signature !== null &&
|
||||
signature.mutableOnlyIfOperandsAreMutable &&
|
||||
areArgumentsImmutableAndNonMutating(state, instrValue.args)
|
||||
) {
|
||||
/*
|
||||
* None of the args are mutable or mutate their params, we can downgrade to
|
||||
* treating as all reads (except that the receiver may be captured)
|
||||
*/
|
||||
for (const arg of instrValue.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
place,
|
||||
Effect.Read,
|
||||
ValueReason.Other,
|
||||
);
|
||||
}
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.receiver,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
state.initialize(instrValue, returnValueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect =
|
||||
instrValue.receiver.effect === Effect.Capture
|
||||
? Effect.Store
|
||||
: Effect.ConditionallyMutate;
|
||||
continuation = {kind: 'funeffects'};
|
||||
break;
|
||||
}
|
||||
|
||||
const effects =
|
||||
signature !== null ? getFunctionEffects(instrValue, signature) : null;
|
||||
let hasCaptureArgument = false;
|
||||
for (let i = 0; i < instrValue.args.length; i++) {
|
||||
const arg = instrValue.args[i];
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* If effects are inferred for an argument, we should fail invalid
|
||||
* mutating effects
|
||||
*/
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
place,
|
||||
getArgumentEffect(effects != null ? effects[i] : null, arg),
|
||||
ValueReason.Other,
|
||||
);
|
||||
hasCaptureArgument ||= place.effect === Effect.Capture;
|
||||
}
|
||||
if (signature !== null) {
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.receiver,
|
||||
signature.calleeEffect,
|
||||
ValueReason.Other,
|
||||
);
|
||||
} else {
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.receiver,
|
||||
Effect.ConditionallyMutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
}
|
||||
hasCaptureArgument ||= instrValue.receiver.effect === Effect.Capture;
|
||||
|
||||
state.initialize(instrValue, returnValueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect = hasCaptureArgument
|
||||
? Effect.Store
|
||||
: Effect.ConditionallyMutate;
|
||||
continuation = {kind: 'funeffects'};
|
||||
break;
|
||||
}
|
||||
@@ -1813,7 +1662,13 @@ function inferBlock(
|
||||
kind === ValueKind.Mutable || kind === ValueKind.Context;
|
||||
let effect;
|
||||
let valueKind: AbstractValue;
|
||||
if (!isMutable || isArrayType(instrValue.collection.identifier)) {
|
||||
const iterator = instrValue.collection.identifier;
|
||||
if (
|
||||
!isMutable ||
|
||||
isArrayType(iterator) ||
|
||||
isMapType(iterator) ||
|
||||
isSetType(iterator)
|
||||
) {
|
||||
// Case 1, assume iterator is a separate mutable object
|
||||
effect = {
|
||||
kind: Effect.Read,
|
||||
@@ -1854,7 +1709,7 @@ function inferBlock(
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.iterator,
|
||||
Effect.ConditionallyMutate,
|
||||
Effect.ConditionallyMutateIterator,
|
||||
ValueReason.Other,
|
||||
);
|
||||
/**
|
||||
@@ -1983,7 +1838,7 @@ export function getFunctionCallSignature(
|
||||
* @returns Inferred effects of function arguments, or null if inference fails.
|
||||
*/
|
||||
export function getFunctionEffects(
|
||||
fn: MethodCall | CallExpression,
|
||||
fn: MethodCall | CallExpression | NewExpression,
|
||||
sig: FunctionSignature,
|
||||
): Array<Effect> | null {
|
||||
const results = [];
|
||||
@@ -2012,6 +1867,33 @@ export function getFunctionEffects(
|
||||
return results;
|
||||
}
|
||||
|
||||
export function isKnownMutableEffect(effect: Effect): boolean {
|
||||
switch (effect) {
|
||||
case Effect.Store:
|
||||
case Effect.ConditionallyMutate:
|
||||
case Effect.ConditionallyMutateIterator:
|
||||
case Effect.Mutate: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case Effect.Unknown: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected unknown effect',
|
||||
description: null,
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
case Effect.Read:
|
||||
case Effect.Capture:
|
||||
case Effect.Freeze: {
|
||||
return false;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns true if all of the arguments are both non-mutable (immutable or frozen)
|
||||
* _and_ are not functions which might mutate their arguments. Note that function
|
||||
@@ -2023,10 +1905,20 @@ function areArgumentsImmutableAndNonMutating(
|
||||
args: MethodCall['args'],
|
||||
): boolean {
|
||||
for (const arg of args) {
|
||||
if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
|
||||
const fnShape = state.env.getFunctionSignature(arg.identifier.type);
|
||||
if (fnShape != null) {
|
||||
return (
|
||||
!fnShape.positionalParams.some(isKnownMutableEffect) &&
|
||||
(fnShape.restParam == null ||
|
||||
!isKnownMutableEffect(fnShape.restParam))
|
||||
);
|
||||
}
|
||||
}
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
|
||||
const kind = state.kind(place).kind;
|
||||
switch (kind) {
|
||||
case ValueKind.Global:
|
||||
case ValueKind.Primitive:
|
||||
case ValueKind.Frozen: {
|
||||
/*
|
||||
@@ -2037,6 +1929,10 @@ function areArgumentsImmutableAndNonMutating(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
/**
|
||||
* Globals, module locals, and other locally defined functions may
|
||||
* mutate their arguments.
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2079,9 +1975,128 @@ function getArgumentEffect(
|
||||
});
|
||||
}
|
||||
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
|
||||
return Effect.ConditionallyMutate;
|
||||
return Effect.ConditionallyMutateIterator;
|
||||
}
|
||||
} else {
|
||||
return Effect.ConditionallyMutate;
|
||||
}
|
||||
}
|
||||
|
||||
function inferCallEffects(
|
||||
state: InferenceState,
|
||||
instr:
|
||||
| TInstruction<CallExpression>
|
||||
| TInstruction<MethodCall>
|
||||
| TInstruction<NewExpression>,
|
||||
freezeActions: Array<FreezeAction>,
|
||||
signature: FunctionSignature | null,
|
||||
): void {
|
||||
const instrValue = instr.value;
|
||||
const returnValueKind: AbstractValue =
|
||||
signature !== null
|
||||
? {
|
||||
kind: signature.returnValueKind,
|
||||
reason: new Set([
|
||||
signature.returnValueReason ?? ValueReason.KnownReturnSignature,
|
||||
]),
|
||||
context: new Set(),
|
||||
}
|
||||
: {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
};
|
||||
|
||||
if (
|
||||
instrValue.kind === 'MethodCall' &&
|
||||
signature !== null &&
|
||||
signature.mutableOnlyIfOperandsAreMutable &&
|
||||
areArgumentsImmutableAndNonMutating(state, instrValue.args)
|
||||
) {
|
||||
/*
|
||||
* None of the args are mutable or mutate their params, we can downgrade to
|
||||
* treating as all reads (except that the receiver may be captured)
|
||||
*/
|
||||
for (const arg of instrValue.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
place,
|
||||
Effect.Read,
|
||||
ValueReason.Other,
|
||||
);
|
||||
}
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
instrValue.receiver,
|
||||
Effect.Capture,
|
||||
ValueReason.Other,
|
||||
);
|
||||
state.initialize(instrValue, returnValueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect =
|
||||
instrValue.receiver.effect === Effect.Capture
|
||||
? Effect.Store
|
||||
: Effect.ConditionallyMutate;
|
||||
return;
|
||||
}
|
||||
|
||||
const effects =
|
||||
signature !== null ? getFunctionEffects(instrValue, signature) : null;
|
||||
let hasCaptureArgument = false;
|
||||
for (let i = 0; i < instrValue.args.length; i++) {
|
||||
const arg = instrValue.args[i];
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* If effects are inferred for an argument, we should fail invalid
|
||||
* mutating effects
|
||||
*/
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
place,
|
||||
getArgumentEffect(effects != null ? effects[i] : null, arg),
|
||||
ValueReason.Other,
|
||||
);
|
||||
hasCaptureArgument ||= place.effect === Effect.Capture;
|
||||
}
|
||||
const callee =
|
||||
instrValue.kind === 'MethodCall' ? instrValue.receiver : instrValue.callee;
|
||||
if (signature !== null) {
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
callee,
|
||||
signature.calleeEffect,
|
||||
ValueReason.Other,
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* For new expressions, we infer a `read` effect on the Class / Function type
|
||||
* to avoid extending mutable ranges of locally created classes, e.g.
|
||||
* ```js
|
||||
* const MyClass = getClass();
|
||||
* const value = new MyClass(val1, val2)
|
||||
* ^ (read) ^ (conditionally mutate)
|
||||
* ```
|
||||
*
|
||||
* Risks:
|
||||
* Classes / functions created during render could technically capture and
|
||||
* mutate their enclosing scope, which we currently do not detect.
|
||||
*/
|
||||
|
||||
state.referenceAndRecordEffects(
|
||||
freezeActions,
|
||||
callee,
|
||||
instrValue.kind === 'NewExpression'
|
||||
? Effect.Read
|
||||
: Effect.ConditionallyMutate,
|
||||
ValueReason.Other,
|
||||
);
|
||||
}
|
||||
hasCaptureArgument ||= callee.effect === Effect.Capture;
|
||||
|
||||
state.initialize(instrValue, returnValueKind);
|
||||
state.define(instr.lvalue, instrValue);
|
||||
instr.lvalue.effect = hasCaptureArgument
|
||||
? Effect.Store
|
||||
: Effect.ConditionallyMutate;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Instruction,
|
||||
LoadGlobal,
|
||||
LoadLocal,
|
||||
NonLocalImportSpecifier,
|
||||
Place,
|
||||
PropertyLoad,
|
||||
isUseContextHookType,
|
||||
@@ -35,7 +36,7 @@ import {inferTypes} from '../TypeInference';
|
||||
|
||||
export function lowerContextAccess(
|
||||
fn: HIRFunction,
|
||||
loweredContextCallee: ExternalFunction,
|
||||
loweredContextCalleeConfig: ExternalFunction,
|
||||
): void {
|
||||
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
|
||||
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
|
||||
@@ -79,6 +80,8 @@ export function lowerContextAccess(
|
||||
}
|
||||
}
|
||||
|
||||
let importLoweredContextCallee: NonLocalImportSpecifier | null = null;
|
||||
|
||||
if (contextAccess.size > 0 && contextKeys.size > 0) {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
let nextInstructions: Array<Instruction> | null = null;
|
||||
@@ -91,9 +94,13 @@ export function lowerContextAccess(
|
||||
isUseContextHookType(value.callee.identifier) &&
|
||||
contextKeys.has(lvalue.identifier.id)
|
||||
) {
|
||||
importLoweredContextCallee ??=
|
||||
fn.env.programContext.addImportSpecifier(
|
||||
loweredContextCalleeConfig,
|
||||
);
|
||||
const loweredContextCalleeInstr = emitLoadLoweredContextCallee(
|
||||
fn.env,
|
||||
loweredContextCallee,
|
||||
importLoweredContextCallee,
|
||||
);
|
||||
|
||||
if (nextInstructions === null) {
|
||||
@@ -122,21 +129,16 @@ export function lowerContextAccess(
|
||||
}
|
||||
markInstructionIds(fn.body);
|
||||
inferTypes(fn);
|
||||
fn.env.hasLoweredContextAccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
function emitLoadLoweredContextCallee(
|
||||
env: Environment,
|
||||
loweredContextCallee: ExternalFunction,
|
||||
importedLowerContextCallee: NonLocalImportSpecifier,
|
||||
): Instruction {
|
||||
const loadGlobal: LoadGlobal = {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {
|
||||
kind: 'ImportNamespace',
|
||||
module: loweredContextCallee.source,
|
||||
name: loweredContextCallee.importSpecifierName,
|
||||
},
|
||||
binding: {...importedLowerContextCallee},
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ function process(
|
||||
return null;
|
||||
}
|
||||
|
||||
const props = collectProps(jsx);
|
||||
const props = collectProps(fn.env, jsx);
|
||||
if (!props) return null;
|
||||
|
||||
const outlinedTag = fn.env.generateGloballyUniqueIdentifierName(null).value;
|
||||
@@ -217,6 +217,7 @@ type OutlinedJsxAttribute = {
|
||||
};
|
||||
|
||||
function collectProps(
|
||||
env: Environment,
|
||||
instructions: Array<JsxInstruction>,
|
||||
): Array<OutlinedJsxAttribute> | null {
|
||||
let id = 1;
|
||||
@@ -227,6 +228,7 @@ function collectProps(
|
||||
newName = `${oldName}${id++}`;
|
||||
}
|
||||
seen.add(newName);
|
||||
env.programContext.addNewReference(newName);
|
||||
return newName;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
renameVariables,
|
||||
} from '.';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {Environment, EnvironmentConfig, ExternalFunction} from '../HIR';
|
||||
import {Environment, ExternalFunction} from '../HIR';
|
||||
import {
|
||||
ArrayPattern,
|
||||
BlockId,
|
||||
@@ -52,7 +52,8 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
import {buildReactiveFunction} from './BuildReactiveFunction';
|
||||
import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
import {ReactFunctionType} from '../HIR/Environment';
|
||||
import {EMIT_FREEZE_GLOBAL_GATING, ReactFunctionType} from '../HIR/Environment';
|
||||
import {ProgramContext} from '../Entrypoint';
|
||||
|
||||
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
|
||||
export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
|
||||
@@ -100,9 +101,9 @@ export type CodegenFunction = {
|
||||
}>;
|
||||
|
||||
/**
|
||||
* This is true if the compiler has the lowered useContext calls.
|
||||
* This is true if the compiler has compiled inferred effect dependencies
|
||||
*/
|
||||
hasLoweredContextAccess: boolean;
|
||||
hasInferredEffect: boolean;
|
||||
|
||||
/**
|
||||
* This is true if the compiler has compiled a fire to a useFire call
|
||||
@@ -156,10 +157,11 @@ export function codegenFunction(
|
||||
const compiled = compileResult.unwrap();
|
||||
|
||||
const hookGuard = fn.env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null) {
|
||||
if (hookGuard != null && fn.env.isInferredMemoEnabled) {
|
||||
compiled.body = t.blockStatement([
|
||||
createHookGuard(
|
||||
hookGuard,
|
||||
fn.env.programContext,
|
||||
compiled.body.body,
|
||||
GuardKind.PushHookGuard,
|
||||
GuardKind.PopHookGuard,
|
||||
@@ -170,13 +172,15 @@ export function codegenFunction(
|
||||
const cacheCount = compiled.memoSlotsUsed;
|
||||
if (cacheCount !== 0) {
|
||||
const preface: Array<t.Statement> = [];
|
||||
const useMemoCacheIdentifier =
|
||||
fn.env.programContext.addMemoCacheImport().name;
|
||||
|
||||
// The import declaration for `useMemoCache` is inserted in the Babel plugin
|
||||
preface.push(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(
|
||||
t.identifier(cx.synthesizeName('$')),
|
||||
t.callExpression(t.identifier(fn.env.useMemoCacheIdentifier), [
|
||||
t.callExpression(t.identifier(useMemoCacheIdentifier), [
|
||||
t.numericLiteral(cacheCount),
|
||||
]),
|
||||
),
|
||||
@@ -250,39 +254,63 @@ export function codegenFunction(
|
||||
}
|
||||
|
||||
const emitInstrumentForget = fn.env.config.enableEmitInstrumentForget;
|
||||
if (emitInstrumentForget != null && fn.id != null) {
|
||||
if (
|
||||
emitInstrumentForget != null &&
|
||||
fn.id != null &&
|
||||
fn.env.isInferredMemoEnabled
|
||||
) {
|
||||
/*
|
||||
* Technically, this is a conditional hook call. However, we expect
|
||||
* __DEV__ and gating identifier to be runtime constants
|
||||
*/
|
||||
let gating: t.Expression;
|
||||
if (
|
||||
emitInstrumentForget.gating != null &&
|
||||
const gating =
|
||||
emitInstrumentForget.gating != null
|
||||
? t.identifier(
|
||||
fn.env.programContext.addImportSpecifier(
|
||||
emitInstrumentForget.gating,
|
||||
).name,
|
||||
)
|
||||
: null;
|
||||
|
||||
const globalGating =
|
||||
emitInstrumentForget.globalGating != null
|
||||
) {
|
||||
gating = t.logicalExpression(
|
||||
'&&',
|
||||
t.identifier(emitInstrumentForget.globalGating),
|
||||
t.identifier(emitInstrumentForget.gating.importSpecifierName),
|
||||
? t.identifier(emitInstrumentForget.globalGating)
|
||||
: null;
|
||||
|
||||
if (emitInstrumentForget.globalGating != null) {
|
||||
const assertResult = fn.env.programContext.assertGlobalBinding(
|
||||
emitInstrumentForget.globalGating,
|
||||
);
|
||||
} else if (emitInstrumentForget.gating != null) {
|
||||
gating = t.identifier(emitInstrumentForget.gating.importSpecifierName);
|
||||
if (assertResult.isErr()) {
|
||||
return assertResult;
|
||||
}
|
||||
}
|
||||
|
||||
let ifTest: t.Expression;
|
||||
if (gating != null && globalGating != null) {
|
||||
ifTest = t.logicalExpression('&&', globalGating, gating);
|
||||
} else if (gating != null) {
|
||||
ifTest = gating;
|
||||
} else {
|
||||
CompilerError.invariant(emitInstrumentForget.globalGating != null, {
|
||||
CompilerError.invariant(globalGating != null, {
|
||||
reason:
|
||||
'Bad config not caught! Expected at least one of gating or globalGating',
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
gating = t.identifier(emitInstrumentForget.globalGating);
|
||||
ifTest = globalGating;
|
||||
}
|
||||
|
||||
const instrumentFnIdentifier = fn.env.programContext.addImportSpecifier(
|
||||
emitInstrumentForget.fn,
|
||||
).name;
|
||||
const test: t.IfStatement = t.ifStatement(
|
||||
gating,
|
||||
ifTest,
|
||||
t.expressionStatement(
|
||||
t.callExpression(
|
||||
t.identifier(emitInstrumentForget.fn.importSpecifierName),
|
||||
[t.stringLiteral(fn.id), t.stringLiteral(fn.env.filename ?? '')],
|
||||
),
|
||||
t.callExpression(t.identifier(instrumentFnIdentifier), [
|
||||
t.stringLiteral(fn.id),
|
||||
t.stringLiteral(fn.env.filename ?? ''),
|
||||
]),
|
||||
),
|
||||
);
|
||||
compiled.body.body.unshift(test);
|
||||
@@ -359,8 +387,8 @@ function codegenReactiveFunction(
|
||||
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
|
||||
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
|
||||
outlined: [],
|
||||
hasLoweredContextAccess: fn.env.hasLoweredContextAccess,
|
||||
hasFireRewrite: fn.env.hasFireRewrite,
|
||||
hasInferredEffect: fn.env.hasInferredEffect,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -548,14 +576,19 @@ function codegenBlockNoReset(
|
||||
}
|
||||
|
||||
function wrapCacheDep(cx: Context, value: t.Expression): t.Expression {
|
||||
if (cx.env.config.enableEmitFreeze != null) {
|
||||
// The import declaration for emitFreeze is inserted in the Babel plugin
|
||||
if (cx.env.config.enableEmitFreeze != null && cx.env.isInferredMemoEnabled) {
|
||||
const emitFreezeIdentifier = cx.env.programContext.addImportSpecifier(
|
||||
cx.env.config.enableEmitFreeze,
|
||||
).name;
|
||||
cx.env.programContext
|
||||
.assertGlobalBinding(EMIT_FREEZE_GLOBAL_GATING, cx.env.scope)
|
||||
.unwrap();
|
||||
return t.conditionalExpression(
|
||||
t.identifier('__DEV__'),
|
||||
t.callExpression(
|
||||
t.identifier(cx.env.config.enableEmitFreeze.importSpecifierName),
|
||||
[value, t.stringLiteral(cx.fnName)],
|
||||
),
|
||||
t.identifier(EMIT_FREEZE_GLOBAL_GATING),
|
||||
t.callExpression(t.identifier(emitFreezeIdentifier), [
|
||||
value,
|
||||
t.stringLiteral(cx.fnName),
|
||||
]),
|
||||
value,
|
||||
);
|
||||
} else {
|
||||
@@ -709,16 +742,14 @@ function codegenReactiveScope(
|
||||
let computationBlock = codegenBlock(cx, block);
|
||||
|
||||
let memoStatement;
|
||||
if (
|
||||
cx.env.config.enableChangeDetectionForDebugging != null &&
|
||||
changeExpressions.length > 0
|
||||
) {
|
||||
const detectionFunction = cx.env.config.enableChangeDetectionForDebugging;
|
||||
if (detectionFunction != null && changeExpressions.length > 0) {
|
||||
const loc =
|
||||
typeof scope.loc === 'symbol'
|
||||
? 'unknown location'
|
||||
: `(${scope.loc.start.line}:${scope.loc.end.line})`;
|
||||
const detectionFunction =
|
||||
cx.env.config.enableChangeDetectionForDebugging.importSpecifierName;
|
||||
const importedDetectionFunctionIdentifier =
|
||||
cx.env.programContext.addImportSpecifier(detectionFunction).name;
|
||||
const cacheLoadOldValueStatements: Array<t.Statement> = [];
|
||||
const changeDetectionStatements: Array<t.Statement> = [];
|
||||
const idempotenceDetectionStatements: Array<t.Statement> = [];
|
||||
@@ -740,7 +771,7 @@ function codegenReactiveScope(
|
||||
);
|
||||
changeDetectionStatements.push(
|
||||
t.expressionStatement(
|
||||
t.callExpression(t.identifier(detectionFunction), [
|
||||
t.callExpression(t.identifier(importedDetectionFunctionIdentifier), [
|
||||
t.identifier(loadName),
|
||||
t.cloneNode(name, true),
|
||||
t.stringLiteral(name.name),
|
||||
@@ -752,7 +783,7 @@ function codegenReactiveScope(
|
||||
);
|
||||
idempotenceDetectionStatements.push(
|
||||
t.expressionStatement(
|
||||
t.callExpression(t.identifier(detectionFunction), [
|
||||
t.callExpression(t.identifier(importedDetectionFunctionIdentifier), [
|
||||
t.cloneNode(slot, true),
|
||||
t.cloneNode(name, true),
|
||||
t.stringLiteral(name.name),
|
||||
@@ -1514,15 +1545,15 @@ const createStringLiteral = withLoc(t.stringLiteral);
|
||||
|
||||
function createHookGuard(
|
||||
guard: ExternalFunction,
|
||||
context: ProgramContext,
|
||||
stmts: Array<t.Statement>,
|
||||
before: GuardKind,
|
||||
after: GuardKind,
|
||||
): t.TryStatement {
|
||||
const guardFnName = context.addImportSpecifier(guard).name;
|
||||
function createHookGuardImpl(kind: number): t.ExpressionStatement {
|
||||
return t.expressionStatement(
|
||||
t.callExpression(t.identifier(guard.importSpecifierName), [
|
||||
t.numericLiteral(kind),
|
||||
]),
|
||||
t.callExpression(t.identifier(guardFnName), [t.numericLiteral(kind)]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1553,7 +1584,7 @@ function createHookGuard(
|
||||
* ```
|
||||
*/
|
||||
function createCallExpression(
|
||||
config: EnvironmentConfig,
|
||||
env: Environment,
|
||||
callee: t.Expression,
|
||||
args: Array<t.Expression | t.SpreadElement>,
|
||||
loc: SourceLocation | null,
|
||||
@@ -1564,14 +1595,15 @@ function createCallExpression(
|
||||
callExpr.loc = loc;
|
||||
}
|
||||
|
||||
const hookGuard = config.enableEmitHookGuards;
|
||||
if (hookGuard != null && isHook) {
|
||||
const hookGuard = env.config.enableEmitHookGuards;
|
||||
if (hookGuard != null && isHook && env.isInferredMemoEnabled) {
|
||||
const iife = t.functionExpression(
|
||||
null,
|
||||
[],
|
||||
t.blockStatement([
|
||||
createHookGuard(
|
||||
hookGuard,
|
||||
env.programContext,
|
||||
[t.returnStatement(callExpr)],
|
||||
GuardKind.AllowHook,
|
||||
GuardKind.DisallowHook,
|
||||
@@ -1701,7 +1733,7 @@ function codegenInstructionValue(
|
||||
const callee = codegenPlaceToExpression(cx, instrValue.callee);
|
||||
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
|
||||
value = createCallExpression(
|
||||
cx.env.config,
|
||||
cx.env,
|
||||
callee,
|
||||
args,
|
||||
instrValue.loc,
|
||||
@@ -1791,7 +1823,7 @@ function codegenInstructionValue(
|
||||
);
|
||||
const args = instrValue.args.map(arg => codegenArgument(cx, arg));
|
||||
value = createCallExpression(
|
||||
cx.env.config,
|
||||
cx.env,
|
||||
memberExpr,
|
||||
args,
|
||||
instrValue.loc,
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
convertHoistedLValueKind,
|
||||
DeclarationId,
|
||||
InstructionKind,
|
||||
ReactiveFunction,
|
||||
@@ -27,7 +29,17 @@ export function pruneHoistedContexts(fn: ReactiveFunction): void {
|
||||
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
|
||||
}
|
||||
|
||||
type HoistedIdentifiers = Map<DeclarationId, InstructionKind>;
|
||||
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
|
||||
'REWRITTEN_HOISTED_CONST',
|
||||
);
|
||||
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
|
||||
|
||||
type HoistedIdentifiers = Map<
|
||||
DeclarationId,
|
||||
| InstructionKind
|
||||
| typeof REWRITTEN_HOISTED_CONST
|
||||
| typeof REWRITTEN_HOISTED_LET
|
||||
>;
|
||||
|
||||
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
override transformInstruction(
|
||||
@@ -35,64 +47,101 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
state: HoistedIdentifiers,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitInstruction(instruction, state);
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedConst'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Const,
|
||||
|
||||
/**
|
||||
* Remove hoisted declarations to preserve TDZ
|
||||
*/
|
||||
if (instruction.value.kind === 'DeclareContext') {
|
||||
const maybeNonHoisted = convertHoistedLValueKind(
|
||||
instruction.value.lvalue.kind,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
if (maybeNonHoisted != null) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
maybeNonHoisted,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
if (instruction.value.lvalue.kind === 'Let') {
|
||||
/**
|
||||
* We don't expect const context variables to be hoisted
|
||||
*/
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_LET,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedLet'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Let,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedFunction'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Function,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'StoreContext' &&
|
||||
state.has(instruction.value.lvalue.place.identifier.declarationId)
|
||||
) {
|
||||
if (instruction.value.kind === 'StoreContext') {
|
||||
const kind = state.get(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
)!;
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
...instruction,
|
||||
);
|
||||
if (kind != null) {
|
||||
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
|
||||
reason: 'Expected exactly one store to a hoisted const variable',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
if (
|
||||
kind === InstructionKind.Const ||
|
||||
kind === InstructionKind.Function
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_CONST,
|
||||
);
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
...instruction.value,
|
||||
lvalue: {
|
||||
...instruction.value.lvalue,
|
||||
kind,
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
...instruction,
|
||||
value: {
|
||||
...instruction.value,
|
||||
lvalue: {
|
||||
...instruction.value.lvalue,
|
||||
kind,
|
||||
},
|
||||
type: null,
|
||||
kind: 'StoreLocal',
|
||||
},
|
||||
},
|
||||
type: null,
|
||||
kind: 'StoreLocal',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
} else if (kind !== REWRITTEN_HOISTED_LET) {
|
||||
/**
|
||||
* Context variables declared with let may have reassignments. Only
|
||||
* insert a `DeclareContext` for the first encountered `StoreContext`
|
||||
* instruction.
|
||||
*/
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_LET,
|
||||
);
|
||||
return {
|
||||
kind: 'replace-many',
|
||||
value: [
|
||||
{
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
id: instruction.id,
|
||||
lvalue: null,
|
||||
value: {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place: {...instruction.value.lvalue.place},
|
||||
},
|
||||
loc: instruction.value.loc,
|
||||
},
|
||||
loc: instruction.loc,
|
||||
},
|
||||
},
|
||||
{kind: 'instruction', instruction},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {kind: 'keep'};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {ProgramContext} from '..';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
@@ -47,7 +48,7 @@ import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
*/
|
||||
export function renameVariables(fn: ReactiveFunction): Set<string> {
|
||||
const globals = collectReferencedGlobals(fn);
|
||||
const scopes = new Scopes(globals);
|
||||
const scopes = new Scopes(globals, fn.env.programContext);
|
||||
renameVariablesImpl(fn, new Visitor(), scopes);
|
||||
return new Set([...scopes.names, ...globals]);
|
||||
}
|
||||
@@ -124,10 +125,12 @@ class Scopes {
|
||||
#seen: Map<DeclarationId, IdentifierName> = new Map();
|
||||
#stack: Array<Map<string, DeclarationId>> = [new Map()];
|
||||
#globals: Set<string>;
|
||||
#programContext: ProgramContext;
|
||||
names: Set<ValidIdentifierName> = new Set();
|
||||
|
||||
constructor(globals: Set<string>) {
|
||||
constructor(globals: Set<string>, programContext: ProgramContext) {
|
||||
this.#globals = globals;
|
||||
this.#programContext = programContext;
|
||||
}
|
||||
|
||||
visit(identifier: Identifier): void {
|
||||
@@ -156,6 +159,7 @@ class Scopes {
|
||||
name = `${originalName.value}$${id++}`;
|
||||
}
|
||||
}
|
||||
this.#programContext.addNewReference(name);
|
||||
const identifierName = makeIdentifierName(name);
|
||||
identifier.name = identifierName;
|
||||
this.#seen.set(identifier.declarationId, identifierName);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
isUseEffectHookType,
|
||||
LoadLocal,
|
||||
makeInstructionId,
|
||||
NonLocalImportSpecifier,
|
||||
Place,
|
||||
promoteTemporary,
|
||||
} from '../HIR';
|
||||
@@ -36,6 +37,7 @@ import {getOrInsertWith} from '../Utils/utils';
|
||||
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
|
||||
import {eachInstructionOperand} from '../HIR/visitors';
|
||||
import {printSourceLocationLine} from '../HIR/PrintHIR';
|
||||
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
|
||||
|
||||
/*
|
||||
* TODO(jmbrown):
|
||||
@@ -56,6 +58,7 @@ export function transformFire(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
let importedUseFire: NonLocalImportSpecifier | null = null;
|
||||
let hasRewrite = false;
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
|
||||
@@ -87,7 +90,15 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
|
||||
] of capturedCallees.entries()) {
|
||||
if (!context.hasCalleeWithInsertedFire(fireCalleePlace)) {
|
||||
context.addCalleeWithInsertedFire(fireCalleePlace);
|
||||
const loadUseFireInstr = makeLoadUseFireInstruction(fn.env);
|
||||
|
||||
importedUseFire ??= fn.env.programContext.addImportSpecifier({
|
||||
source: fn.env.programContext.reactRuntimeModule,
|
||||
importSpecifierName: USE_FIRE_FUNCTION_NAME,
|
||||
});
|
||||
const loadUseFireInstr = makeLoadUseFireInstruction(
|
||||
fn.env,
|
||||
importedUseFire,
|
||||
);
|
||||
const loadFireCalleeInstr = makeLoadFireCalleeInstruction(
|
||||
fn.env,
|
||||
fireCalleeInfo.capturedCalleeIdentifier,
|
||||
@@ -404,18 +415,16 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
|
||||
}
|
||||
}
|
||||
|
||||
function makeLoadUseFireInstruction(env: Environment): Instruction {
|
||||
function makeLoadUseFireInstruction(
|
||||
env: Environment,
|
||||
importedLoadUseFire: NonLocalImportSpecifier,
|
||||
): Instruction {
|
||||
const useFirePlace = createTemporaryPlace(env, GeneratedSource);
|
||||
useFirePlace.effect = Effect.Read;
|
||||
useFirePlace.identifier.type = DefaultNonmutatingHook;
|
||||
const instrValue: InstructionValue = {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {
|
||||
kind: 'ImportSpecifier',
|
||||
name: 'useFire',
|
||||
module: 'react',
|
||||
imported: 'useFire',
|
||||
},
|
||||
binding: {...importedLoadUseFire},
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -261,6 +261,7 @@ function* generateInstructionTypes(
|
||||
kind: 'Function',
|
||||
shapeId: null,
|
||||
return: returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
@@ -277,6 +278,7 @@ function* generateInstructionTypes(
|
||||
kind: 'Function',
|
||||
shapeId: null,
|
||||
return: returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
@@ -333,6 +335,7 @@ function* generateInstructionTypes(
|
||||
kind: 'Function',
|
||||
return: returnType,
|
||||
shapeId: null,
|
||||
isConstructor: false,
|
||||
});
|
||||
|
||||
yield equation(left, returnType);
|
||||
@@ -405,6 +408,7 @@ function* generateInstructionTypes(
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInFunctionId,
|
||||
return: value.loweredFunc.func.returnType,
|
||||
isConstructor: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -425,9 +429,20 @@ function* generateInstructionTypes(
|
||||
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
|
||||
break;
|
||||
}
|
||||
case 'NewExpression': {
|
||||
const returnType = makeType();
|
||||
yield equation(value.callee.identifier.type, {
|
||||
kind: 'Function',
|
||||
return: returnType,
|
||||
shapeId: null,
|
||||
isConstructor: true,
|
||||
});
|
||||
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
}
|
||||
case 'PropertyStore':
|
||||
case 'DeclareLocal':
|
||||
case 'NewExpression':
|
||||
case 'RegExpLiteral':
|
||||
case 'MetaProperty':
|
||||
case 'ComputedStore':
|
||||
@@ -505,7 +520,11 @@ class Unifier {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tB.kind === 'Function' && tA.kind === 'Function') {
|
||||
if (
|
||||
tB.kind === 'Function' &&
|
||||
tA.kind === 'Function' &&
|
||||
tA.isConstructor === tB.isConstructor
|
||||
) {
|
||||
this.unify(tA.return, tB.return);
|
||||
return;
|
||||
}
|
||||
@@ -648,6 +667,7 @@ class Unifier {
|
||||
kind: 'Function',
|
||||
return: returnType,
|
||||
shapeId: type.shapeId,
|
||||
isConstructor: type.isConstructor,
|
||||
};
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Represents the possible kinds of value which may be stored at a given Place during
|
||||
@@ -87,7 +88,9 @@ function joinKinds(a: Kind, b: Kind): Kind {
|
||||
* may not appear as the callee of a conditional call.
|
||||
* See the note for Kind.PotentialHook for sources of potential hooks
|
||||
*/
|
||||
export function validateHooksUsage(fn: HIRFunction): void {
|
||||
export function validateHooksUsage(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const unconditionalBlocks = computeUnconditionalBlocks(fn);
|
||||
|
||||
const errors = new CompilerError();
|
||||
@@ -423,9 +426,7 @@ export function validateHooksUsage(fn: HIRFunction): void {
|
||||
for (const [, error] of errorsByPlace) {
|
||||
errors.push(error);
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ReactiveFunctionVisitor,
|
||||
visitReactiveFunction,
|
||||
} from '../ReactiveScopes/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Validates that all known effect dependencies are memoized. The algorithm checks two things:
|
||||
@@ -47,12 +48,12 @@ import {
|
||||
* mutate(object); // ... mutable range ends here after this mutation
|
||||
* ```
|
||||
*/
|
||||
export function validateMemoizedEffectDependencies(fn: ReactiveFunction): void {
|
||||
export function validateMemoizedEffectDependencies(
|
||||
fn: ReactiveFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
visitReactiveFunction(fn, new Visitor(), errors);
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<CompilerError> {
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import {CompilerError, EnvironmentConfig} from '..';
|
||||
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {DEFAULT_GLOBALS} from '../HIR/Globals';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
export function validateNoCapitalizedCalls(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const envConfig: EnvironmentConfig = fn.env.config;
|
||||
const ALLOW_LIST = new Set([
|
||||
...DEFAULT_GLOBALS.keys(),
|
||||
@@ -26,6 +29,7 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
);
|
||||
};
|
||||
|
||||
const errors = new CompilerError();
|
||||
const capitalLoadGlobals = new Map<IdentifierId, string>();
|
||||
const capitalizedProperties = new Map<IdentifierId, string>();
|
||||
const reason =
|
||||
@@ -73,7 +77,8 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
const propertyIdentifier = value.property.identifier.id;
|
||||
const propertyName = capitalizedProperties.get(propertyIdentifier);
|
||||
if (propertyName != null) {
|
||||
CompilerError.throwInvalidReact({
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason,
|
||||
description: `${propertyName} may be a component.`,
|
||||
loc: value.loc,
|
||||
@@ -85,4 +90,5 @@ export function validateNoCapitalizedCalls(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {HIRFunction} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Checks that known-impure functions are not called during render. Examples of invalid functions to
|
||||
@@ -18,7 +19,9 @@ import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
* this in several of our validation passes and should unify those analyses into a reusable helper
|
||||
* and use it here.
|
||||
*/
|
||||
export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
|
||||
export function validateNoImpureFunctionsInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
@@ -46,7 +49,5 @@ export function validateNoImpureFunctionsInRender(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {BlockId, HIRFunction} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,9 @@ import {retainWhere} from '../Utils/utils';
|
||||
* created within a try block. JSX is allowed within a catch statement, unless that catch
|
||||
* is itself nested inside an outer try.
|
||||
*/
|
||||
export function validateNoJSXInTryStatement(fn: HIRFunction): void {
|
||||
export function validateNoJSXInTryStatement(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const activeTryBlocks: Array<BlockId> = [];
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
@@ -46,7 +49,5 @@ export function validateNoJSXInTryStatement(fn: HIRFunction): void {
|
||||
activeTryBlocks.push(block.terminal.handler);
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -99,9 +99,11 @@ class Env extends Map<IdentifierId, RefAccessType> {
|
||||
}
|
||||
}
|
||||
|
||||
export function validateNoRefAccessInRender(fn: HIRFunction): void {
|
||||
export function validateNoRefAccessInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const env = new Env();
|
||||
validateNoRefAccessInRenderImpl(fn, env).unwrap();
|
||||
return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined);
|
||||
}
|
||||
|
||||
function refTypeOfType(place: Place): RefAccessType {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Validates against calling setState in the body of a *passive* effect (useEffect),
|
||||
@@ -23,7 +24,9 @@ import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
* often bad for performance and frequently has more efficient and straightforward
|
||||
* alternatives. See https://react.dev/learn/you-might-not-need-an-effect for examples.
|
||||
*/
|
||||
export function validateNoSetStateInPassiveEffects(fn: HIRFunction): void {
|
||||
export function validateNoSetStateInPassiveEffects(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const setStateFunctions: Map<IdentifierId, Place> = new Map();
|
||||
const errors = new CompilerError();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
@@ -98,9 +101,7 @@ export function validateNoSetStateInPassiveEffects(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
throw errors;
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function getSetStateCall(
|
||||
|
||||
@@ -9,7 +9,7 @@ import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
|
||||
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Validates that the given function does not have an infinite update loop
|
||||
@@ -39,9 +39,11 @@ import {Err, Ok, Result} from '../Utils/Result';
|
||||
* y();
|
||||
* ```
|
||||
*/
|
||||
export function validateNoSetStateInRender(fn: HIRFunction): void {
|
||||
export function validateNoSetStateInRender(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
|
||||
validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions).unwrap();
|
||||
return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
|
||||
}
|
||||
|
||||
function validateNoSetStateInRenderImpl(
|
||||
@@ -145,9 +147,5 @@ function validateNoSetStateInRenderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
return Err(errors);
|
||||
} else {
|
||||
return Ok(undefined);
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
ReactiveFunctionVisitor,
|
||||
visitReactiveFunction,
|
||||
} from '../ReactiveScopes/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
@@ -39,15 +41,15 @@ import {getOrInsertDefault} from '../Utils/utils';
|
||||
* This can occur if a value's mutable range somehow extended to include a hook and
|
||||
* was pruned.
|
||||
*/
|
||||
export function validatePreservedManualMemoization(fn: ReactiveFunction): void {
|
||||
export function validatePreservedManualMemoization(
|
||||
fn: ReactiveFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const state = {
|
||||
errors: new CompilerError(),
|
||||
manualMemoState: null,
|
||||
};
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
if (state.errors.hasErrors()) {
|
||||
throw state.errors;
|
||||
}
|
||||
return state.errors.asResult();
|
||||
}
|
||||
|
||||
const DEBUG = false;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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, ErrorSeverity} from '../CompilerError';
|
||||
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
/**
|
||||
* Validates against components that are created dynamically and whose identity is not guaranteed
|
||||
* to be stable (which would cause the component to reset on each re-render).
|
||||
*/
|
||||
export function validateStaticComponents(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const error = new CompilerError();
|
||||
const knownDynamicComponents = new Map<IdentifierId, SourceLocation>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
phis: for (const phi of block.phis) {
|
||||
for (const operand of phi.operands.values()) {
|
||||
const loc = knownDynamicComponents.get(operand.identifier.id);
|
||||
if (loc != null) {
|
||||
knownDynamicComponents.set(phi.place.identifier.id, loc);
|
||||
continue phis;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'FunctionExpression':
|
||||
case 'NewExpression':
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
knownDynamicComponents.set(lvalue.identifier.id, value.loc);
|
||||
break;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const loc = knownDynamicComponents.get(value.place.identifier.id);
|
||||
if (loc != null) {
|
||||
knownDynamicComponents.set(lvalue.identifier.id, loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const loc = knownDynamicComponents.get(value.value.identifier.id);
|
||||
if (loc != null) {
|
||||
knownDynamicComponents.set(lvalue.identifier.id, loc);
|
||||
knownDynamicComponents.set(value.lvalue.place.identifier.id, loc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxExpression': {
|
||||
if (value.tag.kind === 'Identifier') {
|
||||
const location = knownDynamicComponents.get(
|
||||
value.tag.identifier.id,
|
||||
);
|
||||
if (location != null) {
|
||||
error.push({
|
||||
reason: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: value.tag.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
error.push({
|
||||
reason: `The component may be created during render`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: location,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return error.asResult();
|
||||
}
|
||||
@@ -5,10 +5,12 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {CompilerError, ErrorSeverity} from '..';
|
||||
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
export function validateUseMemo(fn: HIRFunction): void {
|
||||
export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const useMemos = new Set<IdentifierId>();
|
||||
const react = new Set<IdentifierId>();
|
||||
const functions = new Map<IdentifierId, FunctionExpression>();
|
||||
@@ -61,7 +63,8 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.params.length > 0) {
|
||||
CompilerError.throwInvalidReact({
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason: 'useMemo callbacks may not accept any arguments',
|
||||
description: null,
|
||||
loc: body.loc,
|
||||
@@ -70,7 +73,8 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
|
||||
CompilerError.throwInvalidReact({
|
||||
errors.push({
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
reason:
|
||||
'useMemo callbacks may not be async or generator functions',
|
||||
description: null,
|
||||
@@ -84,4 +88,5 @@ export function validateUseMemo(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ globalThis.constantValue = 'global test value';
|
||||
|
||||
test('literal-constant-propagation', () => {
|
||||
function Component() {
|
||||
'use memo';
|
||||
const x = 'test value 1';
|
||||
return <div>{x}</div>;
|
||||
}
|
||||
@@ -38,6 +39,7 @@ test('literal-constant-propagation', () => {
|
||||
|
||||
test('global-constant-propagation', () => {
|
||||
function Component() {
|
||||
'use memo';
|
||||
const x = constantValue;
|
||||
|
||||
return <div>{x}</div>;
|
||||
@@ -65,6 +67,7 @@ test('global-constant-propagation', () => {
|
||||
|
||||
test('lambda-constant-propagation', () => {
|
||||
function Component() {
|
||||
'use memo';
|
||||
const x = 'test value 1';
|
||||
const getDiv = () => <div>{x}</div>;
|
||||
return getDiv();
|
||||
@@ -92,6 +95,7 @@ test('lambda-constant-propagation', () => {
|
||||
|
||||
test('lambda-constant-propagation-of-phi-node', () => {
|
||||
function Component({noopCallback}) {
|
||||
'use memo';
|
||||
const x = 'test value 1';
|
||||
if (constantValue) {
|
||||
noopCallback();
|
||||
|
||||
@@ -16,6 +16,7 @@ function Button({label}) {
|
||||
|
||||
let currentTheme = 'light';
|
||||
function useTheme() {
|
||||
'use memo';
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as React from 'react';
|
||||
import {expectLogsAndClear, log} from './expectLogs';
|
||||
|
||||
function Counter(props) {
|
||||
'use memo';
|
||||
let value = props.value;
|
||||
let a = value++;
|
||||
expect(a).toBe(props.value); // postfix
|
||||
|
||||
@@ -50,28 +50,55 @@ import { useIdentity, Stringify } from "shared-runtime";
|
||||
* (2) the 1st argument might mutate its callee
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(10);
|
||||
const { value } = t0;
|
||||
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
|
||||
useIdentity();
|
||||
const derived = Array.from(arr, _temp);
|
||||
let t1;
|
||||
if ($[0] !== derived) {
|
||||
t1 = derived.at(-1);
|
||||
$[0] = derived;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <Stringify>{t1}</Stringify>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = { value: "foo" };
|
||||
t2 = { value: "bar" };
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
t1 = $[0];
|
||||
t2 = $[1];
|
||||
}
|
||||
return t2;
|
||||
let t3;
|
||||
if ($[2] !== value) {
|
||||
t3 = [t1, t2, { value }];
|
||||
$[2] = value;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const arr = t3;
|
||||
useIdentity();
|
||||
let t4;
|
||||
if ($[4] !== arr) {
|
||||
t4 = Array.from(arr, _temp);
|
||||
$[4] = arr;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const derived = t4;
|
||||
let t5;
|
||||
if ($[6] !== derived) {
|
||||
t5 = derived.at(-1);
|
||||
$[6] = derived;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== t5) {
|
||||
t6 = <Stringify>{t5}</Stringify>;
|
||||
$[8] = t5;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp(x, idx) {
|
||||
return { ...x, id: idx };
|
||||
|
||||
@@ -50,28 +50,55 @@ import { useIdentity, Stringify } from "shared-runtime";
|
||||
* (2) the 1st argument might mutate its callee
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(10);
|
||||
const { value } = t0;
|
||||
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
|
||||
useIdentity();
|
||||
const derived = Array.from(arr);
|
||||
let t1;
|
||||
if ($[0] !== derived) {
|
||||
t1 = derived.at(-1);
|
||||
$[0] = derived;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <Stringify>{t1}</Stringify>;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = { value: "foo" };
|
||||
t2 = { value: "bar" };
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
t1 = $[0];
|
||||
t2 = $[1];
|
||||
}
|
||||
return t2;
|
||||
let t3;
|
||||
if ($[2] !== value) {
|
||||
t3 = [t1, t2, { value }];
|
||||
$[2] = value;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const arr = t3;
|
||||
useIdentity();
|
||||
let t4;
|
||||
if ($[4] !== arr) {
|
||||
t4 = Array.from(arr);
|
||||
$[4] = arr;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const derived = t4;
|
||||
let t5;
|
||||
if ($[6] !== derived) {
|
||||
t5 = derived.at(-1);
|
||||
$[6] = derived;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== t5) {
|
||||
t6 = <Stringify>{t5}</Stringify>;
|
||||
$[8] = t5;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -7,14 +7,19 @@ import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
|
||||
function Component({value}) {
|
||||
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
|
||||
useIdentity();
|
||||
const derived = Array.from(arr, mutateAndReturn);
|
||||
return <Stringify>{derived.at(-1)}</Stringify>;
|
||||
const derived = Array.from(arr).map(mutateAndReturn);
|
||||
return (
|
||||
<Stringify>
|
||||
{derived.at(0)}
|
||||
{derived.at(-1)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 5}],
|
||||
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
|
||||
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}],
|
||||
};
|
||||
|
||||
```
|
||||
@@ -26,39 +31,54 @@ import { c as _c } from "react/compiler-runtime";
|
||||
import { mutateAndReturn, Stringify, useIdentity } from "shared-runtime";
|
||||
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(7);
|
||||
const { value } = t0;
|
||||
const arr = [{ value: "foo" }, { value: "bar" }, { value }];
|
||||
useIdentity();
|
||||
const derived = Array.from(arr, mutateAndReturn);
|
||||
const derived = Array.from(arr).map(mutateAndReturn);
|
||||
let t1;
|
||||
if ($[0] !== derived) {
|
||||
t1 = derived.at(-1);
|
||||
t1 = derived.at(0);
|
||||
$[0] = derived;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] !== t1) {
|
||||
t2 = <Stringify>{t1}</Stringify>;
|
||||
$[2] = t1;
|
||||
if ($[2] !== derived) {
|
||||
t2 = derived.at(-1);
|
||||
$[2] = derived;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
let t3;
|
||||
if ($[4] !== t1 || $[5] !== t2) {
|
||||
t3 = (
|
||||
<Stringify>
|
||||
{t1}
|
||||
{t2}
|
||||
</Stringify>
|
||||
);
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ value: 5 }],
|
||||
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }],
|
||||
sequentialRenders: [{ value: 5 }, { value: 6 }, { value: 6 }, { value: 7 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"children":{"value":5,"wat0":"joe"}}</div>
|
||||
<div>{"children":{"value":6,"wat0":"joe"}}</div>
|
||||
<div>{"children":{"value":6,"wat0":"joe"}}</div>
|
||||
(kind: ok) <div>{"children":[{"value":"foo","wat0":"joe"},{"value":5,"wat0":"joe"}]}</div>
|
||||
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
|
||||
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":6,"wat0":"joe"}]}</div>
|
||||
<div>{"children":[{"value":"foo","wat0":"joe"},{"value":7,"wat0":"joe"}]}</div>
|
||||
@@ -3,12 +3,17 @@ import {mutateAndReturn, Stringify, useIdentity} from 'shared-runtime';
|
||||
function Component({value}) {
|
||||
const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
|
||||
useIdentity();
|
||||
const derived = Array.from(arr, mutateAndReturn);
|
||||
return <Stringify>{derived.at(-1)}</Stringify>;
|
||||
const derived = Array.from(arr).map(mutateAndReturn);
|
||||
return (
|
||||
<Stringify>
|
||||
{derived.at(0)}
|
||||
{derived.at(-1)}
|
||||
</Stringify>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{value: 5}],
|
||||
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}],
|
||||
sequentialRenders: [{value: 5}, {value: 6}, {value: 6}, {value: 7}],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Test,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
|
||||
const Test = () => {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <div />;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
};
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Test,
|
||||
params: [{}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div></div>
|
||||
@@ -0,0 +1,7 @@
|
||||
// @compilationMode(infer)
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Test,
|
||||
params: [{}],
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import * as React from 'react';
|
||||
import {someImport} from 'react/compiler-runtime';
|
||||
import {calculateExpensiveNumber} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const [x] = React.useState(0);
|
||||
const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{expensiveNumber}
|
||||
{`${someImport}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import * as React from "react";
|
||||
import { someImport, c as _c } from "react/compiler-runtime";
|
||||
import { calculateExpensiveNumber } from "shared-runtime";
|
||||
|
||||
function Component(props) {
|
||||
const $ = _c(4);
|
||||
const [x] = React.useState(0);
|
||||
let t0;
|
||||
let t1;
|
||||
if ($[0] !== x) {
|
||||
t1 = calculateExpensiveNumber(x);
|
||||
$[0] = x;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
t0 = t1;
|
||||
const expensiveNumber = t0;
|
||||
let t2;
|
||||
if ($[2] !== expensiveNumber) {
|
||||
t2 = (
|
||||
<div>
|
||||
{expensiveNumber}
|
||||
{`${someImport}`}
|
||||
</div>
|
||||
);
|
||||
$[2] = expensiveNumber;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>0undefined</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import {someImport} from 'react/compiler-runtime';
|
||||
import {calculateExpensiveNumber} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
const [x] = React.useState(0);
|
||||
const expensiveNumber = React.useMemo(() => calculateExpensiveNumber(x), [x]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{expensiveNumber}
|
||||
{`${someImport}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [],
|
||||
};
|
||||
@@ -14,9 +14,9 @@ function useFoo(props) {
|
||||
|
||||
```javascript
|
||||
import {
|
||||
useRenderCounter,
|
||||
shouldInstrument,
|
||||
makeReadOnly,
|
||||
shouldInstrument,
|
||||
useRenderCounter,
|
||||
} from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @enableEmitInstrumentForget
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ function Foo(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { useRenderCounter, shouldInstrument } from "react-compiler-runtime";
|
||||
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
function Bar(props) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
const shouldInstrument = identity(null);
|
||||
const _shouldInstrument = identity(null);
|
||||
const _x2 = () => {
|
||||
const _shouldInstrument2 = 'hello world';
|
||||
return identity({_shouldInstrument2});
|
||||
};
|
||||
return (
|
||||
<div style={shouldInstrument} other={_shouldInstrument}>
|
||||
{props.bar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo(props) {
|
||||
'use forget';
|
||||
return <Foo>{props.bar}</Foo>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import {
|
||||
shouldInstrument as _shouldInstrument3,
|
||||
useRenderCounter,
|
||||
} from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
function Bar(props) {
|
||||
"use forget";
|
||||
if (DEV && _shouldInstrument3)
|
||||
useRenderCounter("Bar", "/conflict-codegen-instrument-forget.ts");
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = identity(null);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const shouldInstrument = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = identity(null);
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const _shouldInstrument = t1;
|
||||
let t2;
|
||||
if ($[2] !== props.bar) {
|
||||
t2 = (
|
||||
<div style={shouldInstrument} other={_shouldInstrument}>
|
||||
{props.bar}
|
||||
</div>
|
||||
);
|
||||
$[2] = props.bar;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
|
||||
function Foo(props) {
|
||||
"use forget";
|
||||
if (DEV && _shouldInstrument3)
|
||||
useRenderCounter("Foo", "/conflict-codegen-instrument-forget.ts");
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.bar) {
|
||||
t0 = <Foo>{props.bar}</Foo>;
|
||||
$[0] = props.bar;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,23 @@
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
const shouldInstrument = identity(null);
|
||||
const _shouldInstrument = identity(null);
|
||||
const _x2 = () => {
|
||||
const _shouldInstrument2 = 'hello world';
|
||||
return identity({_shouldInstrument2});
|
||||
};
|
||||
return (
|
||||
<div style={shouldInstrument} other={_shouldInstrument}>
|
||||
{props.bar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Foo(props) {
|
||||
'use forget';
|
||||
return <Foo>{props.bar}</Foo>;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitFreeze @instrumentForget
|
||||
|
||||
let makeReadOnly = 'conflicting identifier';
|
||||
function useFoo(props) {
|
||||
return foo(props.x);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { makeReadOnly as _makeReadOnly } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @instrumentForget
|
||||
|
||||
let makeReadOnly = "conflicting identifier";
|
||||
function useFoo(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.x) {
|
||||
t0 = foo(props.x);
|
||||
$[0] = props.x;
|
||||
$[1] = __DEV__ ? _makeReadOnly(t0, "useFoo") : t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitFreeze @instrumentForget
|
||||
function useFoo(props) {
|
||||
return foo(props.x, __DEV__);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { makeReadOnly } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitFreeze @instrumentForget
|
||||
function useFoo(props) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.x) {
|
||||
t0 = foo(props.x, __DEV__);
|
||||
$[0] = props.x;
|
||||
$[1] = __DEV__ ? makeReadOnly(t0, "useFoo") : t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: exception) Fixture not implemented
|
||||
@@ -0,0 +1,4 @@
|
||||
// @enableEmitFreeze @instrumentForget
|
||||
function useFoo(props) {
|
||||
return foo(props.x, __DEV__);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitFreeze @instrumentForget
|
||||
|
||||
let makeReadOnly = 'conflicting identifier';
|
||||
function useFoo(props) {
|
||||
return foo(props.x);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
Invariant: Encountered conflicting import specifiers for makeReadOnly in generated program.
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitFreeze @instrumentForget
|
||||
function useFoo(props) {
|
||||
const __DEV__ = 'conflicting global';
|
||||
console.log(__DEV__);
|
||||
return foo(props.x);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @enableEmitFreeze @instrumentForget
|
||||
2 | function useFoo(props) {
|
||||
> 3 | const __DEV__ = 'conflicting global';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Encountered conflicting global in generated program. Conflict from local binding __DEV__ (3:3)
|
||||
4 | console.log(__DEV__);
|
||||
5 | return foo(props.x);
|
||||
6 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// @enableEmitFreeze @instrumentForget
|
||||
function useFoo(props) {
|
||||
const __DEV__ = 'conflicting global';
|
||||
console.log(__DEV__);
|
||||
return foo(props.x);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoJSXInTryStatements
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
function Component(props) {
|
||||
let el;
|
||||
try {
|
||||
let value;
|
||||
try {
|
||||
value = identity(props.foo);
|
||||
} catch {
|
||||
el = <div value={value} />;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
9 | value = identity(props.foo);
|
||||
10 | } catch {
|
||||
> 11 | el = <div value={value} />;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) (11:11)
|
||||
12 | }
|
||||
13 | } catch {
|
||||
14 | return null;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoJSXInTryStatements
|
||||
function Component(props) {
|
||||
let el;
|
||||
try {
|
||||
el = <div />;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
3 | let el;
|
||||
4 | try {
|
||||
> 5 | el = <div />;
|
||||
| ^^^^^^^ InvalidReact: Unexpected JSX element within a try statement. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) (5:5)
|
||||
6 | } catch {
|
||||
7 | return null;
|
||||
8 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInPassiveEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
const f = () => {
|
||||
setState(s => s + 1);
|
||||
};
|
||||
const g = () => {
|
||||
f();
|
||||
};
|
||||
useEffect(() => {
|
||||
g();
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
11 | };
|
||||
12 | useEffect(() => {
|
||||
> 13 | g();
|
||||
| ^ InvalidReact: Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect) (13:13)
|
||||
14 | });
|
||||
15 | return state;
|
||||
16 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoSetStateInPassiveEffects
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
function Component() {
|
||||
const [state, setState] = useState(0);
|
||||
useEffect(() => {
|
||||
setState(s => s + 1);
|
||||
});
|
||||
return state;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
5 | const [state, setState] = useState(0);
|
||||
6 | useEffect(() => {
|
||||
> 7 | setState(s => s + 1);
|
||||
| ^^^^^^^^ InvalidReact: Calling setState directly within a useEffect causes cascading renders and is not recommended. Consider alternatives to useEffect. (https://react.dev/learn/you-might-not-need-an-effect) (7:7)
|
||||
8 | });
|
||||
9 | return state;
|
||||
10 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
|
||||
import { Stringify } from "shared-runtime";
|
||||
const ErrorView = isForgetEnabled_Fixtures()
|
||||
? (t0) => {
|
||||
|
||||
@@ -36,9 +36,9 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { useRenderCounter, shouldInstrument } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation) @gating
|
||||
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @enableEmitInstrumentForget @compilationMode(annotation) @gating
|
||||
const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -20,14 +20,14 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { Stringify } from "shared-runtime";
|
||||
import * as React from "react";
|
||||
|
||||
const Foo = React.forwardRef(Foo_withRef);
|
||||
const _isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
|
||||
function _Foo_withRef_optimized(_$$empty_props_placeholder$$, ref) {
|
||||
const isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
|
||||
function Foo_withRef_optimized(_$$empty_props_placeholder$$, ref) {
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== ref) {
|
||||
@@ -39,16 +39,15 @@ function _Foo_withRef_optimized(_$$empty_props_placeholder$$, ref) {
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _Foo_withRef_unoptimized(
|
||||
function Foo_withRef_unoptimized(
|
||||
_$$empty_props_placeholder$$: $ReadOnly<{}>,
|
||||
ref: React.RefSetter<Controls>,
|
||||
): React.Node {
|
||||
return <Stringify ref={ref} />;
|
||||
}
|
||||
function Foo_withRef(arg0, arg1) {
|
||||
if (_isForgetEnabled_Fixtures_result)
|
||||
return _Foo_withRef_optimized(arg0, arg1);
|
||||
else return _Foo_withRef_unoptimized(arg0, arg1);
|
||||
if (isForgetEnabled_Fixtures_result) return Foo_withRef_optimized(arg0, arg1);
|
||||
else return Foo_withRef_unoptimized(arg0, arg1);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @gating
|
||||
|
||||
export const isForgetEnabled_Fixtures = () => {
|
||||
'use no forget';
|
||||
return false;
|
||||
};
|
||||
|
||||
export function Bar(props) {
|
||||
'use forget';
|
||||
return <div>{props.bar}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: eval('Bar'),
|
||||
params: [{bar: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures as _isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
|
||||
|
||||
export const isForgetEnabled_Fixtures = () => {
|
||||
"use no forget";
|
||||
return false;
|
||||
};
|
||||
|
||||
export const Bar = _isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
const $ = _c(2);
|
||||
let t0;
|
||||
if ($[0] !== props.bar) {
|
||||
t0 = <div>{props.bar}</div>;
|
||||
$[0] = props.bar;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
: function Bar(props) {
|
||||
"use forget";
|
||||
return <div>{props.bar}</div>;
|
||||
};
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: eval("Bar"),
|
||||
params: [{ bar: 2 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>2</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
// @gating
|
||||
|
||||
export const isForgetEnabled_Fixtures = () => {
|
||||
'use no forget';
|
||||
return false;
|
||||
};
|
||||
|
||||
export function Bar(props) {
|
||||
'use forget';
|
||||
return <div>{props.bar}</div>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: eval('Bar'),
|
||||
params: [{bar: 2}],
|
||||
};
|
||||
@@ -18,8 +18,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
|
||||
const Component = isForgetEnabled_Fixtures()
|
||||
? function Component() {
|
||||
const $ = _c(1);
|
||||
|
||||
@@ -24,8 +24,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
|
||||
import { identity, useHook as useRenamed } from "shared-runtime";
|
||||
const _ = {
|
||||
useHook: isForgetEnabled_Fixtures() ? () => {} : () => {},
|
||||
|
||||
@@ -26,8 +26,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
|
||||
const Component = isForgetEnabled_Fixtures()
|
||||
? function Component() {
|
||||
const $ = _c(1);
|
||||
|
||||
@@ -27,8 +27,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
|
||||
const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -34,8 +34,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
|
||||
const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -27,8 +27,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
|
||||
export const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -27,8 +27,8 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating @compilationMode(annotation)
|
||||
const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -21,14 +21,14 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { c as _c } from "react/compiler-runtime"; // @gating
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @gating
|
||||
import { createRef, forwardRef } from "react";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
const Foo = forwardRef(Foo_withRef);
|
||||
const _isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
|
||||
function _Foo_withRef_optimized(props, ref) {
|
||||
const isForgetEnabled_Fixtures_result = isForgetEnabled_Fixtures();
|
||||
function Foo_withRef_optimized(props, ref) {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] !== props || $[1] !== ref) {
|
||||
@@ -41,13 +41,12 @@ function _Foo_withRef_optimized(props, ref) {
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
function _Foo_withRef_unoptimized(props, ref) {
|
||||
function Foo_withRef_unoptimized(props, ref) {
|
||||
return <Stringify ref={ref} {...props} />;
|
||||
}
|
||||
function Foo_withRef(arg0, arg1) {
|
||||
if (_isForgetEnabled_Fixtures_result)
|
||||
return _Foo_withRef_optimized(arg0, arg1);
|
||||
else return _Foo_withRef_unoptimized(arg0, arg1);
|
||||
if (isForgetEnabled_Fixtures_result) return Foo_withRef_optimized(arg0, arg1);
|
||||
else return Foo_withRef_unoptimized(arg0, arg1);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user