Compare commits
207 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc2f6b0fd | ||
|
|
2a5f77c261 | ||
|
|
34179fe344 | ||
|
|
0e7cdebb32 | ||
|
|
81d8115116 | ||
|
|
8f4ce72f0b | ||
|
|
7ce2a63acc | ||
|
|
b067c6fe79 | ||
|
|
e081cb3446 | ||
|
|
7b67dc92b0 | ||
|
|
7c28c15465 | ||
|
|
90ccbd71c1 | ||
|
|
0cf6d0c929 | ||
|
|
df080d228b | ||
|
|
66cfe048d3 | ||
|
|
ae962653d6 | ||
|
|
e1dc03492e | ||
|
|
90bee81902 | ||
|
|
75e78d243f | ||
|
|
5d24c64cc9 | ||
|
|
6b7e207cab | ||
|
|
d60f77a533 | ||
|
|
12bc60f509 | ||
|
|
ed023cfc73 | ||
|
|
a00ca6f6b5 | ||
|
|
888ea60d8e | ||
|
|
b7e2de632b | ||
|
|
ff93c4448c | ||
|
|
6c86e56a0f | ||
|
|
56408a5b12 | ||
|
|
c38e268978 | ||
|
|
80c03eb7e0 | ||
|
|
b6c0aa8814 | ||
|
|
428ab82001 | ||
|
|
4df098c4c2 | ||
|
|
95bcf87e6b | ||
|
|
911dbd9e34 | ||
|
|
c0b5a0cad3 | ||
|
|
e4b88ae4c6 | ||
|
|
6c8bcdaf1b | ||
|
|
b367b60927 | ||
|
|
9666605abf | ||
|
|
65ec57df37 | ||
|
|
b3d5e90786 | ||
|
|
280ff6fed2 | ||
|
|
82f3684c63 | ||
|
|
142aa0744d | ||
|
|
6ccf328499 | ||
|
|
a374e0ec87 | ||
|
|
ab859e31be | ||
|
|
e8d15fa19e | ||
|
|
d177272802 | ||
|
|
22b929156c | ||
|
|
a3be6829c6 | ||
|
|
b1759882c0 | ||
|
|
dddcae7a11 | ||
|
|
43714eb4e9 | ||
|
|
a5110b22f0 | ||
|
|
b4477d3800 | ||
|
|
93f1668045 | ||
|
|
37054867c1 | ||
|
|
d742611ce4 | ||
|
|
1540081725 | ||
|
|
9cc74fec74 | ||
|
|
157ac578de | ||
|
|
45da4e055d | ||
|
|
d8919a0a68 | ||
|
|
2e9f8cd3e0 | ||
|
|
65a46c7eeb | ||
|
|
3fb17d16a4 | ||
|
|
acee65d6d0 | ||
|
|
1ae0a845bd | ||
|
|
2b4064eb9b | ||
|
|
3531b26729 | ||
|
|
4a1f29079c | ||
|
|
526dd340b3 | ||
|
|
ee76351917 | ||
|
|
8b55eb4e72 | ||
|
|
14094f80cb | ||
|
|
5717f1933f | ||
|
|
b07717d857 | ||
|
|
283f87f083 | ||
|
|
f9ae0a4c2e | ||
|
|
f702620cea | ||
|
|
c0464aedb1 | ||
|
|
6a1dfe3777 | ||
|
|
99efc627a5 | ||
|
|
bfaeb4a461 | ||
|
|
3e9db65fc3 | ||
|
|
0d072884f9 | ||
|
|
abf9fd559d | ||
|
|
13f20044f3 | ||
|
|
8ce15b0f56 | ||
|
|
91ac1fea1a | ||
|
|
08064ea671 | ||
|
|
99781d605b | ||
|
|
459a2c4298 | ||
|
|
1c43d0aed7 | ||
|
|
1835b3f7d9 | ||
|
|
f4041aa388 | ||
|
|
3710c4d4f9 | ||
|
|
2388481283 | ||
|
|
9c7b10e22e | ||
|
|
50389e1792 | ||
|
|
99aa685cef | ||
|
|
d38c7e10d3 | ||
|
|
c4676e72a6 | ||
|
|
4c6967be29 | ||
|
|
c6c2a52ad8 | ||
|
|
5dc1b212c3 | ||
|
|
a3abf5f2f8 | ||
|
|
462d08f9ba | ||
|
|
6060367ef8 | ||
|
|
c250b7d980 | ||
|
|
4448b18760 | ||
|
|
4a45ba92c4 | ||
|
|
08cb2d7ee7 | ||
|
|
203df2c940 | ||
|
|
65b5aae010 | ||
|
|
3f67d0857e | ||
|
|
96eb84e493 | ||
|
|
63d664b220 | ||
|
|
d85f86cf01 | ||
|
|
3a5b326d81 | ||
|
|
59440424d0 | ||
|
|
b480865db0 | ||
|
|
62d3f36ea7 | ||
|
|
0cac32d60d | ||
|
|
676f0879f3 | ||
|
|
997c7bc930 | ||
|
|
b94603b955 | ||
|
|
2bcf06b692 | ||
|
|
5d04d73274 | ||
|
|
3820740a7f | ||
|
|
5069e18060 | ||
|
|
21fdf308a1 | ||
|
|
4ca97e4891 | ||
|
|
9b79292ae7 | ||
|
|
ac06829246 | ||
|
|
38ef6550a8 | ||
|
|
b629a865fb | ||
|
|
fbe7bc21b9 | ||
|
|
9518f11856 | ||
|
|
557a64795c | ||
|
|
8a8df5dbdd | ||
|
|
946da518eb | ||
|
|
a437c99ff7 | ||
|
|
4206fe4982 | ||
|
|
4a702865dd | ||
|
|
0ff1d13b80 | ||
|
|
53c9f81049 | ||
|
|
e5a8de81e5 | ||
|
|
7a2c7045ae | ||
|
|
845d93742f | ||
|
|
54a50729cc | ||
|
|
587cb8f896 | ||
|
|
79586c7eb6 | ||
|
|
edf550b679 | ||
|
|
b9cfa0d308 | ||
|
|
c129c2424b | ||
|
|
0c1575cee8 | ||
|
|
52ea641449 | ||
|
|
3ec88e797f | ||
|
|
0ca8420f9d | ||
|
|
0db8db178c | ||
|
|
8570116bd1 | ||
|
|
4f1d2ddf95 | ||
|
|
73d7e816b7 | ||
|
|
ac2cae5245 | ||
|
|
66de8e5a99 | ||
|
|
26ecc98a00 | ||
|
|
f0ca53d133 | ||
|
|
e39b380a21 | ||
|
|
9de0304ad7 | ||
|
|
0d695bea10 | ||
|
|
4c4a57c4f9 | ||
|
|
dc2b11817b | ||
|
|
b5450b0738 | ||
|
|
f150c046ec | ||
|
|
12b094d2f6 | ||
|
|
e5f0315efa | ||
|
|
f739642745 | ||
|
|
0ed6ceb9f6 | ||
|
|
ee7fee8f88 | ||
|
|
ee077b6ccd | ||
|
|
bb57fa7351 | ||
|
|
e9db3cc2d4 | ||
|
|
d8074cbc79 | ||
|
|
71797c871b | ||
|
|
9d795d3808 | ||
|
|
12f4cb85c5 | ||
|
|
90a124a980 | ||
|
|
49ea8bf569 | ||
|
|
9a52ad9fd9 | ||
|
|
fa8e3a251e | ||
|
|
408d055a3b | ||
|
|
fbf29ccaa3 | ||
|
|
62960c67c8 | ||
|
|
cd4e4d7599 | ||
|
|
18212ca960 | ||
|
|
88b9767404 | ||
|
|
0038c501a3 | ||
|
|
5dc00d6b2b | ||
|
|
c498bfce8b | ||
|
|
8e9a5fc6c1 | ||
|
|
89e8875ec4 | ||
|
|
2d0a5e399f |
@@ -496,6 +496,7 @@ module.exports = {
|
||||
'packages/react-devtools-shared/src/devtools/views/**/*.js',
|
||||
'packages/react-devtools-shared/src/hook.js',
|
||||
'packages/react-devtools-shared/src/backend/console.js',
|
||||
'packages/react-devtools-shared/src/backend/fiber/renderer.js',
|
||||
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
|
||||
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
|
||||
],
|
||||
@@ -504,6 +505,7 @@ module.exports = {
|
||||
__IS_FIREFOX__: 'readonly',
|
||||
__IS_EDGE__: 'readonly',
|
||||
__IS_NATIVE__: 'readonly',
|
||||
__IS_INTERNAL_MCP_BUILD__: 'readonly',
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
chrome: 'readonly',
|
||||
},
|
||||
@@ -559,6 +561,7 @@ module.exports = {
|
||||
ConsoleTask: 'readonly', // TOOD: Figure out what the official name of this will be.
|
||||
ReturnType: 'readonly',
|
||||
AnimationFrameID: 'readonly',
|
||||
WeakRef: 'readonly',
|
||||
// For Flow type annotation. Only `BigInt` is valid at runtime.
|
||||
bigint: 'readonly',
|
||||
BigInt: 'readonly',
|
||||
@@ -579,6 +582,7 @@ module.exports = {
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
React$AbstractComponent: 'readonly',
|
||||
@@ -608,6 +612,7 @@ module.exports = {
|
||||
TimeoutID: 'readonly',
|
||||
WheelEventHandler: 'readonly',
|
||||
FinalizationRegistry: 'readonly',
|
||||
Exclude: 'readonly',
|
||||
Omit: 'readonly',
|
||||
Keyframe: 'readonly',
|
||||
PropertyIndexedKeyframes: 'readonly',
|
||||
@@ -634,5 +639,6 @@ module.exports = {
|
||||
AsyncLocalStorage: 'readonly',
|
||||
async_hooks: 'readonly',
|
||||
globalThis: 'readonly',
|
||||
navigation: 'readonly',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,10 +11,12 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
29
.github/workflows/runtime_build_and_test.yml
vendored
29
.github/workflows/runtime_build_and_test.yml
vendored
@@ -280,6 +280,35 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }}
|
||||
|
||||
# Hardcoded to improve parallelism
|
||||
test-linter:
|
||||
name: Test eslint-plugin-react-hooks
|
||||
needs: [runtime_compiler_node_modules_cache]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: |
|
||||
yarn.lock
|
||||
compiler/yarn.lock
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v4
|
||||
id: node_modules
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
key: runtime-and-compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'compiler/yarn.lock') }}
|
||||
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: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
|
||||
- run: yarn workspace eslint-plugin-react-hooks test
|
||||
|
||||
# ----- BUILD -----
|
||||
build_and_lint:
|
||||
name: yarn build and lint
|
||||
|
||||
14
.github/workflows/runtime_commit_artifacts.yml
vendored
14
.github/workflows/runtime_commit_artifacts.yml
vendored
@@ -132,9 +132,9 @@ jobs:
|
||||
mkdir ./compiled/facebook-www/__test_utils__
|
||||
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
|
||||
|
||||
# Move eslint-plugin-react-hooks into eslint-plugin-react-hooks
|
||||
# Copy eslint-plugin-react-hooks
|
||||
mkdir ./compiled/eslint-plugin-react-hooks
|
||||
mv build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
./compiled/eslint-plugin-react-hooks/index.js
|
||||
|
||||
# Move unstable_server-external-runtime.js into facebook-www
|
||||
@@ -167,6 +167,12 @@ jobs:
|
||||
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
|
||||
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
|
||||
|
||||
# Copy eslint-plugin-react-hooks
|
||||
# NOTE: This is different from www, here we include the full package
|
||||
# including package.json to include dependencies in fbsource.
|
||||
mkdir "$BASE_FOLDER/tools"
|
||||
cp -r build/oss-experimental/eslint-plugin-react-hooks "$BASE_FOLDER/tools"
|
||||
|
||||
# Move React Native version file
|
||||
mv build/facebook-react-native/VERSION_NATIVE_FB ./compiled-rn/VERSION_NATIVE_FB
|
||||
|
||||
@@ -326,10 +332,10 @@ jobs:
|
||||
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
|
||||
echo "===================="
|
||||
# Ignore REVISION or lines removing @generated headers.
|
||||
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
echo "Changes detected"
|
||||
echo "===== Changes ====="
|
||||
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
|
||||
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
|
||||
echo "==================="
|
||||
echo "should_commit=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
||||
2
.github/workflows/runtime_discord_notify.yml
vendored
2
.github/workflows/runtime_discord_notify.yml
vendored
@@ -11,10 +11,12 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
check_access:
|
||||
if: ${{ github.event.pull_request.draft == false }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
41
.github/workflows/runtime_prereleases.yml
vendored
41
.github/workflows/runtime_prereleases.yml
vendored
@@ -17,6 +17,17 @@ on:
|
||||
description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
|
||||
required: false
|
||||
type: boolean
|
||||
only_packages:
|
||||
description: Packages to publish (space separated)
|
||||
type: string
|
||||
skip_packages:
|
||||
description: Packages to NOT publish (space separated)
|
||||
type: string
|
||||
dry:
|
||||
required: true
|
||||
description: Dry run instead of publish?
|
||||
type: boolean
|
||||
default: true
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL:
|
||||
description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
|
||||
@@ -61,10 +72,36 @@ jobs:
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: yarn --cwd scripts/release install --frozen-lockfile
|
||||
if: steps.node_modules.outputs.cache-hit != 'true'
|
||||
- run: cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
- run: |
|
||||
GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --ci --tags ${{ inputs.dist_tag }}
|
||||
- name: Check prepared files
|
||||
run: ls -R build/node_modules
|
||||
- if: '${{ inputs.only_packages }}'
|
||||
name: 'Publish ${{ inputs.only_packages }}'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ inputs.skip_packages }}'
|
||||
name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--skipTests \
|
||||
--tags=${{ inputs.dist_tag }} \
|
||||
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
|
||||
name: 'Publish all packages'
|
||||
run: |
|
||||
scripts/release/publish.js \
|
||||
--ci \
|
||||
--tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
|
||||
${{ inputs.dry && '--dry'}}
|
||||
- name: Notify Discord on failure
|
||||
if: failure() && inputs.enableFailureNotification == true
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
|
||||
45
.github/workflows/runtime_prereleases_manual.yml
vendored
45
.github/workflows/runtime_prereleases_manual.yml
vendored
@@ -5,6 +5,25 @@ on:
|
||||
inputs:
|
||||
prerelease_commit_sha:
|
||||
required: true
|
||||
only_packages:
|
||||
description: Packages to publish (space separated)
|
||||
type: string
|
||||
skip_packages:
|
||||
description: Packages to NOT publish (space separated)
|
||||
type: string
|
||||
dry:
|
||||
required: true
|
||||
description: Dry run instead of publish?
|
||||
type: boolean
|
||||
default: true
|
||||
experimental_only:
|
||||
type: boolean
|
||||
description: Only publish to the experimental tag
|
||||
default: false
|
||||
force_notify:
|
||||
description: Force a Discord notification?
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -12,8 +31,26 @@ env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
embed-author-name: ${{ github.event.sender.login }}
|
||||
embed-author-url: ${{ github.event.sender.html_url }}
|
||||
embed-author-icon-url: ${{ github.event.sender.avatar_url }}
|
||||
embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
|
||||
embed-description: |
|
||||
```json
|
||||
${{ toJson(inputs) }}
|
||||
```
|
||||
embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
|
||||
|
||||
publish_prerelease_canary:
|
||||
if: ${{ !inputs.experimental_only }}
|
||||
name: Publish to Canary channel
|
||||
uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
|
||||
permissions:
|
||||
@@ -33,6 +70,9 @@ jobs:
|
||||
# downstream consumers might still expect that tag. We can remove this
|
||||
# after some time has elapsed and the change has been communicated.
|
||||
dist_tag: canary,next
|
||||
only_packages: ${{ inputs.only_packages }}
|
||||
skip_packages: ${{ inputs.skip_packages }}
|
||||
dry: ${{ inputs.dry }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -48,10 +88,15 @@ jobs:
|
||||
# different versions of the same package, even if they use different
|
||||
# dist tags.
|
||||
needs: publish_prerelease_canary
|
||||
# Ensures the job runs even if canary is skipped
|
||||
if: always()
|
||||
with:
|
||||
commit_sha: ${{ inputs.prerelease_commit_sha }}
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
only_packages: ${{ inputs.only_packages }}
|
||||
skip_packages: ${{ inputs.skip_packages }}
|
||||
dry: ${{ inputs.dry }}
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
release_channel: stable
|
||||
dist_tag: canary,next
|
||||
enableFailureNotification: true
|
||||
dry: false
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -43,6 +44,7 @@ jobs:
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
enableFailureNotification: true
|
||||
dry: false
|
||||
secrets:
|
||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
5
.github/workflows/shared_stale.yml
vendored
5
.github/workflows/shared_stale.yml
vendored
@@ -6,7 +6,10 @@ on:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
# https://github.com/actions/stale/tree/v9/?tab=readme-ov-file#recommended-permissions
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
|
||||
|
||||
module.exports = {
|
||||
plugins: ['prettier-plugin-hermes-parser'],
|
||||
bracketSpacing: false,
|
||||
singleQuote: true,
|
||||
bracketSameLine: true,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 80,
|
||||
parser: 'hermes',
|
||||
parser: 'flow',
|
||||
arrowParens: 'avoid',
|
||||
overrides: [
|
||||
{
|
||||
|
||||
14
compiler/.gitignore
vendored
14
compiler/.gitignore
vendored
@@ -1,28 +1,14 @@
|
||||
.DS_Store
|
||||
.spr.yml
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
node_modules
|
||||
.watchmanconfig
|
||||
.watchman-cookie-*
|
||||
dist
|
||||
.vscode
|
||||
!packages/playground/.vscode
|
||||
.spr.yml
|
||||
testfilter.txt
|
||||
|
||||
bundle-oss.sh
|
||||
|
||||
# forgive
|
||||
*.vsix
|
||||
.vscode-test
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## 19.1.0-rc.2 (May 14, 2025)
|
||||
|
||||
## babel-plugin-react-compiler
|
||||
|
||||
* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)
|
||||
|
||||
## 19.1.0-rc.1 (April 21, 2025)
|
||||
|
||||
## eslint-plugin-react-hooks
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode(all)
|
||||
@compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function useFoo(propVal: {+baz: number}) {
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-infer',
|
||||
input: `// @compilationMode(infer)
|
||||
input: `// @compilationMode:"infer"
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function nonReactFn() {
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-all',
|
||||
input: `// @compilationMode(all)
|
||||
input: `// @compilationMode:"all"
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ set -eo pipefail
|
||||
|
||||
HERE=$(pwd)
|
||||
|
||||
cd ../../packages/react-compiler-runtime && yarn --silent link && cd $HERE
|
||||
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd $HERE
|
||||
cd ../../packages/react-compiler-runtime && yarn --silent link && cd "$HERE"
|
||||
cd ../../packages/babel-plugin-react-compiler && yarn --silent link && cd "$HERE"
|
||||
|
||||
yarn --silent link babel-plugin-react-compiler
|
||||
yarn --silent link react-compiler-runtime
|
||||
|
||||
@@ -18,8 +18,9 @@ import {
|
||||
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';
|
||||
import {LoggerEvent, PluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
path: NodePath<t.Program>,
|
||||
@@ -52,32 +53,65 @@ export function validateRestrictedImports(
|
||||
}
|
||||
}
|
||||
|
||||
type ProgramContextOptions = {
|
||||
program: NodePath<t.Program>;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
};
|
||||
export class ProgramContext {
|
||||
/* Program and environment context */
|
||||
/**
|
||||
* Program and environment context
|
||||
*/
|
||||
scope: BabelScope;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
reactRuntimeModule: string;
|
||||
hookPattern: string | null;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
|
||||
/*
|
||||
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
|
||||
* consistently respect the `skip()` function to avoid revisiting a node within
|
||||
* a pass, so we use this set to track nodes that we have compiled.
|
||||
*/
|
||||
alreadyCompiled: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
|
||||
// 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;
|
||||
/**
|
||||
* Metadata from compilation
|
||||
*/
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
inferredEffectLocations: Set<t.SourceLocation> = new Set();
|
||||
|
||||
constructor({
|
||||
program,
|
||||
suppressions,
|
||||
opts,
|
||||
filename,
|
||||
code,
|
||||
hasModuleScopeOptOut,
|
||||
}: ProgramContextOptions) {
|
||||
this.scope = program.scope;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
|
||||
this.opts = opts;
|
||||
this.filename = filename;
|
||||
this.code = code;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
|
||||
this.suppressions = suppressions;
|
||||
this.hasModuleScopeOptOut = hasModuleScopeOptOut;
|
||||
}
|
||||
|
||||
isHookName(name: string): boolean {
|
||||
if (this.hookPattern == null) {
|
||||
if (this.opts.environment.hookPattern == null) {
|
||||
return isHookName(name);
|
||||
} else {
|
||||
const match = new RegExp(this.hookPattern).exec(name);
|
||||
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
|
||||
return (
|
||||
match != null && typeof match[1] === 'string' && isHookName(match[1])
|
||||
);
|
||||
@@ -179,6 +213,12 @@ export class ProgramContext {
|
||||
});
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
logEvent(event: LoggerEvent): void {
|
||||
if (this.opts.logger != null) {
|
||||
this.opts.logger.logEvent(this.filename, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingImports(
|
||||
|
||||
@@ -37,6 +37,14 @@ const PanicThresholdOptionsSchema = z.enum([
|
||||
]);
|
||||
|
||||
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
|
||||
const DynamicGatingOptionsSchema = z.object({
|
||||
source: z.string(),
|
||||
});
|
||||
export type DynamicGatingOptions = z.infer<typeof DynamicGatingOptionsSchema>;
|
||||
const CustomOptOutDirectiveSchema = z
|
||||
.nullable(z.array(z.string()))
|
||||
.default(null);
|
||||
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
|
||||
|
||||
export type PluginOptions = {
|
||||
environment: EnvironmentConfig;
|
||||
@@ -65,6 +73,28 @@ export type PluginOptions = {
|
||||
*/
|
||||
gating: ExternalFunction | null;
|
||||
|
||||
/**
|
||||
* If specified, this enables dynamic gating which matches `use memo if(...)`
|
||||
* directives.
|
||||
*
|
||||
* Example usage:
|
||||
* ```js
|
||||
* // @dynamicGating:{"source":"myModule"}
|
||||
* export function MyComponent() {
|
||||
* 'use memo if(isEnabled)';
|
||||
* return <div>...</div>;
|
||||
* }
|
||||
* ```
|
||||
* This will emit:
|
||||
* ```js
|
||||
* import {isEnabled} from 'myModule';
|
||||
* export const MyComponent = isEnabled()
|
||||
* ? <optimized version>
|
||||
* : <original version>;
|
||||
* ```
|
||||
*/
|
||||
dynamicGating: DynamicGatingOptions | null;
|
||||
|
||||
panicThreshold: PanicThresholdOptions;
|
||||
|
||||
/*
|
||||
@@ -98,7 +128,7 @@ export type PluginOptions = {
|
||||
* provided rules will skip compilation. To disable this feature (never bailout of compilation
|
||||
* even if the default ESLint is suppressed), pass an empty array.
|
||||
*/
|
||||
eslintSuppressionRules?: Array<string> | null | undefined;
|
||||
eslintSuppressionRules: Array<string> | null | undefined;
|
||||
|
||||
flowSuppressions: boolean;
|
||||
/*
|
||||
@@ -106,7 +136,12 @@ export type PluginOptions = {
|
||||
*/
|
||||
ignoreUseNoForget: boolean;
|
||||
|
||||
sources?: Array<string> | ((filename: string) => boolean) | null;
|
||||
/**
|
||||
* Unstable / do not use
|
||||
*/
|
||||
customOptOutDirectives: CustomOptOutDirective;
|
||||
|
||||
sources: Array<string> | ((filename: string) => boolean) | null;
|
||||
|
||||
/**
|
||||
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
|
||||
@@ -244,6 +279,7 @@ export const defaultOptions: PluginOptions = {
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
dynamicGating: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
ignoreUseNoForget: false,
|
||||
@@ -251,6 +287,7 @@ export const defaultOptions: PluginOptions = {
|
||||
return filename.indexOf('node_modules') === -1;
|
||||
},
|
||||
enableReanimatedCheck: true,
|
||||
customOptOutDirectives: null,
|
||||
target: '19',
|
||||
} as const;
|
||||
|
||||
@@ -292,6 +329,40 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'dynamicGating': {
|
||||
if (value == null) {
|
||||
parsedOptions[key] = null;
|
||||
} else {
|
||||
const result = DynamicGatingOptionsSchema.safeParse(value);
|
||||
if (result.success) {
|
||||
parsedOptions[key] = result.data;
|
||||
} else {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason:
|
||||
'Could not parse dynamic gating. Update React Compiler config to fix the error',
|
||||
description: `${fromZodError(result.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'customOptOutDirectives': {
|
||||
const result = CustomOptOutDirectiveSchema.safeParse(value);
|
||||
if (result.success) {
|
||||
parsedOptions[key] = result.data;
|
||||
} else {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason:
|
||||
'Could not parse custom opt out directives. Update React Compiler config to fix the error',
|
||||
description: `${fromZodError(result.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
parsedOptions[key] = value;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,9 @@ import {transformFire} from '../Transform';
|
||||
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -129,6 +132,7 @@ function run(
|
||||
mode,
|
||||
config,
|
||||
contextIdentifiers,
|
||||
func,
|
||||
logger,
|
||||
filename,
|
||||
code,
|
||||
@@ -225,15 +229,27 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
|
||||
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
}
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
@@ -247,8 +263,21 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
}
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
@@ -274,6 +303,13 @@ function runWithEnvironment(
|
||||
if (env.config.validateNoImpureFunctionsInRender) {
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {EnvironmentConfig, ReactFunctionType} from '../HIR/Environment';
|
||||
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
|
||||
import {isHookDeclaration} from '../Utils/HookDeclaration';
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
suppressionsToCompilerError,
|
||||
} from './Suppression';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
|
||||
export type CompilerPass = {
|
||||
opts: PluginOptions;
|
||||
@@ -40,22 +41,102 @@ export type CompilerPass = {
|
||||
};
|
||||
export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
|
||||
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
|
||||
const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
|
||||
|
||||
export function findDirectiveEnablingMemoization(
|
||||
export function tryFindDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
opts: PluginOptions,
|
||||
): Result<t.Directive | null, CompilerError> {
|
||||
const optIn = directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
);
|
||||
if (optIn != null) {
|
||||
return Ok(optIn);
|
||||
}
|
||||
const dynamicGating = findDirectivesDynamicGating(directives, opts);
|
||||
if (dynamicGating.isOk()) {
|
||||
return Ok(dynamicGating.unwrap()?.directive ?? null);
|
||||
} else {
|
||||
return Err(dynamicGating.unwrapErr());
|
||||
}
|
||||
}
|
||||
|
||||
export function findDirectiveDisablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
{customOptOutDirectives}: PluginOptions,
|
||||
): t.Directive | null {
|
||||
if (customOptOutDirectives != null) {
|
||||
return (
|
||||
directives.find(
|
||||
directive =>
|
||||
customOptOutDirectives.indexOf(directive.value.value) !== -1,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
function findDirectivesDynamicGating(
|
||||
directives: Array<t.Directive>,
|
||||
opts: PluginOptions,
|
||||
): Result<
|
||||
{
|
||||
gating: ExternalFunction;
|
||||
directive: t.Directive;
|
||||
} | null,
|
||||
CompilerError
|
||||
> {
|
||||
if (opts.dynamicGating === null) {
|
||||
return Ok(null);
|
||||
}
|
||||
const errors = new CompilerError();
|
||||
const result: Array<{directive: t.Directive; match: string}> = [];
|
||||
|
||||
for (const directive of directives) {
|
||||
const maybeMatch = DYNAMIC_GATING_DIRECTIVE.exec(directive.value.value);
|
||||
if (maybeMatch != null && maybeMatch[1] != null) {
|
||||
if (t.isValidIdentifier(maybeMatch[1])) {
|
||||
result.push({directive, match: maybeMatch[1]});
|
||||
} else {
|
||||
errors.push({
|
||||
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
|
||||
description: `Found '${directive.value.value}'`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
return Err(errors);
|
||||
} else if (result.length > 1) {
|
||||
const error = new CompilerError();
|
||||
error.push({
|
||||
reason: `Multiple dynamic gating directives found`,
|
||||
description: `Expected a single directive but found [${result
|
||||
.map(r => r.directive.value.value)
|
||||
.join(', ')}]`,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
loc: result[0].directive.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
return Err(error);
|
||||
} else if (result.length === 1) {
|
||||
return Ok({
|
||||
gating: {
|
||||
source: opts.dynamicGating.source,
|
||||
importSpecifierName: result[0].match,
|
||||
},
|
||||
directive: result[0].directive,
|
||||
});
|
||||
} else {
|
||||
return Ok(null);
|
||||
}
|
||||
}
|
||||
|
||||
function isCriticalError(err: unknown): boolean {
|
||||
return !(err instanceof CompilerError) || err.isCritical();
|
||||
@@ -88,13 +169,16 @@ export type CompileResult = {
|
||||
|
||||
function logError(
|
||||
err: unknown,
|
||||
pass: CompilerPass,
|
||||
context: {
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
},
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
if (pass.opts.logger) {
|
||||
if (context.opts.logger) {
|
||||
if (err instanceof CompilerError) {
|
||||
for (const detail of err.details) {
|
||||
pass.opts.logger.logEvent(pass.filename, {
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc,
|
||||
detail: detail.options,
|
||||
@@ -108,7 +192,7 @@ function logError(
|
||||
stringifiedError = err?.toString() ?? '[ null ]';
|
||||
}
|
||||
|
||||
pass.opts.logger.logEvent(pass.filename, {
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
kind: 'PipelineError',
|
||||
fnLoc,
|
||||
data: stringifiedError,
|
||||
@@ -118,13 +202,17 @@ function logError(
|
||||
}
|
||||
function handleError(
|
||||
err: unknown,
|
||||
pass: CompilerPass,
|
||||
context: {
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
},
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
logError(err, pass, fnLoc);
|
||||
logError(err, context, fnLoc);
|
||||
if (
|
||||
pass.opts.panicThreshold === 'all_errors' ||
|
||||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
|
||||
context.opts.panicThreshold === 'all_errors' ||
|
||||
(context.opts.panicThreshold === 'critical_errors' &&
|
||||
isCriticalError(err)) ||
|
||||
isConfigError(err) // Always throws regardless of panic threshold
|
||||
) {
|
||||
throw err;
|
||||
@@ -187,7 +275,6 @@ export function createNewFunctionNode(
|
||||
}
|
||||
}
|
||||
// Avoid visiting the new transformed version
|
||||
ALREADY_COMPILED.add(transformedFn);
|
||||
return transformedFn;
|
||||
}
|
||||
|
||||
@@ -239,13 +326,6 @@ function insertNewOutlinedFunctionNode(
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
|
||||
* consistently respect the `skip()` function to avoid revisiting a node within
|
||||
* a pass, so we use this set to track nodes that we have compiled.
|
||||
*/
|
||||
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
|
||||
|
||||
const DEFAULT_ESLINT_SUPPRESSIONS = [
|
||||
'react-hooks/exhaustive-deps',
|
||||
'react-hooks/rules-of-hooks',
|
||||
@@ -268,41 +348,43 @@ function isFilePartOfSources(
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CompileProgramResult = {
|
||||
export type CompileProgramMetadata = {
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
|
||||
inferredEffectLocations: Set<t.SourceLocation>;
|
||||
};
|
||||
/**
|
||||
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
|
||||
* exceptions thrown by this function will fail the babel build.
|
||||
* - call `handleError` if your error is recoverable.
|
||||
* Unless the error is a warning / info diagnostic, compilation of a function
|
||||
* / entire file should also be skipped.
|
||||
* - throw an exception if the error is fatal / not recoverable.
|
||||
* Examples of this are invalid compiler configs or failure to codegen outlined
|
||||
* functions *after* already emitting optimized components / hooks that invoke
|
||||
* the outlined functions.
|
||||
* Main entrypoint for React Compiler.
|
||||
*
|
||||
* @param program The Babel program node to compile
|
||||
* @param pass Compiler configuration and context
|
||||
* @returns Compilation results or null if compilation was skipped
|
||||
*/
|
||||
export function compileProgram(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): CompileProgramResult | null {
|
||||
): CompileProgramMetadata | null {
|
||||
/**
|
||||
* This is directly invoked by the react-compiler babel plugin, so exceptions
|
||||
* thrown by this function will fail the babel build.
|
||||
* - call `handleError` if your error is recoverable.
|
||||
* Unless the error is a warning / info diagnostic, compilation of a function
|
||||
* / entire file should also be skipped.
|
||||
* - throw an exception if the error is fatal / not recoverable.
|
||||
* Examples of this are invalid compiler configs or failure to codegen outlined
|
||||
* functions *after* already emitting optimized components / hooks that invoke
|
||||
* the outlined functions.
|
||||
*/
|
||||
if (shouldSkipCompilation(program, pass)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const environment = pass.opts.environment;
|
||||
const restrictedImportsErr = validateRestrictedImports(program, environment);
|
||||
const restrictedImportsErr = validateRestrictedImports(
|
||||
program,
|
||||
pass.opts.environment,
|
||||
);
|
||||
if (restrictedImportsErr) {
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -313,16 +395,103 @@ export function compileProgram(
|
||||
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
|
||||
pass.opts.flowSuppressions,
|
||||
);
|
||||
const queue: Array<{
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
fnType: ReactFunctionType;
|
||||
}> = [];
|
||||
|
||||
const programContext = new ProgramContext({
|
||||
program: program,
|
||||
opts: pass.opts,
|
||||
filename: pass.filename,
|
||||
code: pass.code,
|
||||
suppressions,
|
||||
hasModuleScopeOptOut:
|
||||
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
|
||||
null,
|
||||
});
|
||||
|
||||
const queue: Array<CompileSource> = findFunctionsToCompile(
|
||||
program,
|
||||
pass,
|
||||
programContext,
|
||||
);
|
||||
const compiledFns: Array<CompileResult> = [];
|
||||
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType, programContext);
|
||||
|
||||
if (compiled != null) {
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
current.fn,
|
||||
outlined.fn,
|
||||
);
|
||||
fn.skip();
|
||||
programContext.alreadyCompiled.add(fn.node);
|
||||
if (outlined.type !== null) {
|
||||
queue.push({
|
||||
kind: 'outlined',
|
||||
fn,
|
||||
fnType: outlined.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
compiledFns.push({
|
||||
kind: current.kind,
|
||||
originalFn: current.fn,
|
||||
compiledFn: compiled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid modifying the program if we find a program level opt-out
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
if (compiledFns.length > 0) {
|
||||
const error = new CompilerError();
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
handleError(error, programContext, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Insert React Compiler generated functions into the Babel AST
|
||||
applyCompiledFunctions(program, compiledFns, pass, programContext);
|
||||
|
||||
return {
|
||||
retryErrors: programContext.retryErrors,
|
||||
inferredEffectLocations: programContext.inferredEffectLocations,
|
||||
};
|
||||
}
|
||||
|
||||
type CompileSource = {
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
fnType: ReactFunctionType;
|
||||
};
|
||||
/**
|
||||
* Find all React components and hooks that need to be compiled
|
||||
*
|
||||
* @returns An array of React functions from @param program to transform
|
||||
*/
|
||||
function findFunctionsToCompile(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): Array<CompileSource> {
|
||||
const queue: Array<CompileSource> = [];
|
||||
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
|
||||
const fnType = getReactFunctionType(fn, pass, environment);
|
||||
if (fnType === null || ALREADY_COMPILED.has(fn.node)) {
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,7 +500,7 @@ export function compileProgram(
|
||||
* traversal will loop infinitely.
|
||||
* Ensure we avoid visiting the original function again.
|
||||
*/
|
||||
ALREADY_COMPILED.add(fn.node);
|
||||
programContext.alreadyCompiled.add(fn.node);
|
||||
fn.skip();
|
||||
|
||||
queue.push({kind: 'original', fn, fnType});
|
||||
@@ -346,7 +515,6 @@ export function compileProgram(
|
||||
* can reference `this` which is unsafe for compilation
|
||||
*/
|
||||
node.skip();
|
||||
return;
|
||||
},
|
||||
|
||||
ClassExpression(node: NodePath<t.ClassExpression>) {
|
||||
@@ -355,7 +523,6 @@ export function compileProgram(
|
||||
* can reference `this` which is unsafe for compilation
|
||||
*/
|
||||
node.skip();
|
||||
return;
|
||||
},
|
||||
|
||||
FunctionDeclaration: traverseFunction,
|
||||
@@ -370,223 +537,254 @@ export function compileProgram(
|
||||
filename: pass.filename ?? null,
|
||||
},
|
||||
);
|
||||
const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
const inferredEffectLocations = new Set<t.SourceLocation>();
|
||||
const processFn = (
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
): null | CodegenFunction => {
|
||||
let optInDirectives: Array<t.Directive> = [];
|
||||
let optOutDirectives: Array<t.Directive> = [];
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
optInDirectives = findDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
);
|
||||
optOutDirectives = findDirectiveDisablingMemoization(
|
||||
fn.node.body.directives,
|
||||
);
|
||||
}
|
||||
return queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
suppressions,
|
||||
fn,
|
||||
/**
|
||||
* Try to compile a source function, taking into account all local suppressions,
|
||||
* opt-ins, and opt-outs.
|
||||
*
|
||||
* Errors encountered during compilation are either logged (if recoverable) or
|
||||
* thrown (if non-recoverable).
|
||||
*
|
||||
* @returns the compiled function or null if the function was skipped (due to
|
||||
* config settings and/or outputs)
|
||||
*/
|
||||
function processFn(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): null | CodegenFunction {
|
||||
let directives: {
|
||||
optIn: t.Directive | null;
|
||||
optOut: t.Directive | null;
|
||||
};
|
||||
if (fn.node.body.type !== 'BlockStatement') {
|
||||
directives = {
|
||||
optIn: null,
|
||||
optOut: null,
|
||||
};
|
||||
} else {
|
||||
const optIn = tryFindDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
programContext.opts,
|
||||
);
|
||||
let compileResult:
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown};
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
compileResult = {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
};
|
||||
if (optIn.isErr()) {
|
||||
/**
|
||||
* If parsing opt-in directive fails, it's most likely that React Compiler
|
||||
* was not tested or rolled out on this function. In that case, we handle
|
||||
* the error and fall back to the safest option which is to not optimize
|
||||
* the function.
|
||||
*/
|
||||
handleError(optIn.unwrapErr(), programContext, fn.node.loc ?? null);
|
||||
return null;
|
||||
}
|
||||
directives = {
|
||||
optIn: optIn.unwrapOr(null),
|
||||
optOut: findDirectiveDisablingMemoization(
|
||||
fn.node.body.directives,
|
||||
programContext.opts,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
const compileResult = tryCompileFunction(fn, fnType, programContext);
|
||||
if (compileResult.kind === 'error') {
|
||||
if (directives.optOut != null) {
|
||||
logError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
} else {
|
||||
try {
|
||||
compileResult = {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
compileResult = {kind: 'error', error: err};
|
||||
}
|
||||
handleError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
}
|
||||
|
||||
if (compileResult.kind === 'error') {
|
||||
/**
|
||||
* If an opt out directive is present, log only instead of throwing and don't mark as
|
||||
* containing a critical error.
|
||||
*/
|
||||
if (optOutDirectives.length > 0) {
|
||||
logError(compileResult.error, pass, fn.node.loc ?? null);
|
||||
} else {
|
||||
handleError(compileResult.error, pass, fn.node.loc ?? null);
|
||||
}
|
||||
// If non-memoization features are enabled, retry regardless of error kind
|
||||
if (
|
||||
!(environment.enableFire || environment.inferEffectDependencies != null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
compileResult = {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
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) {
|
||||
retryErrors.push({fn, error: err});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
|
||||
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
|
||||
* unused 'use no forget/memo' directive.
|
||||
*/
|
||||
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
|
||||
for (const directive of optOutDirectives) {
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directive.value.value}' directive.`,
|
||||
loc: directive.loc ?? null,
|
||||
});
|
||||
}
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
compiledFn = compileResult.compiledFn;
|
||||
}
|
||||
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
fnName: compileResult.compiledFn.id?.name ?? null,
|
||||
memoSlots: compileResult.compiledFn.memoSlotsUsed,
|
||||
memoBlocks: compileResult.compiledFn.memoBlocks,
|
||||
memoValues: compileResult.compiledFn.memoValues,
|
||||
prunedMemoBlocks: compileResult.compiledFn.prunedMemoBlocks,
|
||||
prunedMemoValues: compileResult.compiledFn.prunedMemoValues,
|
||||
/**
|
||||
* If 'use no forget/memo' is present and we still ran the code through the
|
||||
* compiler for validation, log a skip event and don't mutate the babel AST.
|
||||
* This allows us to flag if there is an unused 'use no forget/memo'
|
||||
* directive.
|
||||
*/
|
||||
if (
|
||||
programContext.opts.ignoreUseNoForget === false &&
|
||||
directives.optOut != null
|
||||
) {
|
||||
programContext.logEvent({
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directives.optOut.value}' directive.`,
|
||||
loc: directives.optOut.loc ?? null,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
programContext.logEvent({
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
fnName: compiledFn.id?.name ?? null,
|
||||
memoSlots: compiledFn.memoSlotsUsed,
|
||||
memoBlocks: compiledFn.memoBlocks,
|
||||
memoValues: compiledFn.memoValues,
|
||||
prunedMemoBlocks: compiledFn.prunedMemoBlocks,
|
||||
prunedMemoValues: compiledFn.prunedMemoValues,
|
||||
});
|
||||
|
||||
/**
|
||||
* Always compile functions with opt in directives.
|
||||
*/
|
||||
if (optInDirectives.length > 0) {
|
||||
return compileResult.compiledFn;
|
||||
} else if (pass.opts.compilationMode === 'annotation') {
|
||||
/**
|
||||
* No opt-in directive in annotation mode, so don't insert the compiled function.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pass.opts.noEmit) {
|
||||
return compileResult.compiledFn;
|
||||
}
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
return null;
|
||||
} else if (programContext.opts.noEmit) {
|
||||
/**
|
||||
* inferEffectDependencies + noEmit is currently only used for linting. In
|
||||
* this mode, add source locations for where the compiler *can* infer effect
|
||||
* dependencies.
|
||||
*/
|
||||
for (const loc of compileResult.compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) inferredEffectLocations.add(loc);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType);
|
||||
if (compiled === null) {
|
||||
continue;
|
||||
}
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
current.fn,
|
||||
outlined.fn,
|
||||
);
|
||||
fn.skip();
|
||||
ALREADY_COMPILED.add(fn.node);
|
||||
if (outlined.type !== null) {
|
||||
queue.push({
|
||||
kind: 'outlined',
|
||||
fn,
|
||||
fnType: outlined.type,
|
||||
});
|
||||
for (const loc of compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) {
|
||||
programContext.inferredEffectLocations.add(loc);
|
||||
}
|
||||
}
|
||||
compiledFns.push({
|
||||
kind: current.kind,
|
||||
compiledFn: compiled,
|
||||
originalFn: current.fn,
|
||||
});
|
||||
return null;
|
||||
} else if (
|
||||
programContext.opts.compilationMode === 'annotation' &&
|
||||
directives.optIn == null
|
||||
) {
|
||||
/**
|
||||
* If no opt-in directive is found and the compiler is configured in
|
||||
* annotation mode, don't insert the compiled function.
|
||||
*/
|
||||
return null;
|
||||
} else {
|
||||
return compiledFn;
|
||||
}
|
||||
}
|
||||
|
||||
function tryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
):
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown} {
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
programContext.suppressions,
|
||||
fn,
|
||||
);
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not modify source if there is a module scope level opt out directive.
|
||||
*/
|
||||
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
|
||||
program.node.directives,
|
||||
);
|
||||
if (moduleScopeOptOutDirectives.length > 0) {
|
||||
try {
|
||||
return {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
return {kind: 'error', error: err};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If non-memo feature flags are enabled, retry compilation with a more minimal
|
||||
* feature set.
|
||||
*
|
||||
* @returns a CodegenFunction if retry was successful
|
||||
*/
|
||||
function retryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): CodegenFunction | null {
|
||||
const environment = programContext.opts.environment;
|
||||
if (
|
||||
!(environment.enableFire || environment.inferEffectDependencies != null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
* Only insert Forget-ified functions if we have not encountered a critical
|
||||
* error elsewhere in the file, regardless of bailout mode.
|
||||
/**
|
||||
* Note that function suppressions are not checked in the retry pipeline, as
|
||||
* they only affect auto-memoization features.
|
||||
*/
|
||||
const referencedBeforeDeclared =
|
||||
pass.opts.gating != null
|
||||
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
|
||||
: null;
|
||||
try {
|
||||
const retryResult = compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
);
|
||||
|
||||
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
|
||||
return null;
|
||||
}
|
||||
return retryResult;
|
||||
} catch (err) {
|
||||
// TODO: we might want to log error here, but this will also result in duplicate logging
|
||||
if (err instanceof CompilerError) {
|
||||
programContext.retryErrors.push({fn, error: err});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies React Compiler generated functions to the babel AST by replacing
|
||||
* existing functions in place or inserting new declarations.
|
||||
*/
|
||||
function applyCompiledFunctions(
|
||||
program: NodePath<t.Program>,
|
||||
compiledFns: Array<CompileResult>,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
let referencedBeforeDeclared = null;
|
||||
for (const result of compiledFns) {
|
||||
const {kind, originalFn, compiledFn} = result;
|
||||
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
|
||||
programContext.alreadyCompiled.add(transformedFn);
|
||||
|
||||
if (referencedBeforeDeclared != null && kind === 'original') {
|
||||
CompilerError.invariant(pass.opts.gating != null, {
|
||||
reason: "Expected 'gating' import to be present",
|
||||
loc: null,
|
||||
});
|
||||
let dynamicGating: ExternalFunction | null = null;
|
||||
if (originalFn.node.body.type === 'BlockStatement') {
|
||||
const result = findDirectivesDynamicGating(
|
||||
originalFn.node.body.directives,
|
||||
pass.opts,
|
||||
);
|
||||
if (result.isOk()) {
|
||||
dynamicGating = result.unwrap()?.gating ?? null;
|
||||
}
|
||||
}
|
||||
const functionGating = dynamicGating ?? pass.opts.gating;
|
||||
if (kind === 'original' && functionGating != null) {
|
||||
referencedBeforeDeclared ??=
|
||||
getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns);
|
||||
insertGatedFunctionDeclaration(
|
||||
originalFn,
|
||||
transformedFn,
|
||||
programContext,
|
||||
pass.opts.gating,
|
||||
functionGating,
|
||||
referencedBeforeDeclared.has(result),
|
||||
);
|
||||
} else {
|
||||
@@ -598,7 +796,6 @@ export function compileProgram(
|
||||
if (compiledFns.length > 0) {
|
||||
addImportsToProgram(program, programContext);
|
||||
}
|
||||
return {retryErrors, inferredEffectLocations};
|
||||
}
|
||||
|
||||
function shouldSkipCompilation(
|
||||
@@ -640,15 +837,16 @@ function shouldSkipCompilation(
|
||||
function getReactFunctionType(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
/**
|
||||
* TODO(mofeiZ): remove once we validate PluginOptions with Zod
|
||||
*/
|
||||
environment: EnvironmentConfig,
|
||||
): ReactFunctionType | null {
|
||||
const hookPattern = environment.hookPattern;
|
||||
const hookPattern = pass.opts.environment.hookPattern;
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
|
||||
const optInDirectives = tryFindDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
pass.opts,
|
||||
);
|
||||
if (optInDirectives.unwrapOr(null) != null) {
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
// Component and hook declarations are known components/hooks
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramResult} from './Program';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
@@ -109,7 +109,7 @@ export default function validateNoUntransformedReferences(
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
env: EnvironmentConfig,
|
||||
compileResult: CompileProgramResult | null,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
): void {
|
||||
const moduleLoadChecks = new Map<
|
||||
string,
|
||||
@@ -236,7 +236,7 @@ function transformProgram(
|
||||
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
compileResult: CompileProgramResult | null,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
): void {
|
||||
const traversalState: TraversalState = {
|
||||
shouldInvalidateScopes: true,
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import invariant from 'invariant';
|
||||
import {HIRFunction, Identifier, MutableRange} from './HIR';
|
||||
import {HIRFunction, MutableRange, Place} from './HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from './visitors';
|
||||
import {CompilerError} from '..';
|
||||
import {printPlace} from './PrintHIR';
|
||||
|
||||
/*
|
||||
* Checks that all mutable ranges in the function are well-formed, with
|
||||
@@ -20,38 +21,43 @@ import {
|
||||
export function assertValidMutableRanges(fn: HIRFunction): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
visitIdentifier(phi.place.identifier);
|
||||
for (const [, operand] of phi.operands) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(phi.place, `phi for block bb${block.id}`);
|
||||
for (const [pred, operand] of phi.operands) {
|
||||
visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(operand, `instruction [${instr.id}]`);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(operand, `instruction [${instr.id}]`);
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
visitIdentifier(operand.identifier);
|
||||
visit(operand, `terminal [${block.terminal.id}]`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visitIdentifier(identifier: Identifier): void {
|
||||
validateMutableRange(identifier.mutableRange);
|
||||
if (identifier.scope !== null) {
|
||||
validateMutableRange(identifier.scope.range);
|
||||
function visit(place: Place, description: string): void {
|
||||
validateMutableRange(place, place.identifier.mutableRange, description);
|
||||
if (place.identifier.scope !== null) {
|
||||
validateMutableRange(place, place.identifier.scope.range, description);
|
||||
}
|
||||
}
|
||||
|
||||
function validateMutableRange(mutableRange: MutableRange): void {
|
||||
invariant(
|
||||
(mutableRange.start === 0 && mutableRange.end === 0) ||
|
||||
mutableRange.end > mutableRange.start,
|
||||
'Identifier scope mutableRange was invalid: [%s:%s]',
|
||||
mutableRange.start,
|
||||
mutableRange.end,
|
||||
function validateMutableRange(
|
||||
place: Place,
|
||||
range: MutableRange,
|
||||
description: string,
|
||||
): void {
|
||||
CompilerError.invariant(
|
||||
(range.start === 0 && range.end === 0) || range.end > range.start,
|
||||
{
|
||||
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
|
||||
description: `${printPlace(place)} in ${description}`,
|
||||
loc: place.loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings} from './HIRBuilder';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
|
||||
/*
|
||||
@@ -70,21 +70,23 @@ import {BuiltInArrayId} from './ObjectShape';
|
||||
export function lower(
|
||||
func: NodePath<t.Function>,
|
||||
env: Environment,
|
||||
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
|
||||
bindings: Bindings | null = null,
|
||||
capturedRefs: Array<t.Identifier> = [],
|
||||
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
|
||||
parent: NodePath<t.Function> | null = null,
|
||||
capturedRefs: Map<t.Identifier, SourceLocation> = new Map(),
|
||||
): Result<HIRFunction, CompilerError> {
|
||||
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
|
||||
const builder = new HIRBuilder(env, {
|
||||
bindings,
|
||||
context: capturedRefs,
|
||||
});
|
||||
const context: HIRFunction['context'] = [];
|
||||
|
||||
for (const ref of capturedRefs ?? []) {
|
||||
for (const [ref, loc] of capturedRefs ?? []) {
|
||||
context.push({
|
||||
kind: 'Identifier',
|
||||
identifier: builder.resolveBinding(ref),
|
||||
effect: Effect.Unknown,
|
||||
reactive: false,
|
||||
loc: ref.loc ?? GeneratedSource,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,6 +181,7 @@ export function lower(
|
||||
loc: GeneratedSource,
|
||||
value: lowerExpressionToTemporary(builder, body),
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
};
|
||||
builder.terminateWithContinuation(terminal, fallthrough);
|
||||
} else if (body.isBlockStatement()) {
|
||||
@@ -208,6 +211,7 @@ export function lower(
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -215,9 +219,10 @@ export function lower(
|
||||
return Ok({
|
||||
id,
|
||||
params,
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
fnType: bindings == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
|
||||
body: builder.build(),
|
||||
context,
|
||||
generator: func.node.generator === true,
|
||||
@@ -225,6 +230,7 @@ export function lower(
|
||||
loc: func.node.loc ?? GeneratedSource,
|
||||
env,
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
directives,
|
||||
});
|
||||
}
|
||||
@@ -285,6 +291,7 @@ function lowerStatement(
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
value,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
};
|
||||
builder.terminate(terminal, 'block');
|
||||
return;
|
||||
@@ -1235,6 +1242,7 @@ function lowerStatement(
|
||||
kind: 'Debugger',
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
return;
|
||||
@@ -1892,6 +1900,7 @@ function lowerExpression(
|
||||
place: leftValue,
|
||||
loc: exprLoc,
|
||||
},
|
||||
effects: null,
|
||||
loc: exprLoc,
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
@@ -2827,6 +2836,7 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
@@ -2840,6 +2850,7 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -3417,7 +3428,7 @@ function lowerFunction(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
): LoweredFunction | null {
|
||||
const componentScope: Scope = builder.parentFunction.scope;
|
||||
const componentScope: Scope = builder.environment.parentFunction.scope;
|
||||
const capturedContext = gatherCapturedContext(expr, componentScope);
|
||||
|
||||
/*
|
||||
@@ -3432,8 +3443,7 @@ function lowerFunction(
|
||||
expr,
|
||||
builder.environment,
|
||||
builder.bindings,
|
||||
[...builder.context, ...capturedContext],
|
||||
builder.parentFunction,
|
||||
new Map([...builder.context, ...capturedContext]),
|
||||
);
|
||||
let loweredFunc: HIRFunction;
|
||||
if (lowering.isErr()) {
|
||||
@@ -3456,7 +3466,7 @@ function lowerExpressionToTemporary(
|
||||
return lowerValueToTemporary(builder, value);
|
||||
}
|
||||
|
||||
function lowerValueToTemporary(
|
||||
export function lowerValueToTemporary(
|
||||
builder: HIRBuilder,
|
||||
value: InstructionValue,
|
||||
): Place {
|
||||
@@ -3466,9 +3476,10 @@ function lowerValueToTemporary(
|
||||
const place: Place = buildTemporaryPlace(builder, value.loc);
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
value: value,
|
||||
loc: value.loc,
|
||||
lvalue: {...place},
|
||||
value: value,
|
||||
effects: null,
|
||||
loc: value.loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
@@ -3609,31 +3620,40 @@ function lowerAssignment(
|
||||
|
||||
let temporary;
|
||||
if (builder.isContextIdentifier(lvalue)) {
|
||||
if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
|
||||
if (kind === InstructionKind.Const) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place: {...place},
|
||||
},
|
||||
loc: place.loc,
|
||||
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
|
||||
builder.errors.push({
|
||||
reason: `Expected \`const\` declaration not to be reassigned`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'StoreContext',
|
||||
lvalue: {place: {...place}, kind: InstructionKind.Reassign},
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
if (
|
||||
kind !== InstructionKind.Const &&
|
||||
kind !== InstructionKind.Reassign &&
|
||||
kind !== InstructionKind.Let &&
|
||||
kind !== InstructionKind.Function
|
||||
) {
|
||||
builder.errors.push({
|
||||
reason: `Unexpected context variable kind`,
|
||||
severity: ErrorSeverity.InvalidJS,
|
||||
loc: lvalue.node.loc ?? null,
|
||||
suggestions: null,
|
||||
});
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'UnsupportedNode',
|
||||
node: lvalueNode,
|
||||
loc: lvalueNode.loc ?? GeneratedSource,
|
||||
});
|
||||
} else {
|
||||
temporary = lowerValueToTemporary(builder, {
|
||||
kind: 'StoreContext',
|
||||
lvalue: {place: {...place}, kind},
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const typeAnnotation = lvalue.get('typeAnnotation');
|
||||
let type: t.FlowType | t.TSType | null;
|
||||
@@ -4142,6 +4162,11 @@ function captureScopes({from, to}: {from: Scope; to: Scope}): Set<Scope> {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapping of "context" identifiers — references to free variables that
|
||||
* will become part of the function expression's `context` array — along with the
|
||||
* source location of their first reference within the function.
|
||||
*/
|
||||
function gatherCapturedContext(
|
||||
fn: NodePath<
|
||||
| t.FunctionExpression
|
||||
@@ -4150,8 +4175,8 @@ function gatherCapturedContext(
|
||||
| t.ObjectMethod
|
||||
>,
|
||||
componentScope: Scope,
|
||||
): Array<t.Identifier> {
|
||||
const capturedIds = new Set<t.Identifier>();
|
||||
): Map<t.Identifier, SourceLocation> {
|
||||
const capturedIds = new Map<t.Identifier, SourceLocation>();
|
||||
|
||||
/*
|
||||
* Capture all the scopes from the parent of this function up to and including
|
||||
@@ -4194,8 +4219,15 @@ function gatherCapturedContext(
|
||||
|
||||
// Add the base identifier binding as a dependency.
|
||||
const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
|
||||
if (binding !== undefined && pureScopes.has(binding.scope)) {
|
||||
capturedIds.add(binding.identifier);
|
||||
if (
|
||||
binding !== undefined &&
|
||||
pureScopes.has(binding.scope) &&
|
||||
!capturedIds.has(binding.identifier)
|
||||
) {
|
||||
capturedIds.set(
|
||||
binding.identifier,
|
||||
path.node.loc ?? binding.identifier.loc ?? GeneratedSource,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4232,7 +4264,7 @@ function gatherCapturedContext(
|
||||
},
|
||||
});
|
||||
|
||||
return [...capturedIds.keys()];
|
||||
return capturedIds;
|
||||
}
|
||||
|
||||
function notNull<T>(value: T | null): value is T {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
DependencyPathEntry,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
getHookKind,
|
||||
HIRFunction,
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
PropertyLiteral,
|
||||
ReactiveScopeDependency,
|
||||
ScopeId,
|
||||
TInstruction,
|
||||
} from './HIR';
|
||||
|
||||
const DEBUG_PRINT = false;
|
||||
@@ -127,6 +129,33 @@ export function collectHoistablePropertyLoads(
|
||||
});
|
||||
}
|
||||
|
||||
export function collectHoistablePropertyLoadsInInnerFn(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
|
||||
): ReadonlyMap<BlockId, BlockInfo> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
const initialContext: CollectHoistablePropertyLoadsContext = {
|
||||
temporaries,
|
||||
knownImmutableIdentifiers: new Set(),
|
||||
hoistableFromOptionals,
|
||||
registry: new PropertyPathRegistry(),
|
||||
nestedFnImmutableContext: null,
|
||||
assumedInvokedFns: fn.env.config.enableTreatFunctionDepsAsConditional
|
||||
? new Set()
|
||||
: getAssumedInvokedFunctions(fn),
|
||||
};
|
||||
const nestedFnImmutableContext = new Set(
|
||||
fn.context
|
||||
.filter(place =>
|
||||
isImmutableAtInstr(place.identifier, fnInstr.id, initialContext),
|
||||
)
|
||||
.map(place => place.identifier.id),
|
||||
);
|
||||
initialContext.nestedFnImmutableContext = nestedFnImmutableContext;
|
||||
return collectHoistablePropertyLoadsImpl(fn, initialContext);
|
||||
}
|
||||
|
||||
type CollectHoistablePropertyLoadsContext = {
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
|
||||
knownImmutableIdentifiers: ReadonlySet<IdentifierId>;
|
||||
@@ -212,7 +241,10 @@ type PropertyPathNode =
|
||||
class PropertyPathRegistry {
|
||||
roots: Map<IdentifierId, RootNode> = new Map();
|
||||
|
||||
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
|
||||
getOrCreateIdentifier(
|
||||
identifier: Identifier,
|
||||
reactive: boolean,
|
||||
): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
* with the exception of TDZ (not addressed by this pass).
|
||||
@@ -226,12 +258,19 @@ class PropertyPathRegistry {
|
||||
optionalProperties: new Map(),
|
||||
fullPath: {
|
||||
identifier,
|
||||
reactive,
|
||||
path: [],
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
};
|
||||
this.roots.set(identifier.id, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
|
||||
reason:
|
||||
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
|
||||
loc: identifier.loc,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -249,6 +288,7 @@ class PropertyPathRegistry {
|
||||
parent: parent,
|
||||
fullPath: {
|
||||
identifier: parent.fullPath.identifier,
|
||||
reactive: parent.fullPath.reactive,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
@@ -264,7 +304,7 @@ class PropertyPathRegistry {
|
||||
* so all subpaths of a PropertyLoad should already exist
|
||||
* (e.g. a.b is added before a.b.c),
|
||||
*/
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier);
|
||||
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
@@ -286,10 +326,11 @@ function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
context: CollectHoistablePropertyLoadsContext,
|
||||
): PropertyPathNode | null {
|
||||
let path = null;
|
||||
let path: ReactiveScopeDependency | null = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = context.temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
reactive: instr.object.reactive,
|
||||
path: [],
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
@@ -352,7 +393,7 @@ function collectNonNullsInBlocks(
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(
|
||||
context.registry.getOrCreateIdentifier(identifier),
|
||||
context.registry.getOrCreateIdentifier(identifier, true),
|
||||
);
|
||||
}
|
||||
const nodes = new Map<
|
||||
@@ -587,9 +628,11 @@ function reduceMaybeOptionalChains(
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath} = original.fullPath;
|
||||
let currNode: PropertyPathNode =
|
||||
registry.getOrCreateIdentifier(identifier);
|
||||
let {identifier, path: origPath, reactive} = original.fullPath;
|
||||
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
|
||||
identifier,
|
||||
reactive,
|
||||
);
|
||||
for (let i = 0; i < origPath.length; i++) {
|
||||
const entry = origPath[i];
|
||||
// If the base is known to be non-null, replace with a non-optional load
|
||||
|
||||
@@ -290,6 +290,7 @@ function traverseOptionalBlock(
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
reactive: maybeTest.instructions[0].value.place.reactive,
|
||||
path,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
@@ -391,6 +392,7 @@ function traverseOptionalBlock(
|
||||
);
|
||||
const load = {
|
||||
identifier: baseObject.identifier,
|
||||
reactive: baseObject.reactive,
|
||||
path: [
|
||||
...baseObject.path,
|
||||
{
|
||||
|
||||
@@ -25,8 +25,9 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
|
||||
* evaluate (non-optional) PropertyLoads from.
|
||||
*/
|
||||
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
|
||||
#deps: Map<Identifier, DependencyNode> = new Map();
|
||||
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
|
||||
new Map();
|
||||
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
|
||||
|
||||
/**
|
||||
* @param hoistableObjects a set of paths from which we can safely evaluate
|
||||
@@ -35,9 +36,10 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier} of hoistableObjects) {
|
||||
for (const {path, identifier, reactive} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
);
|
||||
@@ -70,7 +72,8 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
|
||||
static #getOrCreateRoot<T extends string>(
|
||||
identifier: Identifier,
|
||||
roots: Map<Identifier, TreeNode<T>>,
|
||||
reactive: boolean,
|
||||
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
|
||||
defaultAccessType: T,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
@@ -79,9 +82,16 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
if (rootNode === undefined) {
|
||||
rootNode = {
|
||||
properties: new Map(),
|
||||
reactive,
|
||||
accessType: defaultAccessType,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
} else {
|
||||
CompilerError.invariant(reactive === rootNode.reactive, {
|
||||
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
|
||||
description: `Identifier ${printIdentifier(identifier)}`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
@@ -92,9 +102,10 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopeDependency): void {
|
||||
const {identifier, path} = dep;
|
||||
const {identifier, reactive, path} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
reactive,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
@@ -172,7 +183,13 @@ export class ReactiveScopeDependencyTreeHIR {
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
|
||||
collectMinimalDependenciesInSubtree(
|
||||
rootNode,
|
||||
rootNode.reactive,
|
||||
rootId,
|
||||
[],
|
||||
results,
|
||||
);
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -294,25 +311,24 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
|
||||
type DependencyNode = TreeNode<PropertyAccessType>;
|
||||
|
||||
/**
|
||||
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
|
||||
* longer have conditionally accessed nodes, we can simplify
|
||||
*
|
||||
* Recursively calculates minimal dependencies in a subtree.
|
||||
* @param node DependencyNode representing a dependency subtree.
|
||||
* @returns a minimal list of dependencies in this subtree.
|
||||
*/
|
||||
function collectMinimalDependenciesInSubtree(
|
||||
node: DependencyNode,
|
||||
reactive: boolean,
|
||||
rootIdentifier: Identifier,
|
||||
path: Array<DependencyPathEntry>,
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, path});
|
||||
results.add({identifier: rootIdentifier, reactive, path});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
childNode,
|
||||
reactive,
|
||||
rootIdentifier,
|
||||
[
|
||||
...path,
|
||||
|
||||
@@ -9,15 +9,7 @@ import * as t from '@babel/types';
|
||||
import {ZodError, z} from 'zod';
|
||||
import {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
defaultOptions,
|
||||
Logger,
|
||||
PanicThresholdOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
ProgramContext,
|
||||
} from '../Entrypoint';
|
||||
import {Logger, ProgramContext} from '../Entrypoint';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {
|
||||
DEFAULT_GLOBALS,
|
||||
@@ -55,7 +47,7 @@ import {
|
||||
ShapeRegistry,
|
||||
addHook,
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
import {Scope as BabelScope, NodePath} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
@@ -158,7 +150,7 @@ export type Hook = z.infer<typeof HookSchema>;
|
||||
* missing some recursive Object / Function shapeIds
|
||||
*/
|
||||
|
||||
const EnvironmentConfigSchema = z.object({
|
||||
export const EnvironmentConfigSchema = z.object({
|
||||
customHooks: z.map(z.string(), HookSchema).default(new Map()),
|
||||
|
||||
/**
|
||||
@@ -251,6 +243,11 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
@@ -367,6 +364,11 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoImpureFunctionsInRender: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validate against passing mutable functions to hooks
|
||||
*/
|
||||
validateNoFreezingKnownMutableFunctions: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* When enabled, the compiler assumes that hooks follow the Rules of React:
|
||||
* - Hooks may memoize computation based on any of their parameters, thus
|
||||
@@ -635,191 +637,6 @@ const EnvironmentConfigSchema = z.object({
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
|
||||
/**
|
||||
* For test fixtures and playground only.
|
||||
*
|
||||
* Pragmas are straightforward to parse for boolean options (`:true` and
|
||||
* `:false`). These are 'enabled' config values for non-boolean configs (i.e.
|
||||
* what is used when parsing `:true`).
|
||||
*/
|
||||
const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
validateNoCapitalizedCalls: [],
|
||||
enableChangeDetectionForDebugging: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$structuralCheck',
|
||||
},
|
||||
enableEmitFreeze: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'makeReadOnly',
|
||||
},
|
||||
enableEmitInstrumentForget: {
|
||||
fn: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useRenderCounter',
|
||||
},
|
||||
gating: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'shouldInstrument',
|
||||
},
|
||||
globalGating: 'DEV',
|
||||
},
|
||||
enableEmitHookGuards: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$dispatcherGuard',
|
||||
},
|
||||
inlineJsxTransform: {
|
||||
elementSymbol: 'react.transitional.element',
|
||||
globalDevVar: 'DEV',
|
||||
},
|
||||
lowerContextAccess: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useContext_withSelector',
|
||||
},
|
||||
inferEffectDependencies: [
|
||||
{
|
||||
function: {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: any = {};
|
||||
// Get the defaults to programmatically check for boolean properties
|
||||
const defaultConfig = EnvironmentConfigSchema.parse({});
|
||||
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
let [key, val = undefined] = keyVal.split(':');
|
||||
const isSet = val === undefined || val === 'true';
|
||||
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] =
|
||||
testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'customMacros' && val) {
|
||||
const valSplit = val.split('.');
|
||||
if (valSplit.length > 0) {
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
key !== 'enableResetCacheOnSourceFileChanges' &&
|
||||
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
|
||||
) {
|
||||
// skip parsing non-boolean properties
|
||||
continue;
|
||||
}
|
||||
if (val === undefined || val === 'true') {
|
||||
maybeConfig[key] = true;
|
||||
} else {
|
||||
maybeConfig[key] = false;
|
||||
}
|
||||
}
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
if (config.success) {
|
||||
/**
|
||||
* Unless explicitly enabled, do not insert HMR handling code
|
||||
* in test fixtures or playground to reduce visual noise.
|
||||
*/
|
||||
if (config.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
config.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Internal error, could not parse config from pragma string',
|
||||
description: `${fromZodError(config.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
let compilationMode: CompilationMode = defaults.compilationMode;
|
||||
let panicThreshold: PanicThresholdOptions = 'all_errors';
|
||||
let noEmit: boolean = defaultOptions.noEmit;
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
switch (token) {
|
||||
case '@compilationMode(annotation)': {
|
||||
compilationMode = 'annotation';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(infer)': {
|
||||
compilationMode = 'infer';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(all)': {
|
||||
compilationMode = 'all';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(syntax)': {
|
||||
compilationMode = 'syntax';
|
||||
break;
|
||||
}
|
||||
case '@panicThreshold(none)': {
|
||||
panicThreshold = 'none';
|
||||
break;
|
||||
}
|
||||
case '@noEmit': {
|
||||
noEmit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsePluginOptions({
|
||||
environment,
|
||||
compilationMode,
|
||||
panicThreshold,
|
||||
noEmit,
|
||||
});
|
||||
}
|
||||
|
||||
export type PartialEnvironmentConfig = Partial<EnvironmentConfig>;
|
||||
|
||||
export type ReactFunctionType = 'Component' | 'Hook' | 'Other';
|
||||
@@ -863,6 +680,7 @@ export class Environment {
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
parentFunction: NodePath<t.Function>;
|
||||
|
||||
constructor(
|
||||
scope: BabelScope,
|
||||
@@ -870,6 +688,7 @@ export class Environment {
|
||||
compilerMode: CompilerMode,
|
||||
config: EnvironmentConfig,
|
||||
contextIdentifiers: Set<t.Identifier>,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
logger: Logger | null,
|
||||
filename: string | null,
|
||||
code: string | null,
|
||||
@@ -928,6 +747,7 @@ export class Environment {
|
||||
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
|
||||
}
|
||||
|
||||
this.parentFunction = parentFunction;
|
||||
this.#contextIdentifiers = contextIdentifiers;
|
||||
this.#hoistedIdentifiers = new Set();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
BuiltInSetId,
|
||||
BuiltInUseActionStateId,
|
||||
BuiltInUseContextHookId,
|
||||
BuiltInUseEffectEventId,
|
||||
BuiltInUseEffectHookId,
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
@@ -25,6 +26,10 @@ import {
|
||||
BuiltInUseRefId,
|
||||
BuiltInUseStateId,
|
||||
BuiltInUseTransitionId,
|
||||
BuiltInWeakMapId,
|
||||
BuiltInWeakSetId,
|
||||
BuiltinEffectEventId,
|
||||
ReanimatedSharedValueId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
addHook,
|
||||
@@ -491,6 +496,38 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
true,
|
||||
),
|
||||
],
|
||||
[
|
||||
'WeakMap',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [Effect.ConditionallyMutateIterator],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInWeakMapId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
null,
|
||||
true,
|
||||
),
|
||||
],
|
||||
[
|
||||
'WeakSet',
|
||||
addFunction(
|
||||
DEFAULT_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [Effect.ConditionallyMutateIterator],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInWeakSetId},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
null,
|
||||
true,
|
||||
),
|
||||
],
|
||||
// TODO: rest of Global objects
|
||||
];
|
||||
|
||||
@@ -607,6 +644,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: ['@effect'],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@effect',
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@effect',
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
BuiltInUseEffectHookId,
|
||||
),
|
||||
@@ -687,6 +759,27 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInFireId,
|
||||
),
|
||||
],
|
||||
[
|
||||
'useEffectEvent',
|
||||
addHook(
|
||||
DEFAULT_SHAPES,
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {
|
||||
kind: 'Function',
|
||||
return: {kind: 'Poly'},
|
||||
shapeId: BuiltinEffectEventId,
|
||||
isConstructor: false,
|
||||
},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffectEvent',
|
||||
// Frozen because it should not mutate any locally-bound values
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
},
|
||||
BuiltInUseEffectEventId,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
TYPED_GLOBALS.push(
|
||||
@@ -812,6 +905,7 @@ export function installTypeConfig(
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
typeConfig.mutableOnlyIfOperandsAreMutable === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
});
|
||||
}
|
||||
case 'hook': {
|
||||
@@ -829,6 +923,7 @@ export function installTypeConfig(
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
aliasing: typeConfig.aliasing,
|
||||
});
|
||||
}
|
||||
case 'object': {
|
||||
@@ -908,7 +1003,7 @@ export function getReanimatedModuleType(registry: ShapeRegistry): ObjectType {
|
||||
addHook(registry, {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {kind: 'Poly'},
|
||||
returnType: {kind: 'Object', shapeId: ReanimatedSharedValueId},
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
calleeEffect: Effect.Read,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import type {AliasingEffect} from '../Inference/AliasingEffects';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -100,6 +101,7 @@ export type ReactiveInstruction = {
|
||||
id: InstructionId;
|
||||
lvalue: Place | null;
|
||||
value: ReactiveValue;
|
||||
effects?: Array<AliasingEffect> | null; // TODO make non-optional
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -278,12 +280,14 @@ export type HIRFunction = {
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returnType: Type;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
directives: Array<string>;
|
||||
aliasingEffects?: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type FunctionEffect =
|
||||
@@ -449,6 +453,7 @@ export type ReturnTerminal = {
|
||||
value: Place;
|
||||
id: InstructionId;
|
||||
fallthrough?: never;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type GotoTerminal = {
|
||||
@@ -609,6 +614,7 @@ export type MaybeThrowTerminal = {
|
||||
id: InstructionId;
|
||||
loc: SourceLocation;
|
||||
fallthrough?: never;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type ReactiveScopeTerminal = {
|
||||
@@ -645,12 +651,14 @@ export type Instruction = {
|
||||
lvalue: Place;
|
||||
value: InstructionValue;
|
||||
loc: SourceLocation;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type TInstruction<T extends InstructionValue> = {
|
||||
id: InstructionId;
|
||||
lvalue: Place;
|
||||
value: T;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -746,6 +754,27 @@ export enum InstructionKind {
|
||||
Function = 'Function',
|
||||
}
|
||||
|
||||
export function convertHoistedLValueKind(
|
||||
kind: InstructionKind,
|
||||
): InstructionKind | null {
|
||||
switch (kind) {
|
||||
case InstructionKind.HoistedLet:
|
||||
return InstructionKind.Let;
|
||||
case InstructionKind.HoistedConst:
|
||||
return InstructionKind.Const;
|
||||
case InstructionKind.HoistedFunction:
|
||||
return InstructionKind.Function;
|
||||
case InstructionKind.Let:
|
||||
case InstructionKind.Const:
|
||||
case InstructionKind.Function:
|
||||
case InstructionKind.Reassign:
|
||||
case InstructionKind.Catch:
|
||||
return null;
|
||||
default:
|
||||
assertExhaustive(kind, 'Unexpected lvalue kind');
|
||||
}
|
||||
}
|
||||
|
||||
function _staticInvariantInstructionValueHasLocation(
|
||||
value: InstructionValue,
|
||||
): SourceLocation {
|
||||
@@ -880,8 +909,20 @@ export type InstructionValue =
|
||||
| StoreLocal
|
||||
| {
|
||||
kind: 'StoreContext';
|
||||
/**
|
||||
* StoreContext kinds:
|
||||
* Reassign: context variable reassignment in source
|
||||
* Const: const declaration + assignment in source
|
||||
* ('const' context vars are ones whose declarations are hoisted)
|
||||
* Let: let declaration + assignment in source
|
||||
* Function: function declaration in source (similar to `const`)
|
||||
*/
|
||||
lvalue: {
|
||||
kind: InstructionKind.Reassign;
|
||||
kind:
|
||||
| InstructionKind.Reassign
|
||||
| InstructionKind.Const
|
||||
| InstructionKind.Let
|
||||
| InstructionKind.Function;
|
||||
place: Place;
|
||||
};
|
||||
value: Place;
|
||||
@@ -1347,6 +1388,21 @@ export enum ValueReason {
|
||||
*/
|
||||
JsxCaptured = 'jsx-captured',
|
||||
|
||||
/**
|
||||
* Argument to a hook
|
||||
*/
|
||||
HookCaptured = 'hook-captured',
|
||||
|
||||
/**
|
||||
* Return value of a hook
|
||||
*/
|
||||
HookReturn = 'hook-return',
|
||||
|
||||
/**
|
||||
* Passed to an effect
|
||||
*/
|
||||
Effect = 'effect',
|
||||
|
||||
/**
|
||||
* Return value of a function with known frozen return value, e.g. `useState`.
|
||||
*/
|
||||
@@ -1397,6 +1453,20 @@ export const ValueKindSchema = z.enum([
|
||||
ValueKind.Context,
|
||||
]);
|
||||
|
||||
export const ValueReasonSchema = z.enum([
|
||||
ValueReason.Context,
|
||||
ValueReason.Effect,
|
||||
ValueReason.Global,
|
||||
ValueReason.HookCaptured,
|
||||
ValueReason.HookReturn,
|
||||
ValueReason.JsxCaptured,
|
||||
ValueReason.KnownReturnSignature,
|
||||
ValueReason.Other,
|
||||
ValueReason.ReactiveFunctionArgument,
|
||||
ValueReason.ReducerState,
|
||||
ValueReason.State,
|
||||
]);
|
||||
|
||||
// The effect with which a value is modified.
|
||||
export enum Effect {
|
||||
// Default value: not allowed after lifetime inference
|
||||
@@ -1535,6 +1605,18 @@ export type DependencyPathEntry = {
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
/**
|
||||
* Reflects whether the base identifier is reactive. Note that some reactive
|
||||
* objects may have non-reactive properties, but we do not currently track
|
||||
* this.
|
||||
*
|
||||
* ```js
|
||||
* // Technically, result[0] is reactive and result[1] is not.
|
||||
* // Currently, both dependencies would be marked as reactive.
|
||||
* const result = useState();
|
||||
* ```
|
||||
*/
|
||||
reactive: boolean;
|
||||
path: DependencyPath;
|
||||
};
|
||||
|
||||
@@ -1692,6 +1774,18 @@ export function isRefOrRefValue(id: Identifier): boolean {
|
||||
return isUseRefType(id) || isRefValueType(id);
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if the type is a Ref or a custom user type that acts like a ref when it
|
||||
* shouldn't. For now the only other case of this is Reanimated's shared values.
|
||||
*/
|
||||
export function isRefOrRefLikeMutableType(type: Type): boolean {
|
||||
return (
|
||||
type.kind === 'Object' &&
|
||||
(type.shapeId === 'BuiltInUseRefId' ||
|
||||
type.shapeId == 'ReanimatedSharedValueId')
|
||||
);
|
||||
}
|
||||
|
||||
export function isSetStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
|
||||
}
|
||||
@@ -1728,6 +1822,13 @@ export function isFireFunctionType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isEffectEventFunctionType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' &&
|
||||
id.type.shapeId === 'BuiltInEffectEventFunction'
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableType(id: Identifier): boolean {
|
||||
return (
|
||||
isSetStateType(id) ||
|
||||
@@ -1738,6 +1839,40 @@ export function isStableType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isStableTypeContainer(id: Identifier): boolean {
|
||||
const type_ = id.type;
|
||||
if (type_.kind !== 'Object') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isUseStateType(id) || // setState
|
||||
type_.shapeId === 'BuiltInUseActionState' || // setActionState
|
||||
isUseReducerType(id) || // dispatcher
|
||||
type_.shapeId === 'BuiltInUseTransition' // startTransition
|
||||
);
|
||||
}
|
||||
|
||||
export function evaluatesToStableTypeOrContainer(
|
||||
env: Environment,
|
||||
{value}: Instruction,
|
||||
): boolean {
|
||||
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
|
||||
const callee =
|
||||
value.kind === 'CallExpression' ? value.callee : value.property;
|
||||
|
||||
const calleeHookKind = getHookKind(env, callee.identifier);
|
||||
switch (calleeHookKind) {
|
||||
case 'useState':
|
||||
case 'useReducer':
|
||||
case 'useActionState':
|
||||
case 'useRef':
|
||||
case 'useTransition':
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isUseEffectHookType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'
|
||||
|
||||
@@ -106,11 +106,10 @@ export default class HIRBuilder {
|
||||
#current: WipBlock;
|
||||
#entry: BlockId;
|
||||
#scopes: Array<Scope> = [];
|
||||
#context: Array<t.Identifier>;
|
||||
#context: Map<t.Identifier, SourceLocation>;
|
||||
#bindings: Bindings;
|
||||
#env: Environment;
|
||||
#exceptionHandlerStack: Array<BlockId> = [];
|
||||
parentFunction: NodePath<t.Function>;
|
||||
errors: CompilerError = new CompilerError();
|
||||
/**
|
||||
* Traversal context: counts the number of `fbt` tag parents
|
||||
@@ -122,7 +121,7 @@ export default class HIRBuilder {
|
||||
return this.#env.nextIdentifierId;
|
||||
}
|
||||
|
||||
get context(): Array<t.Identifier> {
|
||||
get context(): Map<t.Identifier, SourceLocation> {
|
||||
return this.#context;
|
||||
}
|
||||
|
||||
@@ -136,16 +135,17 @@ export default class HIRBuilder {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
parentFunction: NodePath<t.Function>, // the outermost function being compiled
|
||||
bindings: Bindings | null = null,
|
||||
context: Array<t.Identifier> | null = null,
|
||||
options?: {
|
||||
bindings?: Bindings | null;
|
||||
context?: Map<t.Identifier, SourceLocation>;
|
||||
entryBlockKind?: BlockKind;
|
||||
},
|
||||
) {
|
||||
this.#env = env;
|
||||
this.#bindings = bindings ?? new Map();
|
||||
this.parentFunction = parentFunction;
|
||||
this.#context = context ?? [];
|
||||
this.#bindings = options?.bindings ?? new Map();
|
||||
this.#context = options?.context ?? new Map();
|
||||
this.#entry = makeBlockId(env.nextBlockId);
|
||||
this.#current = newBlock(this.#entry, 'block');
|
||||
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
|
||||
}
|
||||
|
||||
currentBlockKind(): BlockKind {
|
||||
@@ -165,6 +165,7 @@ export default class HIRBuilder {
|
||||
handler: exceptionHandler,
|
||||
id: makeInstructionId(0),
|
||||
loc: instruction.loc,
|
||||
effects: null,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
@@ -239,7 +240,7 @@ export default class HIRBuilder {
|
||||
|
||||
// Check if the binding is from module scope
|
||||
const outerBinding =
|
||||
this.parentFunction.scope.parent.getBinding(originalName);
|
||||
this.#env.parentFunction.scope.parent.getBinding(originalName);
|
||||
if (babelBinding === outerBinding) {
|
||||
const path = babelBinding.path;
|
||||
if (path.isImportDefaultSpecifier()) {
|
||||
@@ -293,7 +294,7 @@ export default class HIRBuilder {
|
||||
const binding = this.#resolveBabelBinding(path);
|
||||
if (binding) {
|
||||
// Check if the binding is from module scope, if so return null
|
||||
const outerBinding = this.parentFunction.scope.parent.getBinding(
|
||||
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
|
||||
path.node.name,
|
||||
);
|
||||
if (binding === outerBinding) {
|
||||
@@ -376,7 +377,7 @@ export default class HIRBuilder {
|
||||
}
|
||||
|
||||
// Terminate the current block w the given terminal, and start a new block
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
|
||||
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
|
||||
const {id: blockId, kind, instructions} = this.#current;
|
||||
this.#completed.set(blockId, {
|
||||
kind,
|
||||
@@ -390,6 +391,7 @@ export default class HIRBuilder {
|
||||
const nextId = this.#env.nextBlockId;
|
||||
this.#current = newBlock(nextId, nextBlockKind);
|
||||
}
|
||||
return blockId;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -746,6 +748,11 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
|
||||
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
|
||||
*/
|
||||
const block = func.blocks.get(blockId)!;
|
||||
CompilerError.invariant(block != null, {
|
||||
reason: '[HIRBuilder] Unexpected null block',
|
||||
description: `expected block ${blockId} to exist`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
|
||||
const fallthrough = terminalFallthrough(block.terminal);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Instruction,
|
||||
Place,
|
||||
} from './HIR';
|
||||
import {markPredecessors} from './HIRBuilder';
|
||||
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
|
||||
@@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
suggestions: null,
|
||||
});
|
||||
const operand = Array.from(phi.operands.values())[0]!;
|
||||
const lvalue: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: phi.place.identifier,
|
||||
effect: Effect.ConditionallyMutate,
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const instr: Instruction = {
|
||||
id: predecessor.terminal.id,
|
||||
lvalue: {
|
||||
kind: 'Identifier',
|
||||
identifier: phi.place.identifier,
|
||||
effect: Effect.ConditionallyMutate,
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: {...lvalue},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {...operand},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
predecessor.instructions.push(instr);
|
||||
|
||||
@@ -6,14 +6,30 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
Hole,
|
||||
makeDeclarationId,
|
||||
makeIdentifierId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
SourceLocation,
|
||||
SpreadPattern,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
import {
|
||||
BuiltInType,
|
||||
FunctionType,
|
||||
makeType,
|
||||
ObjectType,
|
||||
PolyType,
|
||||
PrimitiveType,
|
||||
} from './Types';
|
||||
import {AliasingEffectConfig, AliasingSignatureConfig} from './TypeSchema';
|
||||
|
||||
/*
|
||||
* This file exports types and defaults for JavaScript object shapes. These are
|
||||
@@ -42,13 +58,20 @@ function createAnonId(): string {
|
||||
export function addFunction(
|
||||
registry: ShapeRegistry,
|
||||
properties: Iterable<[string, BuiltInType | PolyType]>,
|
||||
fn: Omit<FunctionSignature, 'hookKind'>,
|
||||
fn: Omit<FunctionSignature, 'hookKind' | 'aliasing'> & {
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
},
|
||||
id: string | null = null,
|
||||
isConstructor: boolean = false,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
const aliasing =
|
||||
fn.aliasing != null
|
||||
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
|
||||
: null;
|
||||
addShape(registry, shapeId, properties, {
|
||||
...fn,
|
||||
aliasing,
|
||||
hookKind: null,
|
||||
});
|
||||
return {
|
||||
@@ -66,11 +89,18 @@ export function addFunction(
|
||||
*/
|
||||
export function addHook(
|
||||
registry: ShapeRegistry,
|
||||
fn: FunctionSignature & {hookKind: HookKind},
|
||||
fn: Omit<FunctionSignature, 'aliasing'> & {
|
||||
hookKind: HookKind;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
},
|
||||
id: string | null = null,
|
||||
): FunctionType {
|
||||
const shapeId = id ?? createAnonId();
|
||||
addShape(registry, shapeId, [], fn);
|
||||
const aliasing =
|
||||
fn.aliasing != null
|
||||
? parseAliasingSignatureConfig(fn.aliasing, '<builtin>', GeneratedSource)
|
||||
: null;
|
||||
addShape(registry, shapeId, [], {...fn, aliasing});
|
||||
return {
|
||||
kind: 'Function',
|
||||
return: fn.returnType,
|
||||
@@ -79,6 +109,129 @@ export function addHook(
|
||||
};
|
||||
}
|
||||
|
||||
function parseAliasingSignatureConfig(
|
||||
typeConfig: AliasingSignatureConfig,
|
||||
moduleName: string,
|
||||
loc: SourceLocation,
|
||||
): AliasingSignature {
|
||||
const lifetimes = new Map<string, Place>();
|
||||
function define(temp: string): Place {
|
||||
CompilerError.invariant(!lifetimes.has(temp), {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
const place = signatureArgument(lifetimes.size);
|
||||
lifetimes.set(temp, place);
|
||||
return place;
|
||||
}
|
||||
function lookup(temp: string): Place {
|
||||
const place = lifetimes.get(temp);
|
||||
CompilerError.invariant(place != null, {
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
|
||||
loc,
|
||||
});
|
||||
return place;
|
||||
}
|
||||
const receiver = define(typeConfig.receiver);
|
||||
const params = typeConfig.params.map(define);
|
||||
const rest = typeConfig.rest != null ? define(typeConfig.rest) : null;
|
||||
const returns = define(typeConfig.returns);
|
||||
const temporaries = typeConfig.temporaries.map(define);
|
||||
const effects = typeConfig.effects.map(
|
||||
(effect: AliasingEffectConfig): AliasingEffect => {
|
||||
switch (effect.kind) {
|
||||
case 'CreateFrom':
|
||||
case 'Capture':
|
||||
case 'Alias':
|
||||
case 'Assign': {
|
||||
const from = lookup(effect.from);
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: effect.kind,
|
||||
from,
|
||||
into,
|
||||
};
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
const value = lookup(effect.value);
|
||||
return {kind: effect.kind, value};
|
||||
}
|
||||
case 'Create': {
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Create',
|
||||
into,
|
||||
reason: effect.reason,
|
||||
value: effect.value,
|
||||
};
|
||||
}
|
||||
case 'Freeze': {
|
||||
const value = lookup(effect.value);
|
||||
return {
|
||||
kind: 'Freeze',
|
||||
value,
|
||||
reason: effect.reason,
|
||||
};
|
||||
}
|
||||
case 'Impure': {
|
||||
const place = lookup(effect.place);
|
||||
return {
|
||||
kind: 'Impure',
|
||||
place,
|
||||
error: CompilerError.throwTodo({
|
||||
reason: 'Support impure effect declarations',
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'Apply': {
|
||||
const receiver = lookup(effect.receiver);
|
||||
const fn = lookup(effect.function);
|
||||
const args: Array<Place | SpreadPattern | Hole> = effect.args.map(
|
||||
arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return lookup(arg);
|
||||
} else if (arg.kind === 'Spread') {
|
||||
return {kind: 'Spread', place: lookup(arg.place)};
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
},
|
||||
);
|
||||
const into = lookup(effect.into);
|
||||
return {
|
||||
kind: 'Apply',
|
||||
receiver,
|
||||
function: fn,
|
||||
mutatesFunction: effect.mutatesFunction,
|
||||
args,
|
||||
into,
|
||||
loc,
|
||||
signature: null,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind '${(effect as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
receiver: receiver.identifier.id,
|
||||
params: params.map(p => p.identifier.id),
|
||||
rest: rest != null ? rest.identifier.id : null,
|
||||
returns: returns.identifier.id,
|
||||
temporaries,
|
||||
effects,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an object to an existing ShapeRegistry.
|
||||
*
|
||||
@@ -131,6 +284,7 @@ export type HookKind =
|
||||
| 'useCallback'
|
||||
| 'useTransition'
|
||||
| 'useImperativeHandle'
|
||||
| 'useEffectEvent'
|
||||
| 'Custom';
|
||||
|
||||
/*
|
||||
@@ -179,6 +333,8 @@ export type FunctionSignature = {
|
||||
impure?: boolean;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature | null | undefined;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -203,6 +359,8 @@ export const BuiltInPropsId = 'BuiltInProps';
|
||||
export const BuiltInArrayId = 'BuiltInArray';
|
||||
export const BuiltInSetId = 'BuiltInSet';
|
||||
export const BuiltInMapId = 'BuiltInMap';
|
||||
export const BuiltInWeakSetId = 'BuiltInWeakSet';
|
||||
export const BuiltInWeakMapId = 'BuiltInWeakMap';
|
||||
export const BuiltInFunctionId = 'BuiltInFunction';
|
||||
export const BuiltInJsxId = 'BuiltInJsx';
|
||||
export const BuiltInObjectId = 'BuiltInObject';
|
||||
@@ -224,6 +382,11 @@ export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
export const BuiltInFireId = 'BuiltInFire';
|
||||
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
|
||||
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
|
||||
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
|
||||
|
||||
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
|
||||
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
|
||||
|
||||
// ShapeRegistry with default definitions for built-ins.
|
||||
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
|
||||
@@ -297,6 +460,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Push directly mutates the array itself
|
||||
{kind: 'Mutate', value: '@receiver'},
|
||||
// The arguments are captured into the array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@receiver',
|
||||
},
|
||||
// Returns the new length, a primitive
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -327,6 +514,60 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: ['@callback'],
|
||||
rest: null,
|
||||
returns: '@returns',
|
||||
temporaries: [
|
||||
// Temporary representing captured items of the receiver
|
||||
'@item',
|
||||
// Temporary representing the result of the callback
|
||||
'@callbackReturn',
|
||||
/*
|
||||
* Undefined `this` arg to the callback. Note the signature does not
|
||||
* support passing an explicit thisArg second param
|
||||
*/
|
||||
'@thisArg',
|
||||
],
|
||||
effects: [
|
||||
// Map creates a new mutable array
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The first arg to the callback is an item extracted from the receiver array
|
||||
{
|
||||
kind: 'CreateFrom',
|
||||
from: '@receiver',
|
||||
into: '@item',
|
||||
},
|
||||
// The undefined this for the callback
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@thisArg',
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// calls the callback, returning the result into a temporary
|
||||
{
|
||||
kind: 'Apply',
|
||||
receiver: '@thisArg',
|
||||
args: ['@item', {kind: 'Hole'}, '@receiver'],
|
||||
function: '@callback',
|
||||
into: '@callbackReturn',
|
||||
mutatesFunction: false,
|
||||
},
|
||||
// captures the result of the callback into the return array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@callbackReturn',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -474,6 +715,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
|
||||
calleeEffect: Effect.Store,
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Set.add returns the receiver Set
|
||||
{
|
||||
kind: 'Assign',
|
||||
from: '@receiver',
|
||||
into: '@returns',
|
||||
},
|
||||
// Set.add mutates the set itself
|
||||
{
|
||||
kind: 'Mutate',
|
||||
value: '@receiver',
|
||||
},
|
||||
// Captures the rest params into the set
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: '@rest',
|
||||
into: '@receiver',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -764,6 +1031,101 @@ addObject(BUILTIN_SHAPES, BuiltInMapId, [
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInWeakSetId, [
|
||||
[
|
||||
/**
|
||||
* 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: BuiltInWeakSetId},
|
||||
calleeEffect: Effect.Store,
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
/**
|
||||
* 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,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInWeakMapId, [
|
||||
[
|
||||
'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: BuiltInWeakMapId},
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
|
||||
['0', {kind: 'Poly'}],
|
||||
[
|
||||
@@ -848,6 +1210,19 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
|
||||
['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
|
||||
]);
|
||||
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
},
|
||||
BuiltinEffectEventId,
|
||||
);
|
||||
|
||||
/**
|
||||
* MixedReadOnly =
|
||||
* | primitive
|
||||
@@ -1066,6 +1441,53 @@ export const DefaultNonmutatingHook = addHook(
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'Custom',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: '@receiver',
|
||||
params: [],
|
||||
rest: '@rest',
|
||||
returns: '@returns',
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Freeze the arguments
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: '@rest',
|
||||
reason: ValueReason.HookCaptured,
|
||||
},
|
||||
// Returns a frozen value
|
||||
{
|
||||
kind: 'Create',
|
||||
into: '@returns',
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.HookReturn,
|
||||
},
|
||||
// May alias any arguments into the return
|
||||
{
|
||||
kind: 'Alias',
|
||||
from: '@rest',
|
||||
into: '@returns',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'DefaultNonmutatingHook',
|
||||
);
|
||||
|
||||
export function signatureArgument(id: number): Place {
|
||||
const place: Place = {
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Unknown,
|
||||
loc: GeneratedSource,
|
||||
reactive: false,
|
||||
identifier: {
|
||||
declarationId: makeDeclarationId(id),
|
||||
id: makeIdentifierId(id),
|
||||
loc: GeneratedSource,
|
||||
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
},
|
||||
};
|
||||
return place;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
Type,
|
||||
} from './HIR';
|
||||
import {GotoVariant, InstructionKind} from './HIR';
|
||||
import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
|
||||
|
||||
export type Options = {
|
||||
indent: number;
|
||||
@@ -67,13 +68,15 @@ export function printFunction(fn: HIRFunction): string {
|
||||
})
|
||||
.join(', ') +
|
||||
')';
|
||||
} else {
|
||||
definition += '()';
|
||||
}
|
||||
if (definition.length !== 0) {
|
||||
output.push(definition);
|
||||
}
|
||||
output.push(printType(fn.returnType));
|
||||
output.push(printHIR(fn.body));
|
||||
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
|
||||
output.push(...fn.directives);
|
||||
output.push(printHIR(fn.body));
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
@@ -151,7 +154,10 @@ export function printMixedHIR(
|
||||
|
||||
export function printInstruction(instr: ReactiveInstruction): string {
|
||||
const id = `[${instr.id}]`;
|
||||
const value = printInstructionValue(instr.value);
|
||||
let value = printInstructionValue(instr.value);
|
||||
if (instr.effects != null) {
|
||||
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
|
||||
if (instr.lvalue !== null) {
|
||||
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
|
||||
@@ -213,6 +219,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
value = `[${terminal.id}] Return${
|
||||
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
|
||||
}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'goto': {
|
||||
@@ -281,6 +290,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope': {
|
||||
@@ -555,8 +567,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const type = printType(instrValue.loweredFunc.func.returnType).trim();
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
|
||||
const aliasingEffects =
|
||||
instrValue.loweredFunc.func.aliasingEffects
|
||||
?.map(printAliasingEffect)
|
||||
?.join(', ') ?? '';
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -922,3 +937,107 @@ function getFunctionName(
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Assign': {
|
||||
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Alias': {
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Capture': {
|
||||
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Create': {
|
||||
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
|
||||
}
|
||||
case 'CreateFrom': {
|
||||
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
|
||||
}
|
||||
case 'Apply': {
|
||||
const receiverCallee =
|
||||
effect.receiver.identifier.id === effect.function.identifier.id
|
||||
? printPlaceForAliasEffect(effect.receiver)
|
||||
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
|
||||
const args = effect.args
|
||||
.map(arg => {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return printPlaceForAliasEffect(arg);
|
||||
} else if (arg.kind === 'Hole') {
|
||||
return ' ';
|
||||
}
|
||||
return `...${printPlaceForAliasEffect(arg.place)}`;
|
||||
})
|
||||
.join(', ');
|
||||
let signature = '';
|
||||
if (effect.signature != null) {
|
||||
if (effect.signature.aliasing != null) {
|
||||
signature = printAliasingSignature(effect.signature.aliasing);
|
||||
} else {
|
||||
signature = JSON.stringify(effect.signature, null, 2);
|
||||
}
|
||||
}
|
||||
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
|
||||
}
|
||||
case 'Freeze': {
|
||||
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printPlaceForAliasEffect(place: Place): string {
|
||||
return printIdentifier(place.identifier);
|
||||
}
|
||||
|
||||
export function printAliasingSignature(signature: AliasingSignature): string {
|
||||
const tokens: Array<string> = ['function '];
|
||||
if (signature.temporaries.length !== 0) {
|
||||
tokens.push('<');
|
||||
tokens.push(
|
||||
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
|
||||
);
|
||||
tokens.push('>');
|
||||
}
|
||||
tokens.push('(');
|
||||
tokens.push('this=$' + String(signature.receiver));
|
||||
for (const param of signature.params) {
|
||||
tokens.push(', $' + String(param));
|
||||
}
|
||||
if (signature.rest != null) {
|
||||
tokens.push(`, ...$${String(signature.rest)}`);
|
||||
}
|
||||
tokens.push('): ');
|
||||
tokens.push('$' + String(signature.returns) + ':');
|
||||
for (const effect of signature.effects) {
|
||||
tokens.push('\n ' + printAliasingEffect(effect));
|
||||
}
|
||||
return tokens.join('');
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
FunctionExpression,
|
||||
ObjectMethod,
|
||||
PropertyLiteral,
|
||||
convertHoistedLValueKind,
|
||||
} from './HIR';
|
||||
import {
|
||||
collectHoistablePropertyLoads,
|
||||
@@ -116,7 +117,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function findTemporariesUsedOutsideDeclaringScope(
|
||||
export function findTemporariesUsedOutsideDeclaringScope(
|
||||
fn: HIRFunction,
|
||||
): ReadonlySet<DeclarationId> {
|
||||
/*
|
||||
@@ -246,12 +247,18 @@ function isLoadContextMutable(
|
||||
id: InstructionId,
|
||||
): instrValue is LoadContext {
|
||||
if (instrValue.kind === 'LoadContext') {
|
||||
CompilerError.invariant(instrValue.place.identifier.scope != null, {
|
||||
reason:
|
||||
'[PropagateScopeDependencies] Expected all context variables to be assigned a scope',
|
||||
loc: instrValue.loc,
|
||||
});
|
||||
return id >= instrValue.place.identifier.scope.range.end;
|
||||
/**
|
||||
* Not all context variables currently have scopes due to limitations of
|
||||
* mutability analysis for function expressions.
|
||||
*
|
||||
* Currently, many function expressions references are inferred to be
|
||||
* 'Read' | 'Freeze' effects which don't replay mutable effects of captured
|
||||
* context.
|
||||
*/
|
||||
return (
|
||||
instrValue.place.identifier.scope != null &&
|
||||
id >= instrValue.place.identifier.scope.range.end
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -309,6 +316,7 @@ function collectTemporariesSidemapImpl(
|
||||
) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
reactive: value.place.reactive,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
@@ -362,11 +370,13 @@ function getProperty(
|
||||
if (resolvedDependency == null) {
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
reactive: object.reactive,
|
||||
path: [{property: propertyName, optional}],
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
reactive: resolvedDependency.reactive,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
};
|
||||
}
|
||||
@@ -378,7 +388,7 @@ type Decl = {
|
||||
scope: Stack<ReactiveScope>;
|
||||
};
|
||||
|
||||
class Context {
|
||||
export class DependencyCollectionContext {
|
||||
#declarations: Map<DeclarationId, Decl> = new Map();
|
||||
#reassignments: Map<Identifier, Decl> = new Map();
|
||||
|
||||
@@ -471,6 +481,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 {
|
||||
@@ -522,6 +535,7 @@ class Context {
|
||||
this.visitDependency(
|
||||
this.#temporaries.get(place.identifier.id) ?? {
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
},
|
||||
);
|
||||
@@ -586,6 +600,7 @@ class Context {
|
||||
) {
|
||||
maybeDependency = {
|
||||
identifier: maybeDependency.identifier,
|
||||
reactive: maybeDependency.reactive,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
@@ -607,7 +622,11 @@ class Context {
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
) &&
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
this.#checkValidDependency({
|
||||
identifier: place.identifier,
|
||||
reactive: place.reactive,
|
||||
path: [],
|
||||
})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
@@ -645,7 +664,10 @@ enum HIRValue {
|
||||
Terminal,
|
||||
}
|
||||
|
||||
function handleInstruction(instr: Instruction, context: Context): void {
|
||||
export function handleInstruction(
|
||||
instr: Instruction,
|
||||
context: DependencyCollectionContext,
|
||||
): void {
|
||||
const {id, value, lvalue} = instr;
|
||||
context.declare(lvalue.identifier, {
|
||||
id,
|
||||
@@ -669,21 +691,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)) {
|
||||
@@ -695,6 +717,26 @@ 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) ||
|
||||
value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
@@ -708,7 +750,7 @@ function collectDependencies(
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
|
||||
const context = new Context(
|
||||
const context = new DependencyCollectionContext(
|
||||
usedOutsideDeclaringScope,
|
||||
temporaries,
|
||||
processedInstrsInOptional,
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
Place,
|
||||
ReactiveScopeDependency,
|
||||
Identifier,
|
||||
makeInstructionId,
|
||||
InstructionKind,
|
||||
GeneratedSource,
|
||||
BlockId,
|
||||
makeTemporaryIdentifier,
|
||||
Effect,
|
||||
GotoVariant,
|
||||
HIR,
|
||||
} from './HIR';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Environment} from './Environment';
|
||||
import HIRBuilder from './HIRBuilder';
|
||||
import {lowerValueToTemporary} from './BuildHIR';
|
||||
|
||||
type DependencyInstructions = {
|
||||
place: Place;
|
||||
value: HIR;
|
||||
exitBlockId: BlockId;
|
||||
};
|
||||
|
||||
export function buildDependencyInstructions(
|
||||
dep: ReactiveScopeDependency,
|
||||
env: Environment,
|
||||
): DependencyInstructions {
|
||||
const builder = new HIRBuilder(env, {
|
||||
entryBlockKind: 'value',
|
||||
});
|
||||
let dependencyValue: Identifier;
|
||||
if (dep.path.every(path => !path.optional)) {
|
||||
dependencyValue = writeNonOptionalDependency(dep, env, builder);
|
||||
} else {
|
||||
dependencyValue = writeOptionalDependency(dep, builder, null);
|
||||
}
|
||||
|
||||
const exitBlockId = builder.terminate(
|
||||
{
|
||||
kind: 'unsupported',
|
||||
loc: GeneratedSource,
|
||||
id: makeInstructionId(0),
|
||||
},
|
||||
null,
|
||||
);
|
||||
return {
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dependencyValue,
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
value: builder.build(),
|
||||
exitBlockId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write instructions for a simple dependency (without optional chains)
|
||||
*/
|
||||
function writeNonOptionalDependency(
|
||||
dep: ReactiveScopeDependency,
|
||||
env: Environment,
|
||||
builder: HIRBuilder,
|
||||
): Identifier {
|
||||
const loc = dep.identifier.loc;
|
||||
let curr: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: curr,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
identifier: dep.identifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
effects: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Iteratively build up dependency instructions by reading from the last written
|
||||
* instruction.
|
||||
*/
|
||||
for (const path of dep.path) {
|
||||
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
|
||||
builder.push({
|
||||
lvalue: {
|
||||
identifier: next,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: curr,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc,
|
||||
},
|
||||
property: path.property,
|
||||
loc,
|
||||
},
|
||||
id: makeInstructionId(1),
|
||||
loc: loc,
|
||||
effects: null,
|
||||
});
|
||||
curr = next;
|
||||
}
|
||||
return curr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a dependency into optional blocks.
|
||||
*
|
||||
* e.g. `a.b?.c.d` is written to an optional block that tests `a.b` and
|
||||
* conditionally evaluates `c.d`.
|
||||
*/
|
||||
function writeOptionalDependency(
|
||||
dep: ReactiveScopeDependency,
|
||||
builder: HIRBuilder,
|
||||
parentAlternate: BlockId | null,
|
||||
): Identifier {
|
||||
const env = builder.environment;
|
||||
/**
|
||||
* Reserve an identifier which will be used to store the result of this
|
||||
* dependency.
|
||||
*/
|
||||
const dependencyValue: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
|
||||
effect: Effect.Mutate,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reserve a block which is the fallthrough (and transitive successor) of this
|
||||
* optional chain.
|
||||
*/
|
||||
const continuationBlock = builder.reserve(builder.currentBlockKind());
|
||||
let alternate;
|
||||
if (parentAlternate != null) {
|
||||
alternate = parentAlternate;
|
||||
} else {
|
||||
/**
|
||||
* If an outermost alternate block has not been reserved, write one
|
||||
*
|
||||
* $N = Primitive undefined
|
||||
* $M = StoreLocal $OptionalResult = $N
|
||||
* goto fallthrough
|
||||
*/
|
||||
alternate = builder.enter('value', () => {
|
||||
const temp = lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
value: undefined,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
|
||||
value: {...temp},
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Reserve the consequent block, which is the successor of the test block
|
||||
const consequent = builder.reserve('value');
|
||||
|
||||
let testIdentifier: Identifier | null = null;
|
||||
const testBlock = builder.enter('value', () => {
|
||||
const testDependency = {
|
||||
...dep,
|
||||
path: dep.path.slice(0, dep.path.length - 1),
|
||||
};
|
||||
const firstOptional = dep.path.findIndex(path => path.optional);
|
||||
CompilerError.invariant(firstOptional !== -1, {
|
||||
reason:
|
||||
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
|
||||
loc: dep.identifier.loc,
|
||||
description: null,
|
||||
suggestions: null,
|
||||
});
|
||||
if (firstOptional === dep.path.length - 1) {
|
||||
// Base case: the test block is simple
|
||||
testIdentifier = writeNonOptionalDependency(testDependency, env, builder);
|
||||
} else {
|
||||
// Otherwise, the test block is a nested optional chain
|
||||
testIdentifier = writeOptionalDependency(
|
||||
testDependency,
|
||||
builder,
|
||||
alternate,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'branch',
|
||||
test: {
|
||||
identifier: testIdentifier,
|
||||
effect: Effect.Freeze,
|
||||
kind: 'Identifier',
|
||||
loc: GeneratedSource,
|
||||
reactive: dep.reactive,
|
||||
},
|
||||
consequent: consequent.id,
|
||||
alternate,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
fallthrough: continuationBlock.id,
|
||||
};
|
||||
});
|
||||
|
||||
builder.enterReserved(consequent, () => {
|
||||
CompilerError.invariant(testIdentifier !== null, {
|
||||
reason: 'Satisfy type checker',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Const, place: {...dependencyValue}},
|
||||
value: lowerValueToTemporary(builder, {
|
||||
kind: 'PropertyLoad',
|
||||
object: {
|
||||
identifier: testIdentifier,
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Freeze,
|
||||
reactive: dep.reactive,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
property: dep.path.at(-1)!.property,
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
type: null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return {
|
||||
kind: 'goto',
|
||||
variant: GotoVariant.Break,
|
||||
block: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
{
|
||||
kind: 'optional',
|
||||
optional: dep.path.at(-1)!.optional,
|
||||
test: testBlock,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
|
||||
return dependencyValue.identifier;
|
||||
}
|
||||
@@ -8,7 +8,12 @@
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {EffectSchema, ValueKindSchema} from './HIR';
|
||||
import {
|
||||
EffectSchema,
|
||||
ValueKindSchema,
|
||||
ValueReason,
|
||||
ValueReasonSchema,
|
||||
} from './HIR';
|
||||
|
||||
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
|
||||
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
|
||||
@@ -31,6 +36,194 @@ export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
|
||||
properties: ObjectPropertiesSchema.nullable(),
|
||||
});
|
||||
|
||||
export const LifetimeIdSchema = z.string().refine(id => id.startsWith('@'), {
|
||||
message: "Placeholder names must start with '@'",
|
||||
});
|
||||
|
||||
export type FreezeEffectConfig = {
|
||||
kind: 'Freeze';
|
||||
value: string;
|
||||
reason: ValueReason;
|
||||
};
|
||||
|
||||
export const FreezeEffectSchema: z.ZodType<FreezeEffectConfig> = z.object({
|
||||
kind: z.literal('Freeze'),
|
||||
value: LifetimeIdSchema,
|
||||
reason: ValueReasonSchema,
|
||||
});
|
||||
|
||||
export type MutateEffectConfig = {
|
||||
kind: 'Mutate';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MutateEffectSchema: z.ZodType<MutateEffectConfig> = z.object({
|
||||
kind: z.literal('Mutate'),
|
||||
value: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type MutateTransitiveConditionallyConfig = {
|
||||
kind: 'MutateTransitiveConditionally';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const MutateTransitiveConditionallySchema: z.ZodType<MutateTransitiveConditionallyConfig> =
|
||||
z.object({
|
||||
kind: z.literal('MutateTransitiveConditionally'),
|
||||
value: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CreateEffectConfig = {
|
||||
kind: 'Create';
|
||||
into: string;
|
||||
value: ValueKind;
|
||||
reason: ValueReason;
|
||||
};
|
||||
|
||||
export const CreateEffectSchema: z.ZodType<CreateEffectConfig> = z.object({
|
||||
kind: z.literal('Create'),
|
||||
into: LifetimeIdSchema,
|
||||
value: ValueKindSchema,
|
||||
reason: ValueReasonSchema,
|
||||
});
|
||||
|
||||
export type AssignEffectConfig = {
|
||||
kind: 'Assign';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const AssignEffectSchema: z.ZodType<AssignEffectConfig> = z.object({
|
||||
kind: z.literal('Assign'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type AliasEffectConfig = {
|
||||
kind: 'Alias';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
|
||||
kind: z.literal('Alias'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CaptureEffectConfig = {
|
||||
kind: 'Capture';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const CaptureEffectSchema: z.ZodType<CaptureEffectConfig> = z.object({
|
||||
kind: z.literal('Capture'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type CreateFromEffectConfig = {
|
||||
kind: 'CreateFrom';
|
||||
from: string;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const CreateFromEffectSchema: z.ZodType<CreateFromEffectConfig> =
|
||||
z.object({
|
||||
kind: z.literal('CreateFrom'),
|
||||
from: LifetimeIdSchema,
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ApplyArgConfig =
|
||||
| string
|
||||
| {kind: 'Spread'; place: string}
|
||||
| {kind: 'Hole'};
|
||||
|
||||
export const ApplyArgSchema: z.ZodType<ApplyArgConfig> = z.union([
|
||||
LifetimeIdSchema,
|
||||
z.object({
|
||||
kind: z.literal('Spread'),
|
||||
place: LifetimeIdSchema,
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('Hole'),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type ApplyEffectConfig = {
|
||||
kind: 'Apply';
|
||||
receiver: string;
|
||||
function: string;
|
||||
mutatesFunction: boolean;
|
||||
args: Array<ApplyArgConfig>;
|
||||
into: string;
|
||||
};
|
||||
|
||||
export const ApplyEffectSchema: z.ZodType<ApplyEffectConfig> = z.object({
|
||||
kind: z.literal('Apply'),
|
||||
receiver: LifetimeIdSchema,
|
||||
function: LifetimeIdSchema,
|
||||
mutatesFunction: z.boolean(),
|
||||
args: z.array(ApplyArgSchema),
|
||||
into: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type ImpureEffectConfig = {
|
||||
kind: 'Impure';
|
||||
place: string;
|
||||
};
|
||||
|
||||
export const ImpureEffectSchema: z.ZodType<ImpureEffectConfig> = z.object({
|
||||
kind: z.literal('Impure'),
|
||||
place: LifetimeIdSchema,
|
||||
});
|
||||
|
||||
export type AliasingEffectConfig =
|
||||
| FreezeEffectConfig
|
||||
| CreateEffectConfig
|
||||
| CreateFromEffectConfig
|
||||
| AssignEffectConfig
|
||||
| AliasEffectConfig
|
||||
| CaptureEffectConfig
|
||||
| ImpureEffectConfig
|
||||
| MutateEffectConfig
|
||||
| MutateTransitiveConditionallyConfig
|
||||
| ApplyEffectConfig;
|
||||
|
||||
export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
|
||||
FreezeEffectSchema,
|
||||
CreateEffectSchema,
|
||||
CreateFromEffectSchema,
|
||||
AssignEffectSchema,
|
||||
AliasEffectSchema,
|
||||
CaptureEffectSchema,
|
||||
ImpureEffectSchema,
|
||||
MutateEffectSchema,
|
||||
MutateTransitiveConditionallySchema,
|
||||
ApplyEffectSchema,
|
||||
]);
|
||||
|
||||
export type AliasingSignatureConfig = {
|
||||
receiver: string;
|
||||
params: Array<string>;
|
||||
rest: string | null;
|
||||
returns: string;
|
||||
effects: Array<AliasingEffectConfig>;
|
||||
temporaries: Array<string>;
|
||||
};
|
||||
|
||||
export const AliasingSignatureSchema: z.ZodType<AliasingSignatureConfig> =
|
||||
z.object({
|
||||
receiver: LifetimeIdSchema,
|
||||
params: z.array(LifetimeIdSchema),
|
||||
rest: LifetimeIdSchema.nullable(),
|
||||
returns: LifetimeIdSchema,
|
||||
effects: z.array(AliasingEffectSchema),
|
||||
temporaries: z.array(LifetimeIdSchema),
|
||||
});
|
||||
|
||||
export type FunctionTypeConfig = {
|
||||
kind: 'function';
|
||||
positionalParams: Array<Effect>;
|
||||
@@ -42,6 +235,7 @@ export type FunctionTypeConfig = {
|
||||
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
|
||||
impure?: boolean | null | undefined;
|
||||
canonicalName?: string | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
};
|
||||
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
kind: z.literal('function'),
|
||||
@@ -54,6 +248,7 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
|
||||
impure: z.boolean().nullable().optional(),
|
||||
canonicalName: z.string().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type HookTypeConfig = {
|
||||
@@ -63,6 +258,7 @@ export type HookTypeConfig = {
|
||||
returnType: TypeConfig;
|
||||
returnValueKind?: ValueKind | null | undefined;
|
||||
noAlias?: boolean | null | undefined;
|
||||
aliasing?: AliasingSignatureConfig | null | undefined;
|
||||
};
|
||||
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
kind: z.literal('hook'),
|
||||
@@ -71,6 +267,7 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
returnType: z.lazy(() => TypeSchema),
|
||||
returnValueKind: ValueKindSchema.nullable().optional(),
|
||||
noAlias: z.boolean().nullable().optional(),
|
||||
aliasing: AliasingSignatureSchema.nullable().optional(),
|
||||
});
|
||||
|
||||
export type BuiltInTypeConfig =
|
||||
|
||||
@@ -17,7 +17,6 @@ export {buildReactiveScopeTerminalsHIR} from './BuildReactiveScopeTerminalsHIR';
|
||||
export {computeDominatorTree, computePostDominatorTree} from './Dominator';
|
||||
export {
|
||||
Environment,
|
||||
parseConfigPragmaForTests,
|
||||
validateEnvironmentConfig,
|
||||
type EnvironmentConfig,
|
||||
type ExternalFunction,
|
||||
|
||||
@@ -735,6 +735,7 @@ export function mapTerminalSuccessors(
|
||||
loc: terminal.loc,
|
||||
value: terminal.value,
|
||||
id: makeInstructionId(0),
|
||||
effects: terminal.effects,
|
||||
};
|
||||
}
|
||||
case 'throw': {
|
||||
@@ -842,6 +843,7 @@ export function mapTerminalSuccessors(
|
||||
handler,
|
||||
id: makeInstructionId(0),
|
||||
loc: terminal.loc,
|
||||
effects: terminal.effects,
|
||||
};
|
||||
}
|
||||
case 'try': {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
Hole,
|
||||
IdentifierId,
|
||||
ObjectMethod,
|
||||
Place,
|
||||
SourceLocation,
|
||||
SpreadPattern,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from '../HIR';
|
||||
import {FunctionSignature} from '../HIR/ObjectShape';
|
||||
import {printSourceLocation} from '../HIR/PrintHIR';
|
||||
|
||||
/**
|
||||
* `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
|
||||
* more values in a program. These effects include mutation of values, freezing values,
|
||||
* tracking data flow between values, and other specialized cases.
|
||||
*/
|
||||
export type AliasingEffect =
|
||||
/**
|
||||
* Marks the given value and its direct aliases as frozen.
|
||||
*
|
||||
* Captured values are *not* considered frozen, because we cannot be sure that a previously
|
||||
* captured value will still be captured at the point of the freeze.
|
||||
*
|
||||
* For example:
|
||||
* const x = {};
|
||||
* const y = [x];
|
||||
* y.pop(); // y dosn't contain x anymore!
|
||||
* freeze(y);
|
||||
* mutate(x); // safe to mutate!
|
||||
*
|
||||
* The exception to this is FunctionExpressions - since it is impossible to change which
|
||||
* value a function closes over[1] we can transitively freeze functions and their captures.
|
||||
*
|
||||
* [1] Except for `let` values that are reassigned and closed over by a function, but we
|
||||
* handle this explicitly with StoreContext/LoadContext.
|
||||
*/
|
||||
| {kind: 'Freeze'; value: Place; reason: ValueReason}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'Mutate'; value: Place}
|
||||
/**
|
||||
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
|
||||
* This should be rare.
|
||||
*
|
||||
* TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
|
||||
* correct for iterators of unknown types.
|
||||
*/
|
||||
| {kind: 'MutateConditionally'; value: Place}
|
||||
/**
|
||||
* Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
|
||||
*/
|
||||
| {kind: 'MutateTransitive'; value: Place}
|
||||
/**
|
||||
* Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
|
||||
*/
|
||||
| {kind: 'MutateTransitiveConditionally'; value: Place}
|
||||
/**
|
||||
* Records information flow from `from` to `into` in cases where local mutation of the destination
|
||||
* will *not* mutate the source:
|
||||
*
|
||||
* - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
|
||||
* - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
|
||||
*
|
||||
* Example: `array.push(item)`. Information from item is captured into array, but there is not a
|
||||
* direct aliasing, and local mutations of array will not modify item.
|
||||
*/
|
||||
| {kind: 'Capture'; from: Place; into: Place}
|
||||
/**
|
||||
* Records information flow from `from` to `into` in cases where local mutation of the destination
|
||||
* *will* mutate the source:
|
||||
*
|
||||
* - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
|
||||
* - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
|
||||
*
|
||||
* Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
|
||||
* But we have to assume that it _could_ be returning its input, such that a local mutation of
|
||||
* c could be mutating a.
|
||||
*/
|
||||
| {kind: 'Alias'; from: Place; into: Place}
|
||||
/**
|
||||
* Records direct assignment: `into = from`.
|
||||
*/
|
||||
| {kind: 'Assign'; from: Place; into: Place}
|
||||
/**
|
||||
* Creates a value of the given type at the given place
|
||||
*/
|
||||
| {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
|
||||
/**
|
||||
* Creates a new value with the same kind as the starting value.
|
||||
*/
|
||||
| {kind: 'CreateFrom'; from: Place; into: Place}
|
||||
/**
|
||||
* Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
|
||||
*/
|
||||
| {kind: 'ImmutableCapture'; from: Place; into: Place}
|
||||
/**
|
||||
* Calls the function at the given place with the given arguments either captured or aliased,
|
||||
* and captures/aliases the result into the given place.
|
||||
*/
|
||||
| {
|
||||
kind: 'Apply';
|
||||
receiver: Place;
|
||||
function: Place;
|
||||
mutatesFunction: boolean;
|
||||
args: Array<Place | SpreadPattern | Hole>;
|
||||
into: Place;
|
||||
signature: FunctionSignature | null;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
/**
|
||||
* Constructs a function value with the given captures. The mutability of the function
|
||||
* will be determined by the mutability of the capture values when evaluated.
|
||||
*/
|
||||
| {
|
||||
kind: 'CreateFunction';
|
||||
captures: Array<Place>;
|
||||
function: FunctionExpression | ObjectMethod;
|
||||
into: Place;
|
||||
}
|
||||
/**
|
||||
* Mutation of a value known to be immutable
|
||||
*/
|
||||
| {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions}
|
||||
/**
|
||||
* Mutation of a global
|
||||
*/
|
||||
| {
|
||||
kind: 'MutateGlobal';
|
||||
place: Place;
|
||||
error: CompilerErrorDetailOptions;
|
||||
}
|
||||
/**
|
||||
* Indicates a side-effect that is not safe during render
|
||||
*/
|
||||
| {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions}
|
||||
/**
|
||||
* Indicates that a given place is accessed during render. Used to distingush
|
||||
* hook arguments that are known to be called immediately vs those used for
|
||||
* event handlers/effects, and for JSX values known to be called during render
|
||||
* (tags, children) vs those that may be events/effect (other props).
|
||||
*/
|
||||
| {
|
||||
kind: 'Render';
|
||||
place: Place;
|
||||
};
|
||||
|
||||
export function hashEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Apply': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.receiver.identifier.id,
|
||||
effect.function.identifier.id,
|
||||
effect.mutatesFunction,
|
||||
effect.args
|
||||
.map(a => {
|
||||
if (a.kind === 'Hole') {
|
||||
return '';
|
||||
} else if (a.kind === 'Identifier') {
|
||||
return a.identifier.id;
|
||||
} else {
|
||||
return `...${a.place.identifier.id}`;
|
||||
}
|
||||
})
|
||||
.join(','),
|
||||
effect.into.identifier.id,
|
||||
].join(':');
|
||||
}
|
||||
case 'CreateFrom':
|
||||
case 'ImmutableCapture':
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.from.identifier.id,
|
||||
effect.into.identifier.id,
|
||||
].join(':');
|
||||
}
|
||||
case 'Create': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.into.identifier.id,
|
||||
effect.value,
|
||||
effect.reason,
|
||||
].join(':');
|
||||
}
|
||||
case 'Freeze': {
|
||||
return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render': {
|
||||
return [effect.kind, effect.place.identifier.id].join(':');
|
||||
}
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.place.identifier.id,
|
||||
effect.error.severity,
|
||||
effect.error.reason,
|
||||
effect.error.description,
|
||||
printSourceLocation(effect.error.loc ?? GeneratedSource),
|
||||
].join(':');
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return [effect.kind, effect.value.identifier.id].join(':');
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
return [
|
||||
effect.kind,
|
||||
effect.into.identifier.id,
|
||||
// return places are a unique way to identify functions themselves
|
||||
effect.function.loweredFunc.func.returns.identifier.id,
|
||||
effect.captures.map(p => p.identifier.id).join(','),
|
||||
].join(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AliasingSignature = {
|
||||
receiver: IdentifierId;
|
||||
params: Array<IdentifierId>;
|
||||
rest: IdentifierId | null;
|
||||
returns: IdentifierId;
|
||||
effects: Array<AliasingEffect>;
|
||||
temporaries: Array<Place>;
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
@@ -19,6 +20,10 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
|
||||
import {inferFunctionExpressionAliasingEffectsSignature} from './InferFunctionExpressionAliasingEffectsSignature';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
@@ -26,15 +31,27 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
*/
|
||||
for (const operand of instr.value.loweredFunc.func.context) {
|
||||
operand.identifier.mutableRange.start = makeInstructionId(0);
|
||||
operand.identifier.mutableRange.end = makeInstructionId(0);
|
||||
/**
|
||||
* NOTE: inferReactiveScopeVariables makes identifiers in the scope
|
||||
* point to the *same* mutableRange instance. Resetting start/end
|
||||
* here is insufficient, because a later mutation of the range
|
||||
* for any one identifier could affect the range for other identifiers.
|
||||
*/
|
||||
operand.identifier.mutableRange = {
|
||||
start: makeInstructionId(0),
|
||||
end: makeInstructionId(0),
|
||||
};
|
||||
operand.identifier.scope = null;
|
||||
}
|
||||
break;
|
||||
@@ -44,6 +61,87 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
/**
|
||||
* Phase 1: similar to lower(), but using the new mutation/aliasing inference
|
||||
*/
|
||||
analyseFunctions(fn);
|
||||
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
|
||||
deadCodeElimination(fn);
|
||||
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
const effects = inferFunctionExpressionAliasingEffectsSignature(fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
if (effects != null) {
|
||||
fn.aliasingEffects ??= [];
|
||||
fn.aliasingEffects?.push(...effects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: populate the Effect of each context variable to use in inferring
|
||||
* the outer function. For example, InferMutationAliasingEffects uses context variable
|
||||
* effects to decide if the function may be mutable or not.
|
||||
*/
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of effects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
capturedOrMutated.add(effect.value.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal':
|
||||
case 'CreateFunction':
|
||||
case 'Create':
|
||||
case 'Freeze':
|
||||
case 'ImmutableCapture': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind ${(effect as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of fn.context) {
|
||||
if (
|
||||
capturedOrMutated.has(operand.identifier.id) ||
|
||||
operand.effect === Effect.Capture
|
||||
) {
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
|
||||
@@ -197,6 +197,7 @@ function makeManualMemoizationMarkers(
|
||||
deps: depsList,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
@@ -208,6 +209,7 @@ function makeManualMemoizationMarkers(
|
||||
decl: {...memoDecl},
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
/**
|
||||
* If a function captures a mutable value but never gets called, we don't infer a
|
||||
* mutable range for that function. This means that we also don't alias the function
|
||||
* with its mutable captures.
|
||||
*
|
||||
* This case is tricky, because we don't generally know for sure what is a mutation
|
||||
* and what may just be a normal function call. For example:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo() {
|
||||
* const x = makeObject();
|
||||
* return () => {
|
||||
* return readObject(x); // could be a mutation!
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If we pessimistically assume that all such cases are mutations, we'd have to group
|
||||
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
|
||||
*
|
||||
* ```
|
||||
* hook useFoo(createEntryForKey) {
|
||||
* const cache = new WeakMap();
|
||||
* return (key) => {
|
||||
* let entry = cache.get(key);
|
||||
* if (entry == null) {
|
||||
* entry = createEntryForKey(key);
|
||||
* cache.set(key, entry); // known mutation!
|
||||
* }
|
||||
* return entry;
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Then we have to ensure that the function and its mutable captures alias together and
|
||||
* end up in the same scope. However, aliasing together isn't enough if the function
|
||||
* and operands all have empty mutable ranges (end = start + 1).
|
||||
*
|
||||
* This pass finds function expressions and object methods that have an empty mutable range
|
||||
* and known-mutable operands which also don't have a mutable range, and ensures that the
|
||||
* function and those operands are aliased together *and* that their ranges are updated to
|
||||
* end after the function expression. This is sufficient to ensure that a reactive scope is
|
||||
* created for the alias set.
|
||||
*/
|
||||
export function inferAliasForUncalledFunctions(
|
||||
fn: HIRFunction,
|
||||
aliases: DisjointSet<Identifier>,
|
||||
): void {
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
instrs: for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind !== 'ObjectMethod' &&
|
||||
value.kind !== 'FunctionExpression'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function is known to be mutated, we will have
|
||||
* already aliased any mutable operands with it
|
||||
*/
|
||||
const range = lvalue.identifier.mutableRange;
|
||||
if (range.end > range.start + 1) {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* If the function already has operands with an active mutable range,
|
||||
* then we don't need to do anything — the function will have already
|
||||
* been visited and included in some mutable alias set. This case can
|
||||
* also occur due to visiting the same function in an earlier iteration
|
||||
* of the outer fixpoint loop.
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
if (isMutable(instr, operand)) {
|
||||
continue instrs;
|
||||
}
|
||||
}
|
||||
const operands: Set<Identifier> = new Set();
|
||||
for (const effect of value.loweredFunc.func.effects ?? []) {
|
||||
if (effect.kind !== 'ContextMutation') {
|
||||
continue;
|
||||
}
|
||||
/*
|
||||
* We're looking for known-mutations only, so we look at the effects
|
||||
* rather than function context
|
||||
*/
|
||||
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
|
||||
for (const operand of effect.places) {
|
||||
/*
|
||||
* It's possible that function effect analysis thinks there was a context mutation,
|
||||
* but then InferReferenceEffects figures out some operands are globals and therefore
|
||||
* creates a non-mutable effect for those operands.
|
||||
* We should change InferReferenceEffects to swap the ContextMutation for a global
|
||||
* mutation in that case, but for now we just filter them out here
|
||||
*/
|
||||
if (
|
||||
isMutableEffect(operand.effect, operand.loc) &&
|
||||
!isRefOrRefLikeMutableType(operand.identifier.type)
|
||||
) {
|
||||
operands.add(operand.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (operands.size !== 0) {
|
||||
operands.add(lvalue.identifier);
|
||||
aliases.union([...operands]);
|
||||
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
|
||||
for (const operand of operands) {
|
||||
operand.mutableRange.end = makeInstructionId(instr.id + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
ArrayExpression,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
@@ -22,18 +21,41 @@ import {
|
||||
ScopeId,
|
||||
ReactiveScopeDependency,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
ReactiveScopeDependencies,
|
||||
Terminal,
|
||||
isUseRefType,
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
HIR,
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
isEffectEventFunctionType,
|
||||
} from '../HIR';
|
||||
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
|
||||
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
|
||||
import {ReactiveScopeDependencyTreeHIR} from '../HIR/DeriveMinimalDependenciesHIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {
|
||||
collectTemporariesSidemap,
|
||||
DependencyCollectionContext,
|
||||
handleInstruction,
|
||||
} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
import {buildDependencyInstructions} from '../HIR/ScopeDependencyUtils';
|
||||
import {
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
terminalFallthrough,
|
||||
} from '../HIR/visitors';
|
||||
import {empty} from '../Utils/Stack';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
@@ -41,7 +63,6 @@ import {getOrInsertWith} from '../Utils/utils';
|
||||
* a second argument to the useEffect call if no dependency array is provided.
|
||||
*/
|
||||
export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
let hasRewrite = false;
|
||||
const fnExpressions = new Map<
|
||||
IdentifierId,
|
||||
TInstruction<FunctionExpression>
|
||||
@@ -62,10 +83,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
const autodepFnLoads = new Map<IdentifierId, number>();
|
||||
const autodepModuleLoads = new Map<IdentifierId, Map<string, number>>();
|
||||
|
||||
const scopeInfos = new Map<
|
||||
ScopeId,
|
||||
{pruned: boolean; deps: ReactiveScopeDependencies; hasSingleInstr: boolean}
|
||||
>();
|
||||
const scopeInfos = new Map<ScopeId, ReactiveScopeDependencies>();
|
||||
|
||||
const loadGlobals = new Set<IdentifierId>();
|
||||
|
||||
@@ -77,23 +95,23 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
* reactive(Identifier i) = Union_{reference of i}(reactive(reference))
|
||||
*/
|
||||
const reactiveIds = inferReactiveIdentifiers(fn);
|
||||
const rewriteBlocks: Array<BasicBlock> = [];
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'scope' ||
|
||||
block.terminal.kind === 'pruned-scope'
|
||||
) {
|
||||
if (block.terminal.kind === 'scope') {
|
||||
const scopeBlock = fn.body.blocks.get(block.terminal.block)!;
|
||||
scopeInfos.set(block.terminal.scope.id, {
|
||||
pruned: block.terminal.kind === 'pruned-scope',
|
||||
deps: block.terminal.scope.dependencies,
|
||||
hasSingleInstr:
|
||||
scopeBlock.instructions.length === 1 &&
|
||||
scopeBlock.terminal.kind === 'goto' &&
|
||||
scopeBlock.terminal.block === block.terminal.fallthrough,
|
||||
});
|
||||
if (
|
||||
scopeBlock.instructions.length === 1 &&
|
||||
scopeBlock.terminal.kind === 'goto' &&
|
||||
scopeBlock.terminal.block === block.terminal.fallthrough
|
||||
) {
|
||||
scopeInfos.set(
|
||||
block.terminal.scope.id,
|
||||
block.terminal.scope.dependencies,
|
||||
);
|
||||
}
|
||||
}
|
||||
const rewriteInstrs = new Map<InstructionId, Array<Instruction>>();
|
||||
const rewriteInstrs: Array<SpliceInfo> = [];
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
if (value.kind === 'FunctionExpression') {
|
||||
@@ -157,7 +175,6 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
) {
|
||||
// We have a useEffect call with no deps array, so we need to infer the deps
|
||||
const effectDeps: Array<Place> = [];
|
||||
const newInstructions: Array<Instruction> = [];
|
||||
const deps: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements: effectDeps,
|
||||
@@ -173,22 +190,12 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
fnExpr.lvalue.identifier.scope != null
|
||||
? scopeInfos.get(fnExpr.lvalue.identifier.scope.id)
|
||||
: null;
|
||||
CompilerError.invariant(scopeInfo != null, {
|
||||
reason: 'Expected function expression scope to exist',
|
||||
loc: value.loc,
|
||||
});
|
||||
if (scopeInfo.pruned || !scopeInfo.hasSingleInstr) {
|
||||
/**
|
||||
* TODO: retry pipeline that ensures effect function expressions
|
||||
* are placed into their own scope
|
||||
*/
|
||||
CompilerError.throwTodo({
|
||||
reason:
|
||||
'[InferEffectDependencies] Expected effect function to have non-pruned scope and its scope to have exactly one instruction',
|
||||
loc: fnExpr.loc,
|
||||
});
|
||||
let minimalDeps: Set<ReactiveScopeDependency>;
|
||||
if (scopeInfo != null) {
|
||||
minimalDeps = new Set(scopeInfo);
|
||||
} else {
|
||||
minimalDeps = inferMinimalDependencies(fnExpr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: push dependencies to the effect deps array
|
||||
*
|
||||
@@ -196,25 +203,31 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
* the `infer-effect-deps/pruned-nonreactive-obj` fixture for an
|
||||
* explanation.
|
||||
*/
|
||||
|
||||
const usedDeps = [];
|
||||
for (const dep of scopeInfo.deps) {
|
||||
for (const maybeDep of minimalDeps) {
|
||||
if (
|
||||
((isUseRefType(dep.identifier) ||
|
||||
isSetStateType(dep.identifier)) &&
|
||||
!reactiveIds.has(dep.identifier.id)) ||
|
||||
isFireFunctionType(dep.identifier)
|
||||
((isUseRefType(maybeDep.identifier) ||
|
||||
isSetStateType(maybeDep.identifier)) &&
|
||||
!reactiveIds.has(maybeDep.identifier.id)) ||
|
||||
isFireFunctionType(maybeDep.identifier) ||
|
||||
isEffectEventFunctionType(maybeDep.identifier)
|
||||
) {
|
||||
// exclude non-reactive hook results, which will never be in a memo block
|
||||
continue;
|
||||
}
|
||||
|
||||
const {place, instructions} = writeDependencyToInstructions(
|
||||
const dep = truncateDepAtCurrent(maybeDep);
|
||||
const {place, value, exitBlockId} = buildDependencyInstructions(
|
||||
dep,
|
||||
reactiveIds.has(dep.identifier.id),
|
||||
fn.env,
|
||||
fnExpr.loc,
|
||||
);
|
||||
newInstructions.push(...instructions);
|
||||
rewriteInstrs.push({
|
||||
kind: 'block',
|
||||
location: instr.id,
|
||||
value,
|
||||
exitBlockId: exitBlockId,
|
||||
});
|
||||
effectDeps.push(place);
|
||||
usedDeps.push(dep);
|
||||
}
|
||||
@@ -235,27 +248,34 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
});
|
||||
}
|
||||
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
});
|
||||
|
||||
// Step 2: push the inferred deps array as an argument of the useEffect
|
||||
rewriteInstrs.push({
|
||||
kind: 'instr',
|
||||
location: instr.id,
|
||||
value: {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
} else if (loadGlobals.has(value.args[0].identifier.id)) {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
rewriteInstrs.push({
|
||||
kind: 'instr',
|
||||
location: instr.id,
|
||||
value: {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
effects: null,
|
||||
},
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
fn.env.inferredEffectLocations.add(callee.loc);
|
||||
}
|
||||
} else if (
|
||||
@@ -286,85 +306,164 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rewriteInstrs.size > 0) {
|
||||
hasRewrite = true;
|
||||
const newInstrs = [];
|
||||
for (const instr of block.instructions) {
|
||||
const newInstr = rewriteInstrs.get(instr.id);
|
||||
if (newInstr != null) {
|
||||
newInstrs.push(...newInstr, instr);
|
||||
} else {
|
||||
newInstrs.push(instr);
|
||||
}
|
||||
}
|
||||
block.instructions = newInstrs;
|
||||
}
|
||||
rewriteSplices(block, rewriteInstrs, rewriteBlocks);
|
||||
}
|
||||
if (hasRewrite) {
|
||||
|
||||
if (rewriteBlocks.length > 0) {
|
||||
for (const block of rewriteBlocks) {
|
||||
fn.body.blocks.set(block.id, block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixup the HIR to restore RPO, ensure correct predecessors, and renumber
|
||||
* instructions.
|
||||
*/
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
// Renumber instructions and fix scope ranges
|
||||
markInstructionIds(fn.body);
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
|
||||
fn.env.hasInferredEffect = true;
|
||||
}
|
||||
}
|
||||
|
||||
function writeDependencyToInstructions(
|
||||
function truncateDepAtCurrent(
|
||||
dep: ReactiveScopeDependency,
|
||||
reactive: boolean,
|
||||
env: Environment,
|
||||
loc: SourceLocation,
|
||||
): {place: Place; instructions: Array<Instruction>} {
|
||||
const instructions: Array<Instruction> = [];
|
||||
let currValue = createTemporaryPlace(env, GeneratedSource);
|
||||
currValue.reactive = reactive;
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...currValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
},
|
||||
loc: loc,
|
||||
},
|
||||
});
|
||||
for (const path of dep.path) {
|
||||
if (path.optional) {
|
||||
/**
|
||||
* TODO: instead of truncating optional paths, reuse
|
||||
* instructions from hoisted dependencies block(s)
|
||||
*/
|
||||
break;
|
||||
}
|
||||
if (path.property === 'current') {
|
||||
/*
|
||||
* Prune ref.current accesses. This may over-capture for non-ref values with
|
||||
* a current property, but that's fine.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
const nextValue = createTemporaryPlace(env, GeneratedSource);
|
||||
nextValue.reactive = reactive;
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...nextValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {...currValue, effect: Effect.Capture},
|
||||
property: path.property,
|
||||
loc: loc,
|
||||
},
|
||||
});
|
||||
currValue = nextValue;
|
||||
): ReactiveScopeDependency {
|
||||
const idx = dep.path.findIndex(path => path.property === 'current');
|
||||
if (idx === -1) {
|
||||
return dep;
|
||||
} else {
|
||||
return {...dep, path: dep.path.slice(0, idx)};
|
||||
}
|
||||
currValue.effect = Effect.Freeze;
|
||||
return {place: currValue, instructions};
|
||||
}
|
||||
|
||||
type SpliceInfo =
|
||||
| {kind: 'instr'; location: InstructionId; value: Instruction}
|
||||
| {
|
||||
kind: 'block';
|
||||
location: InstructionId;
|
||||
value: HIR;
|
||||
exitBlockId: BlockId;
|
||||
};
|
||||
|
||||
function rewriteSplices(
|
||||
originalBlock: BasicBlock,
|
||||
splices: Array<SpliceInfo>,
|
||||
rewriteBlocks: Array<BasicBlock>,
|
||||
): void {
|
||||
if (splices.length === 0) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Splice instructions or value blocks into the original block.
|
||||
* --- original block ---
|
||||
* bb_original
|
||||
* instr1
|
||||
* ...
|
||||
* instr2 <-- splice location
|
||||
* instr3
|
||||
* ...
|
||||
* <original terminal>
|
||||
*
|
||||
* If there is more than one block in the splice, this means that we're
|
||||
* splicing in a set of value-blocks of the following structure:
|
||||
* --- blocks we're splicing in ---
|
||||
* bb_entry:
|
||||
* instrEntry
|
||||
* ...
|
||||
* <splice terminal> fallthrough=bb_exit
|
||||
*
|
||||
* bb1(value):
|
||||
* ...
|
||||
*
|
||||
* bb_exit:
|
||||
* instrExit
|
||||
* ...
|
||||
* <synthetic terminal>
|
||||
*
|
||||
*
|
||||
* --- rewritten blocks ---
|
||||
* bb_original
|
||||
* instr1
|
||||
* ... (original instructions)
|
||||
* instr2
|
||||
* instrEntry
|
||||
* ... (spliced instructions)
|
||||
* <splice terminal> fallthrough=bb_exit
|
||||
*
|
||||
* bb1(value):
|
||||
* ...
|
||||
*
|
||||
* bb_exit:
|
||||
* instrExit
|
||||
* ... (spliced instructions)
|
||||
* instr3
|
||||
* ... (original instructions)
|
||||
* <original terminal>
|
||||
*/
|
||||
const originalInstrs = originalBlock.instructions;
|
||||
let currBlock: BasicBlock = {...originalBlock, instructions: []};
|
||||
rewriteBlocks.push(currBlock);
|
||||
|
||||
let cursor = 0;
|
||||
for (const rewrite of splices) {
|
||||
while (originalInstrs[cursor].id < rewrite.location) {
|
||||
CompilerError.invariant(
|
||||
originalInstrs[cursor].id < originalInstrs[cursor + 1].id,
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
},
|
||||
);
|
||||
currBlock.instructions.push(originalInstrs[cursor]);
|
||||
cursor++;
|
||||
}
|
||||
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: splice location not found',
|
||||
loc: originalInstrs[cursor].loc,
|
||||
});
|
||||
|
||||
if (rewrite.kind === 'instr') {
|
||||
currBlock.instructions.push(rewrite.value);
|
||||
} else {
|
||||
const {entry, blocks} = rewrite.value;
|
||||
const entryBlock = blocks.get(entry)!;
|
||||
// splice in all instructions from the entry block
|
||||
currBlock.instructions.push(...entryBlock.instructions);
|
||||
if (blocks.size > 1) {
|
||||
/**
|
||||
* We're splicing in a set of value-blocks, which means we need
|
||||
* to push new blocks and update terminals.
|
||||
*/
|
||||
CompilerError.invariant(
|
||||
terminalFallthrough(entryBlock.terminal) === rewrite.exitBlockId,
|
||||
{
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
|
||||
loc: entryBlock.terminal.loc,
|
||||
},
|
||||
);
|
||||
const originalTerminal = currBlock.terminal;
|
||||
currBlock.terminal = entryBlock.terminal;
|
||||
|
||||
for (const [id, block] of blocks) {
|
||||
if (id === entry) {
|
||||
continue;
|
||||
}
|
||||
if (id === rewrite.exitBlockId) {
|
||||
block.terminal = originalTerminal;
|
||||
currBlock = block;
|
||||
}
|
||||
rewriteBlocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currBlock.instructions.push(...originalInstrs.slice(cursor));
|
||||
}
|
||||
|
||||
function inferReactiveIdentifiers(fn: HIRFunction): Set<IdentifierId> {
|
||||
@@ -422,3 +521,132 @@ function collectDepUsages(
|
||||
|
||||
return sourceLocations;
|
||||
}
|
||||
|
||||
function inferMinimalDependencies(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
|
||||
const temporaries = collectTemporariesSidemap(fn, new Set());
|
||||
const {
|
||||
hoistableObjects,
|
||||
processedInstrsInOptional,
|
||||
temporariesReadInOptional,
|
||||
} = collectOptionalChainSidemap(fn);
|
||||
|
||||
const hoistablePropertyLoads = collectHoistablePropertyLoadsInInnerFn(
|
||||
fnInstr,
|
||||
temporaries,
|
||||
hoistableObjects,
|
||||
);
|
||||
const hoistableToFnEntry = hoistablePropertyLoads.get(fn.body.entry);
|
||||
CompilerError.invariant(hoistableToFnEntry != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing entry block',
|
||||
loc: fnInstr.loc,
|
||||
});
|
||||
|
||||
const dependencies = inferDependencies(
|
||||
fnInstr,
|
||||
new Map([...temporaries, ...temporariesReadInOptional]),
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
|
||||
const tree = new ReactiveScopeDependencyTreeHIR(
|
||||
[...hoistableToFnEntry.assumedNonNullObjects].map(o => o.fullPath),
|
||||
);
|
||||
for (const dep of dependencies) {
|
||||
tree.addDependency({...dep});
|
||||
}
|
||||
|
||||
return tree.deriveMinimalDependencies();
|
||||
}
|
||||
|
||||
function inferDependencies(
|
||||
fnInstr: TInstruction<FunctionExpression>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
|
||||
): Set<ReactiveScopeDependency> {
|
||||
const fn = fnInstr.value.loweredFunc.func;
|
||||
const context = new DependencyCollectionContext(
|
||||
new Set(),
|
||||
temporaries,
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
for (const dep of fn.context) {
|
||||
context.declare(dep.identifier, {
|
||||
id: makeInstructionId(0),
|
||||
scope: empty(),
|
||||
});
|
||||
}
|
||||
const placeholderScope: ReactiveScope = {
|
||||
id: makeScopeId(0),
|
||||
range: {
|
||||
start: fnInstr.id,
|
||||
end: makeInstructionId(fnInstr.id + 1),
|
||||
},
|
||||
dependencies: new Set(),
|
||||
reassignments: new Set(),
|
||||
declarations: new Map(),
|
||||
earlyReturnValue: null,
|
||||
merged: new Set(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
context.enterScope(placeholderScope);
|
||||
inferDependenciesInFn(fn, context, temporaries);
|
||||
context.exitScope(placeholderScope, false);
|
||||
const resultUnfiltered = context.deps.get(placeholderScope);
|
||||
CompilerError.invariant(resultUnfiltered != null, {
|
||||
reason:
|
||||
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
|
||||
loc: fn.loc,
|
||||
});
|
||||
|
||||
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));
|
||||
const result = new Set<ReactiveScopeDependency>();
|
||||
for (const dep of resultUnfiltered) {
|
||||
if (fnContext.has(dep.identifier.id)) {
|
||||
result.add(dep);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function inferDependenciesInFn(
|
||||
fn: HIRFunction,
|
||||
context: DependencyCollectionContext,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
// Record referenced optional chains in phis
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands) {
|
||||
const maybeOptionalChain = temporaries.get(operand[1].identifier.id);
|
||||
if (maybeOptionalChain) {
|
||||
context.visitDependency(maybeOptionalChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
context.declare(instr.lvalue.identifier, {
|
||||
id: instr.id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
/**
|
||||
* Recursively visit the inner function to extract dependencies
|
||||
*/
|
||||
const innerFn = instr.value.loweredFunc.func;
|
||||
context.enterInnerFn(instr as TInstruction<FunctionExpression>, () => {
|
||||
inferDependenciesInFn(innerFn, context, temporaries);
|
||||
});
|
||||
} else {
|
||||
handleInstruction(instr, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
@@ -339,6 +339,12 @@ function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
|
||||
return 'Updating a value previously passed as an argument to a hook is not allowed. Consider moving the mutation before calling the hook';
|
||||
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
|
||||
return 'Updating a value returned from a hook is not allowed. Consider moving the mutation into the hook where the value is constructed';
|
||||
} else {
|
||||
return 'This mutates a variable that React considers immutable';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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 {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './AliasingEffects';
|
||||
|
||||
/**
|
||||
* This function tracks data flow within an inner function expression in order to
|
||||
* compute a set of data-flow aliasing effects describing data flow between the function's
|
||||
* params, context variables, and return value.
|
||||
*
|
||||
* For example, consider the following function expression:
|
||||
*
|
||||
* ```
|
||||
* (x) => { return [x, y] }
|
||||
* ```
|
||||
*
|
||||
* This function captures both param `x` and context variable `y` into the return value.
|
||||
* Unlike our previous inference which counted this as a mutation of x and y, we want to
|
||||
* build a signature for the function that describes the data flow. We would infer
|
||||
* `Capture x -> return, Capture y -> return` effects for this function.
|
||||
*
|
||||
* This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
|
||||
* from instructions within the function up to the function itself.
|
||||
*/
|
||||
export function inferFunctionExpressionAliasingEffectsSignature(
|
||||
fn: HIRFunction,
|
||||
): Array<AliasingEffect> | null {
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
|
||||
/**
|
||||
* Map used to identify tracked variables: params, context vars, return value
|
||||
* This is used to detect mutation/capturing/aliasing of params/context vars
|
||||
*/
|
||||
const tracked = new Map<IdentifierId, Place>();
|
||||
tracked.set(fn.returns.identifier.id, fn.returns);
|
||||
for (const operand of [...fn.context, ...fn.params]) {
|
||||
const place = operand.kind === 'Identifier' ? operand : operand.place;
|
||||
tracked.set(place.identifier.id, place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track capturing/aliasing of context vars and params into each other and into the return.
|
||||
* We don't need to track locals and intermediate values, since we're only concerned with effects
|
||||
* as they relate to arguments visible outside the function.
|
||||
*
|
||||
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
|
||||
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
|
||||
*/
|
||||
type AliasedIdentifier = {
|
||||
kind: AliasingKind;
|
||||
place: Place;
|
||||
};
|
||||
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
|
||||
|
||||
/*
|
||||
* Check for aliasing of tracked values. Also joins the effects of how the value is
|
||||
* used (@param kind) with the aliasing type of each value
|
||||
*/
|
||||
function lookup(
|
||||
place: Place,
|
||||
kind: AliasedIdentifier['kind'],
|
||||
): Array<AliasedIdentifier> | null {
|
||||
if (tracked.has(place.identifier.id)) {
|
||||
return [{kind, place}];
|
||||
}
|
||||
return (
|
||||
dataFlow.get(place.identifier.id)?.map(aliased => ({
|
||||
kind: joinEffects(aliased.kind, kind),
|
||||
place: aliased.place,
|
||||
})) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// todo: fixpoint
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
const operands: Array<AliasedIdentifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const inputs = lookup(operand, 'Alias');
|
||||
if (inputs != null) {
|
||||
operands.push(...inputs);
|
||||
}
|
||||
}
|
||||
if (operands.length !== 0) {
|
||||
dataFlow.set(phi.place.identifier.id, operands);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (
|
||||
effect.kind === 'Assign' ||
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'Alias' ||
|
||||
effect.kind === 'CreateFrom'
|
||||
) {
|
||||
const from = lookup(effect.from, effect.kind);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
const into = lookup(effect.into, 'Alias');
|
||||
if (into == null) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
} else {
|
||||
for (const aliased of into) {
|
||||
getOrInsertDefault(
|
||||
dataFlow,
|
||||
aliased.place.identifier.id,
|
||||
[],
|
||||
).push(...from);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
effect.kind === 'Create' ||
|
||||
effect.kind === 'CreateFunction'
|
||||
) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
|
||||
{kind: 'Alias', place: effect.into},
|
||||
]);
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure' ||
|
||||
effect.kind === 'Render'
|
||||
) {
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
const from = lookup(block.terminal.value, 'Alias');
|
||||
if (from != null) {
|
||||
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create aliasing effects based on observed data flow
|
||||
let hasReturn = false;
|
||||
for (const [into, from] of dataFlow) {
|
||||
const input = tracked.get(into);
|
||||
if (input == null) {
|
||||
continue;
|
||||
}
|
||||
for (const aliased of from) {
|
||||
if (
|
||||
aliased.place.identifier.id === input.identifier.id ||
|
||||
!tracked.has(aliased.place.identifier.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const effect = {kind: aliased.kind, from: aliased.place, into: input};
|
||||
effects.push(effect);
|
||||
if (
|
||||
into === fn.returns.identifier.id &&
|
||||
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
|
||||
) {
|
||||
hasReturn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: more precise return effect inference
|
||||
if (!hasReturn) {
|
||||
effects.unshift({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
|
||||
function joinEffects(
|
||||
effect1: AliasingKind,
|
||||
effect2: AliasingKind,
|
||||
): AliasingKind {
|
||||
if (effect1 === 'Capture' || effect2 === 'Capture') {
|
||||
return 'Capture';
|
||||
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
|
||||
return 'Assign';
|
||||
} else {
|
||||
return 'Alias';
|
||||
}
|
||||
}
|
||||
@@ -176,9 +176,15 @@ export function inferMutableLifetimes(
|
||||
if (
|
||||
instr.value.kind === 'DeclareContext' ||
|
||||
(instr.value.kind === 'StoreContext' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign)
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign &&
|
||||
!contextVariableDeclarationInstructions.has(
|
||||
instr.value.lvalue.place.identifier,
|
||||
))
|
||||
) {
|
||||
// Save declarations of context variables
|
||||
/**
|
||||
* Save declarations of context variables if they hasn't already been
|
||||
* declared (due to hoisted declarations).
|
||||
*/
|
||||
contextVariableDeclarationInstructions.set(
|
||||
instr.value.lvalue.place.identifier,
|
||||
instr.id,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import {HIRFunction, Identifier} from '../HIR/HIR';
|
||||
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
|
||||
import {inferAliases} from './InferAlias';
|
||||
import {inferAliasForPhis} from './InferAliasForPhis';
|
||||
import {inferAliasForStores} from './InferAliasForStores';
|
||||
@@ -76,6 +77,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
inferAliasForUncalledFunctions(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
@@ -84,7 +86,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
|
||||
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,737 @@
|
||||
/**
|
||||
* 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 prettyFormat from 'pretty-format';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {printFunction} from '../HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {MutationKind} from './InferFunctionExpressionAliasingEffectsSignature';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
const DEBUG = false;
|
||||
const VERBOSE = false;
|
||||
|
||||
/**
|
||||
* Infers mutable ranges for all values in the program, using previously inferred
|
||||
* mutation/aliasing effects. This pass builds a data flow graph using the effects,
|
||||
* tracking an abstract notion of "when" each effect occurs relative to the others.
|
||||
* It then walks each mutation effect against the graph, updating the range of each
|
||||
* node that would be reachable at the "time" that the effect occurred.
|
||||
*
|
||||
* This pass also validates against invalid effects: any function that is reachable
|
||||
* by being called, or via a Render effect, is validated against mutating globals
|
||||
* or calling impure code.
|
||||
*
|
||||
* Note that this function also populates the outer function's aliasing effects with
|
||||
* any mutations that apply to its params or context variables. For example, a
|
||||
* function expression such as the following:
|
||||
*
|
||||
* ```
|
||||
* (x) => { x.y = true }
|
||||
* ```
|
||||
*
|
||||
* Would populate a `Mutate x` aliasing effect on the outer function.
|
||||
*/
|
||||
export function inferMutationAliasingRanges(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean},
|
||||
): Result<void, CompilerError> {
|
||||
if (VERBOSE) {
|
||||
console.log();
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
/**
|
||||
* Part 1: Infer mutable ranges for values. We build an abstract model of
|
||||
* values, the alias/capture edges between them, and the set of mutations.
|
||||
* Edges and mutations are ordered, with mutations processed against the
|
||||
* abstract model only after it is fully constructed by visiting all blocks
|
||||
* _and_ connecting phis. Phis are considered ordered at the time of the
|
||||
* phi node.
|
||||
*
|
||||
* This should (may?) mean that mutations are able to see the full state
|
||||
* of the graph and mark all the appropriate identifiers as mutated at
|
||||
* the correct point, accounting for both backward and forward edges.
|
||||
* Ie a mutation of x accounts for both values that flowed into x,
|
||||
* and values that x flowed into.
|
||||
*/
|
||||
const state = new AliasingState();
|
||||
type PendingPhiOperand = {from: Place; into: Place; index: number};
|
||||
const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
|
||||
const mutations: Array<{
|
||||
index: number;
|
||||
id: InstructionId;
|
||||
transitive: boolean;
|
||||
kind: MutationKind;
|
||||
place: Place;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.create(place, {kind: 'Object'});
|
||||
}
|
||||
const seenBlocks = new Set<BlockId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
state.create(phi.place, {kind: 'Phi'});
|
||||
for (const [pred, operand] of phi.operands) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// NOTE: annotation required to actually typecheck and not silently infer `any`
|
||||
const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
|
||||
pendingPhis,
|
||||
pred,
|
||||
() => [],
|
||||
);
|
||||
blockPhis.push({from: operand, into: phi.place, index: index++});
|
||||
} else {
|
||||
state.assign(index++, operand, phi.place);
|
||||
}
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
state.create(instr.lvalue, {
|
||||
kind: 'Function',
|
||||
function: instr.value.loweredFunc.func,
|
||||
});
|
||||
} else {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
state.create(lvalue, {kind: 'Object'});
|
||||
}
|
||||
}
|
||||
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Create') {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
} else if (effect.kind === 'CreateFunction') {
|
||||
state.create(effect.into, {
|
||||
kind: 'Function',
|
||||
function: effect.function.loweredFunc.func,
|
||||
});
|
||||
} else if (effect.kind === 'CreateFrom') {
|
||||
state.createFrom(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Assign') {
|
||||
if (!state.nodes.has(effect.into.identifier)) {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
}
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitive' ||
|
||||
effect.kind === 'MutateTransitiveConditionally'
|
||||
) {
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: true,
|
||||
kind:
|
||||
effect.kind === 'MutateTransitive'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'Mutate' ||
|
||||
effect.kind === 'MutateConditionally'
|
||||
) {
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: false,
|
||||
kind:
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.push(effect.error);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
}
|
||||
}
|
||||
}
|
||||
const blockPhis = pendingPhis.get(block.id);
|
||||
if (blockPhis != null) {
|
||||
for (const {from, into, index} of blockPhis) {
|
||||
state.assign(index, from, into);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
state.assign(index++, block.terminal.value, fn.returns);
|
||||
}
|
||||
|
||||
if (
|
||||
(block.terminal.kind === 'maybe-throw' ||
|
||||
block.terminal.kind === 'return') &&
|
||||
block.terminal.effects != null
|
||||
) {
|
||||
for (const effect of block.terminal.effects) {
|
||||
if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else {
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
loc: block.terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(state.debug());
|
||||
console.log(pretty(mutations));
|
||||
}
|
||||
for (const mutation of mutations) {
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
mutation.place.identifier,
|
||||
makeInstructionId(mutation.id + 1),
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(pretty([...state.nodes.keys()]));
|
||||
}
|
||||
fn.aliasingEffects ??= [];
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const node = state.nodes.get(place.identifier);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
let mutated = false;
|
||||
if (node.local != null) {
|
||||
if (node.local.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateConditionally',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
} else if (node.local.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (node.transitive != null) {
|
||||
if (node.transitive.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
} else if (node.transitive.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitive',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mutated) {
|
||||
place.effect = Effect.Capture;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 2
|
||||
* Add legacy operand-specific effects based on instruction effects and mutable ranges.
|
||||
* Also fixes up operand mutable ranges, making sure that start is non-zero if the value
|
||||
* is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
|
||||
* to filter spurious mutations of globals, which we now guard against more precisely)
|
||||
*/
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
// TODO: we don't actually set these effects today!
|
||||
phi.place.effect = Effect.Store;
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.effect = isPhiMutatedAfterCreation
|
||||
? Effect.Capture
|
||||
: Effect.Read;
|
||||
}
|
||||
if (
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
/*
|
||||
* TODO: ideally we'd construct a precise start range, but what really
|
||||
* matters is that the phi's range appears mutable (end > start + 1)
|
||||
* so we just set the start to the previous instruction before this block
|
||||
*/
|
||||
const firstInstructionIdOfBlock =
|
||||
block.instructions.at(0)?.id ?? block.terminal.id;
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
firstInstructionIdOfBlock - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
lvalue.effect = Effect.ConditionallyMutate;
|
||||
if (lvalue.identifier.mutableRange.start === 0) {
|
||||
lvalue.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
if (lvalue.identifier.mutableRange.end === 0) {
|
||||
lvalue.identifier.mutableRange.end = makeInstructionId(
|
||||
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
if (instr.effects == null) {
|
||||
continue;
|
||||
}
|
||||
const operandEffects = new Map<IdentifierId, Effect>();
|
||||
for (const effect of instr.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
const isMutatedOrReassigned =
|
||||
effect.into.identifier.mutableRange.end > instr.id;
|
||||
if (isMutatedOrReassigned) {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Capture);
|
||||
operandEffects.set(effect.into.identifier.id, Effect.Store);
|
||||
} else {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Read);
|
||||
operandEffects.set(effect.into.identifier.id, Effect.Store);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction':
|
||||
case 'Create': {
|
||||
break;
|
||||
}
|
||||
case 'Mutate': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Store);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'MutateTransitive':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
operandEffects.set(
|
||||
effect.value.identifier.id,
|
||||
Effect.ConditionallyMutate,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'Freeze': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
// no-op, Read is the default
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind ${(effect as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
const effect =
|
||||
operandEffects.get(lvalue.identifier.id) ??
|
||||
Effect.ConditionallyMutate;
|
||||
lvalue.effect = effect;
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (
|
||||
operand.identifier.mutableRange.end > instr.id &&
|
||||
operand.identifier.mutableRange.start === 0
|
||||
) {
|
||||
operand.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
|
||||
operand.effect = effect;
|
||||
}
|
||||
|
||||
/**
|
||||
* This case is targeted at hoisted functions like:
|
||||
*
|
||||
* ```
|
||||
* x();
|
||||
* function x() { ... }
|
||||
* ```
|
||||
*
|
||||
* Which turns into:
|
||||
*
|
||||
* t0 = DeclareContext HoistedFunction x
|
||||
* t1 = LoadContext x
|
||||
* t2 = CallExpression t1 ( )
|
||||
* t3 = FunctionExpression ...
|
||||
* t4 = StoreContext Function x = t3
|
||||
*
|
||||
* If the function had captured mutable values, it would already have its
|
||||
* range extended to include the StoreContext. But if the function doesn't
|
||||
* capture any mutable values its range won't have been extended yet. We
|
||||
* want to ensure that the value is memoized along with the context variable,
|
||||
* not independently of it (bc of the way we do codegen for hoisted functions).
|
||||
* So here we check for StoreContext rvalues and if they haven't already had
|
||||
* their range extended to at least this instruction, we extend it.
|
||||
*/
|
||||
if (
|
||||
instr.value.kind === 'StoreContext' &&
|
||||
instr.value.value.identifier.mutableRange.end <= instr.id
|
||||
) {
|
||||
instr.value.value.identifier.mutableRange.end = makeInstructionId(
|
||||
instr.id + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
block.terminal.value.effect = isFunctionExpression
|
||||
? Effect.Read
|
||||
: Effect.Freeze;
|
||||
} else {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
for (const effect of fn.aliasingEffects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.push(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Node = {
|
||||
id: Identifier;
|
||||
createdFrom: Map<Identifier, number>;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
| {kind: 'Function'; function: HIRFunction};
|
||||
};
|
||||
class AliasingState {
|
||||
nodes: Map<Identifier, Node> = new Map();
|
||||
|
||||
create(place: Place, value: Node['value']): void {
|
||||
this.nodes.set(place.identifier, {
|
||||
id: place.identifier,
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
createFrom(index: number, from: Place, into: Place): void {
|
||||
this.create(into, {kind: 'Object'});
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.createdFrom.has(from.identifier)) {
|
||||
toNode.createdFrom.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
capture(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
|
||||
if (!toNode.captures.has(from.identifier)) {
|
||||
toNode.captures.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
assign(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.aliases.has(from.identifier)) {
|
||||
toNode.aliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null || node.transitive != null || node.local != null) {
|
||||
continue;
|
||||
}
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutate(
|
||||
index: number,
|
||||
start: Identifier,
|
||||
end: InstructionId,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
|
||||
);
|
||||
}
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
transitive: boolean;
|
||||
direction: 'backwards' | 'forwards';
|
||||
}> = [{place: start, transitive, direction: 'backwards'}];
|
||||
while (queue.length !== 0) {
|
||||
const {place: current, transitive, direction} = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
|
||||
);
|
||||
}
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
if (
|
||||
node.value.kind === 'Function' &&
|
||||
node.transitive == null &&
|
||||
node.local == null
|
||||
) {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
if (transitive) {
|
||||
if (node.transitive == null || node.transitive.kind < kind) {
|
||||
node.transitive = {kind, loc};
|
||||
}
|
||||
} else {
|
||||
if (node.local == null || node.local.kind < kind) {
|
||||
node.local = {kind, loc};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* all mutations affect "forward" edges by the rules:
|
||||
* - Capture a -> b, mutate(a) => mutate(b)
|
||||
* - Alias a -> b, mutate(a) => mutate(b)
|
||||
*/
|
||||
for (const edge of node.edges) {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards'});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive: true, direction: 'backwards'});
|
||||
}
|
||||
if (direction === 'backwards' || node.value.kind !== 'Phi') {
|
||||
/**
|
||||
* all mutations affect backward alias edges by the rules:
|
||||
* - Alias a -> b, mutate(b) => mutate(a)
|
||||
* - Alias a -> b, mutateTransitive(b) => mutate(a)
|
||||
*
|
||||
* However, if we reached a phi because one of its inputs was mutated
|
||||
* (and we're advancing "forwards" through that node's edges), then
|
||||
* we know we've already processed the mutation at its source. The
|
||||
* phi's other inputs can't be affected.
|
||||
*/
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* but only transitive mutations affect captures
|
||||
*/
|
||||
if (transitive) {
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
const nodes = new Map();
|
||||
for (const id of seen) {
|
||||
const node = this.nodes.get(id);
|
||||
nodes.set(id.id, node);
|
||||
}
|
||||
console.log(pretty(nodes));
|
||||
}
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
return pretty(this.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export function pretty(v: any): string {
|
||||
return prettyFormat(v, {
|
||||
plugins: [
|
||||
{
|
||||
test: v =>
|
||||
v !== null && typeof v === 'object' && v.kind === 'Identifier',
|
||||
serialize: v => printPlace(v),
|
||||
},
|
||||
{
|
||||
test: v =>
|
||||
v !== null &&
|
||||
typeof v === 'object' &&
|
||||
typeof v.declarationId === 'number',
|
||||
serialize: v =>
|
||||
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -9,18 +9,24 @@ import {CompilerError} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
Environment,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
Place,
|
||||
computePostDominatorTree,
|
||||
evaluatesToStableTypeOrContainer,
|
||||
getHookKind,
|
||||
isStableType,
|
||||
isStableTypeContainer,
|
||||
isUseOperator,
|
||||
isUseRefType,
|
||||
} from '../HIR';
|
||||
import {PostDominator} from '../HIR/Dominator';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
@@ -31,6 +37,103 @@ import {
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Side map to track and propagate sources of stability (i.e. hook calls such as
|
||||
* `useRef()` and property reads such as `useState()[1]). Note that this
|
||||
* requires forward data flow analysis since stability is not part of React
|
||||
* Compiler's type system.
|
||||
*/
|
||||
class StableSidemap {
|
||||
map: Map<IdentifierId, {isStable: boolean}> = new Map();
|
||||
env: Environment;
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
handleInstruction(instr: Instruction): void {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
switch (value.kind) {
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
/**
|
||||
* Sources of stability are known hook calls
|
||||
*/
|
||||
if (evaluatesToStableTypeOrContainer(this.env, instr)) {
|
||||
if (isStableType(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
} else {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: false,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
|
||||
isUseRefType(lvalue.identifier)
|
||||
) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'Destructure':
|
||||
case 'PropertyLoad': {
|
||||
/**
|
||||
* PropertyLoads may from stable containers may also produce stable
|
||||
* values. ComputedLoads are technically safe for now (as all stable
|
||||
* containers have differently-typed elements), but are not handled as
|
||||
* they should be rare anyways.
|
||||
*/
|
||||
const source =
|
||||
value.kind === 'Destructure'
|
||||
? value.value.identifier.id
|
||||
: value.object.identifier.id;
|
||||
const entry = this.map.get(source);
|
||||
if (entry) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
if (isStableTypeContainer(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: false,
|
||||
});
|
||||
} else if (isStableType(lvalue.identifier)) {
|
||||
this.map.set(lvalue.identifier.id, {
|
||||
isStable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'StoreLocal': {
|
||||
const entry = this.map.get(value.value.identifier.id);
|
||||
if (entry) {
|
||||
this.map.set(lvalue.identifier.id, entry);
|
||||
this.map.set(value.lvalue.place.identifier.id, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'LoadLocal': {
|
||||
const entry = this.map.get(value.place.identifier.id);
|
||||
if (entry) {
|
||||
this.map.set(lvalue.identifier.id, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isStable(id: IdentifierId): boolean {
|
||||
const entry = this.map.get(id);
|
||||
return entry != null ? entry.isStable : false;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Infers which `Place`s are reactive, ie may *semantically* change
|
||||
* over the course of the component/hook's lifetime. Places are reactive
|
||||
@@ -111,6 +214,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
*/
|
||||
export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
|
||||
const stableIdentifierSources = new StableSidemap(fn.env);
|
||||
for (const param of fn.params) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
reactiveIdentifiers.markReactive(place);
|
||||
@@ -184,11 +288,12 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
for (const instruction of block.instructions) {
|
||||
stableIdentifierSources.handleInstruction(instruction);
|
||||
const {value} = instruction;
|
||||
let hasReactiveInput = false;
|
||||
/*
|
||||
* NOTE: we want to mark all operands as reactive or not, so we
|
||||
* avoid short-circuting here
|
||||
* avoid short-circuiting here
|
||||
*/
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
const reactive = reactiveIdentifiers.isReactive(operand);
|
||||
@@ -218,7 +323,13 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
|
||||
if (hasReactiveInput) {
|
||||
for (const lvalue of eachInstructionLValue(instruction)) {
|
||||
if (isStableType(lvalue.identifier)) {
|
||||
/**
|
||||
* Note that it's not correct to mark all stable-typed identifiers
|
||||
* as non-reactive, since ternaries and other value blocks can
|
||||
* produce reactive identifiers typed as these.
|
||||
* (e.g. `props.cond ? setState1 : setState2`)
|
||||
*/
|
||||
if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
|
||||
continue;
|
||||
}
|
||||
reactiveIdentifiers.markReactive(lvalue);
|
||||
@@ -265,6 +376,41 @@ export function inferReactivePlaces(fn: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
} while (reactiveIdentifiers.snapshot());
|
||||
|
||||
function propagateReactivityToInnerFunctions(
|
||||
fn: HIRFunction,
|
||||
isOutermost: boolean,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'ObjectMethod' ||
|
||||
instr.value.kind === 'FunctionExpression'
|
||||
) {
|
||||
propagateReactivityToInnerFunctions(
|
||||
instr.value.loweredFunc.func,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isOutermost) {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
reactiveIdentifiers.isReactive(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate reactivity for inner functions, as we eventually hoist and dedupe
|
||||
* dependency instructions for scopes.
|
||||
*/
|
||||
propagateReactivityToInnerFunctions(fn, true);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
|
||||
import {
|
||||
inferTerminalFunctionEffects,
|
||||
inferInstructionFunctionEffects,
|
||||
@@ -111,7 +111,10 @@ export default function inferReferenceEffects(
|
||||
* Initial state contains function params
|
||||
* TODO: include module declarations here as well
|
||||
*/
|
||||
const initialState = InferenceState.empty(fn.env);
|
||||
const initialState = InferenceState.empty(
|
||||
fn.env,
|
||||
options.isFunctionExpression,
|
||||
);
|
||||
const value: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
loc: fn.loc,
|
||||
@@ -255,6 +258,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;
|
||||
#isFunctionExpression: boolean;
|
||||
|
||||
// The kind of each value, based on its allocation site
|
||||
#values: Map<InstructionValue, AbstractValue>;
|
||||
@@ -267,16 +271,25 @@ class InferenceState {
|
||||
|
||||
constructor(
|
||||
env: Environment,
|
||||
isFunctionExpression: boolean,
|
||||
values: Map<InstructionValue, AbstractValue>,
|
||||
variables: Map<IdentifierId, Set<InstructionValue>>,
|
||||
) {
|
||||
this.env = env;
|
||||
this.#isFunctionExpression = isFunctionExpression;
|
||||
this.#values = values;
|
||||
this.#variables = variables;
|
||||
}
|
||||
|
||||
static empty(env: Environment): InferenceState {
|
||||
return new InferenceState(env, new Map(), new Map());
|
||||
static empty(
|
||||
env: Environment,
|
||||
isFunctionExpression: boolean,
|
||||
): InferenceState {
|
||||
return new InferenceState(env, isFunctionExpression, new Map(), new Map());
|
||||
}
|
||||
|
||||
get isFunctionExpression(): boolean {
|
||||
return this.#isFunctionExpression;
|
||||
}
|
||||
|
||||
// (Re)initializes a @param value with its default @param kind.
|
||||
@@ -394,9 +407,14 @@ class InferenceState {
|
||||
|
||||
freezeValues(values: Set<InstructionValue>, reason: Set<ValueReason>): void {
|
||||
for (const value of values) {
|
||||
if (value.kind === 'DeclareContext') {
|
||||
if (
|
||||
value.kind === 'DeclareContext' ||
|
||||
(value.kind === 'StoreContext' &&
|
||||
(value.lvalue.kind === InstructionKind.Let ||
|
||||
value.lvalue.kind === InstructionKind.Const))
|
||||
) {
|
||||
/**
|
||||
* Avoid freezing hoisted context declarations
|
||||
* Avoid freezing context variable declarations, hoisted or otherwise
|
||||
* function Component() {
|
||||
* const cb = useBar(() => foo(2)); // produces a hoisted context declaration
|
||||
* const foo = useFoo(); // reassigns to the context variable
|
||||
@@ -613,6 +631,7 @@ class InferenceState {
|
||||
} else {
|
||||
return new InferenceState(
|
||||
this.env,
|
||||
this.#isFunctionExpression,
|
||||
nextValues ?? new Map(this.#values),
|
||||
nextVariables ?? new Map(this.#variables),
|
||||
);
|
||||
@@ -627,6 +646,7 @@ class InferenceState {
|
||||
clone(): InferenceState {
|
||||
return new InferenceState(
|
||||
this.env,
|
||||
this.#isFunctionExpression,
|
||||
new Map(this.#values),
|
||||
new Map(this.#variables),
|
||||
);
|
||||
@@ -759,7 +779,7 @@ function inferParam(
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
|
||||
export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
@@ -801,28 +821,16 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `a` is a superset of `b`.
|
||||
*/
|
||||
function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
|
||||
for (const v of b) {
|
||||
if (!a.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mergeAbstractValues(
|
||||
a: AbstractValue,
|
||||
b: AbstractValue,
|
||||
): AbstractValue {
|
||||
const kind = mergeValues(a.kind, b.kind);
|
||||
const kind = mergeValueKinds(a.kind, b.kind);
|
||||
if (
|
||||
kind === a.kind &&
|
||||
kind === b.kind &&
|
||||
isSuperset(a.reason, b.reason) &&
|
||||
isSuperset(a.context, b.context)
|
||||
Set_isSuperset(a.reason, b.reason) &&
|
||||
Set_isSuperset(a.context, b.context)
|
||||
) {
|
||||
return a;
|
||||
}
|
||||
@@ -1591,6 +1599,14 @@ function inferBlock(
|
||||
);
|
||||
|
||||
const lvalue = instr.lvalue;
|
||||
if (instrValue.lvalue.kind !== InstructionKind.Reassign) {
|
||||
state.initialize(instrValue, {
|
||||
kind: ValueKind.Mutable,
|
||||
reason: new Set([ValueReason.Other]),
|
||||
context: new Set(),
|
||||
});
|
||||
state.define(instrValue.lvalue.place, instrValue);
|
||||
}
|
||||
state.alias(lvalue, instrValue.value);
|
||||
lvalue.effect = Effect.Store;
|
||||
continuation = {kind: 'funeffects'};
|
||||
@@ -1781,8 +1797,15 @@ function inferBlock(
|
||||
if (block.terminal.kind === 'return' || block.terminal.kind === 'throw') {
|
||||
if (
|
||||
state.isDefined(operand) &&
|
||||
state.kind(operand).kind === ValueKind.Context
|
||||
((operand.identifier.type.kind === 'Function' &&
|
||||
state.isFunctionExpression) ||
|
||||
state.kind(operand).kind === ValueKind.Context)
|
||||
) {
|
||||
/**
|
||||
* Returned values should only be typed as 'frozen' if they are both (1)
|
||||
* local and (2) not a function expression which may capture and mutate
|
||||
* this function's outer context.
|
||||
*/
|
||||
effect = Effect.ConditionallyMutate;
|
||||
} else {
|
||||
effect = Effect.Freeze;
|
||||
@@ -1954,7 +1977,7 @@ function areArgumentsImmutableAndNonMutating(
|
||||
return true;
|
||||
}
|
||||
|
||||
function getArgumentEffect(
|
||||
export function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
InstructionKind,
|
||||
LabelTerminal,
|
||||
Place,
|
||||
isStatementBlockKind,
|
||||
makeInstructionId,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
@@ -90,100 +91,106 @@ export function inlineImmediatelyInvokedFunctionExpressions(
|
||||
*/
|
||||
const queue = Array.from(fn.body.blocks.values());
|
||||
queue: for (const block of queue) {
|
||||
for (let ii = 0; ii < block.instructions.length; ii++) {
|
||||
const instr = block.instructions[ii]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
functions.set(instr.lvalue.identifier.id, instr.value);
|
||||
/*
|
||||
* We can't handle labels inside expressions yet, so we don't inline IIFEs if they are in an
|
||||
* expression block.
|
||||
*/
|
||||
if (isStatementBlockKind(block.kind)) {
|
||||
for (let ii = 0; ii < block.instructions.length; ii++) {
|
||||
const instr = block.instructions[ii]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'FunctionExpression': {
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
functions.set(instr.lvalue.identifier.id, instr.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CallExpression': {
|
||||
if (instr.value.args.length !== 0) {
|
||||
// We don't support inlining when there are arguments
|
||||
continue;
|
||||
case 'CallExpression': {
|
||||
if (instr.value.args.length !== 0) {
|
||||
// We don't support inlining when there are arguments
|
||||
continue;
|
||||
}
|
||||
const body = functions.get(instr.value.callee.identifier.id);
|
||||
if (body === undefined) {
|
||||
// Not invoking a local function expression, can't inline
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
body.loweredFunc.func.params.length > 0 ||
|
||||
body.loweredFunc.func.async ||
|
||||
body.loweredFunc.func.generator
|
||||
) {
|
||||
// Can't inline functions with params, or async/generator functions
|
||||
continue;
|
||||
}
|
||||
|
||||
// We know this function is used for an IIFE and can prune it later
|
||||
inlinedFunctions.add(instr.value.callee.identifier.id);
|
||||
|
||||
// Create a new block which will contain code following the IIFE call
|
||||
const continuationBlockId = fn.env.nextBlockId;
|
||||
const continuationBlock: BasicBlock = {
|
||||
id: continuationBlockId,
|
||||
instructions: block.instructions.slice(ii + 1),
|
||||
kind: block.kind,
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: block.terminal,
|
||||
};
|
||||
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
||||
|
||||
/*
|
||||
* Trim the original block to contain instructions up to (but not including)
|
||||
* the IIFE
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure we visit the continuation block, since there may have been
|
||||
* sequential IIFEs that need to be visited.
|
||||
*/
|
||||
queue.push(continuationBlock);
|
||||
continue queue;
|
||||
}
|
||||
const body = functions.get(instr.value.callee.identifier.id);
|
||||
if (body === undefined) {
|
||||
// Not invoking a local function expression, can't inline
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
body.loweredFunc.func.params.length > 0 ||
|
||||
body.loweredFunc.func.async ||
|
||||
body.loweredFunc.func.generator
|
||||
) {
|
||||
// Can't inline functions with params, or async/generator functions
|
||||
continue;
|
||||
}
|
||||
|
||||
// We know this function is used for an IIFE and can prune it later
|
||||
inlinedFunctions.add(instr.value.callee.identifier.id);
|
||||
|
||||
// Create a new block which will contain code following the IIFE call
|
||||
const continuationBlockId = fn.env.nextBlockId;
|
||||
const continuationBlock: BasicBlock = {
|
||||
id: continuationBlockId,
|
||||
instructions: block.instructions.slice(ii + 1),
|
||||
kind: block.kind,
|
||||
phis: new Set(),
|
||||
preds: new Set(),
|
||||
terminal: block.terminal,
|
||||
};
|
||||
fn.body.blocks.set(continuationBlockId, continuationBlock);
|
||||
|
||||
/*
|
||||
* Trim the original block to contain instructions up to (but not including)
|
||||
* the IIFE
|
||||
*/
|
||||
block.instructions.length = ii;
|
||||
|
||||
/*
|
||||
* To account for complex control flow within the lambda, we treat the lambda
|
||||
* as if it were a single labeled statement, and replace all returns with gotos
|
||||
* to the label fallthrough.
|
||||
*/
|
||||
const newTerminal: LabelTerminal = {
|
||||
block: body.loweredFunc.func.body.entry,
|
||||
id: makeInstructionId(0),
|
||||
kind: 'label',
|
||||
fallthrough: continuationBlockId,
|
||||
loc: block.terminal.loc,
|
||||
};
|
||||
block.terminal = newTerminal;
|
||||
|
||||
// We store the result in the IIFE temporary
|
||||
const result = instr.lvalue;
|
||||
|
||||
// Declare the IIFE temporary
|
||||
declareTemporary(fn.env, block, result);
|
||||
|
||||
// Promote the temporary with a name as we require this to persist
|
||||
promoteTemporary(result.identifier);
|
||||
|
||||
/*
|
||||
* Rewrite blocks from the lambda to replace any `return` with a
|
||||
* store to the result and `goto` the continuation block
|
||||
*/
|
||||
for (const [id, block] of body.loweredFunc.func.body.blocks) {
|
||||
block.preds.clear();
|
||||
rewriteBlock(fn.env, block, continuationBlockId, result);
|
||||
fn.body.blocks.set(id, block);
|
||||
}
|
||||
|
||||
/*
|
||||
* Ensure we visit the continuation block, since there may have been
|
||||
* sequential IIFEs that need to be visited.
|
||||
*/
|
||||
queue.push(continuationBlock);
|
||||
continue queue;
|
||||
}
|
||||
default: {
|
||||
for (const place of eachInstructionValueOperand(instr.value)) {
|
||||
// Any other use of a function expression means it isn't an IIFE
|
||||
functions.delete(place.identifier.id);
|
||||
default: {
|
||||
for (const place of eachInstructionValueOperand(instr.value)) {
|
||||
// Any other use of a function expression means it isn't an IIFE
|
||||
functions.delete(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -235,6 +242,7 @@ function rewriteBlock(
|
||||
type: null,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
@@ -263,5 +271,6 @@ function declareTemporary(
|
||||
type: null,
|
||||
loc: result.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -327,6 +327,23 @@ function evaluateInstruction(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case '-': {
|
||||
const operand = read(constants, value.value);
|
||||
if (
|
||||
operand !== null &&
|
||||
operand.kind === 'Primitive' &&
|
||||
typeof operand.value === 'number'
|
||||
) {
|
||||
const result: Primitive = {
|
||||
kind: 'Primitive',
|
||||
value: operand.value * -1,
|
||||
loc: value.loc,
|
||||
};
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -492,6 +509,73 @@ function evaluateInstruction(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'TemplateLiteral': {
|
||||
if (value.subexprs.length === 0) {
|
||||
const result: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
value: value.quasis.map(q => q.cooked).join(''),
|
||||
loc: value.loc,
|
||||
};
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (value.subexprs.length !== value.quasis.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.quasis.some(q => q.cooked === undefined)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let quasiIndex = 0;
|
||||
let resultString = value.quasis[quasiIndex].cooked as string;
|
||||
++quasiIndex;
|
||||
|
||||
for (const subExpr of value.subexprs) {
|
||||
const subExprValue = read(constants, subExpr);
|
||||
if (!subExprValue || subExprValue.kind !== 'Primitive') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expressionValue = subExprValue.value;
|
||||
if (
|
||||
typeof expressionValue !== 'number' &&
|
||||
typeof expressionValue !== 'string' &&
|
||||
typeof expressionValue !== 'boolean' &&
|
||||
!(typeof expressionValue === 'object' && expressionValue === null)
|
||||
) {
|
||||
// value is not supported (function, object) or invalid (symbol), or something else
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = value.quasis[quasiIndex].cooked;
|
||||
++quasiIndex;
|
||||
|
||||
if (suffix === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Spec states that concat calls ToString(argument) internally on its parameters
|
||||
* -> we don't have to implement ToString(argument) ourselves and just use the engine implementation
|
||||
* Refs:
|
||||
* - https://tc39.es/ecma262/2024/#sec-tostring
|
||||
* - https://tc39.es/ecma262/2024/#sec-string.prototype.concat
|
||||
* - https://tc39.es/ecma262/2024/#sec-template-literals-runtime-semantics-evaluation
|
||||
*/
|
||||
resultString = resultString.concat(expressionValue as string, suffix);
|
||||
}
|
||||
|
||||
const result: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
value: resultString,
|
||||
loc: value.loc,
|
||||
};
|
||||
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const placeValue = read(constants, value.place);
|
||||
if (placeValue !== null) {
|
||||
|
||||
@@ -151,6 +151,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(varInstruction);
|
||||
@@ -167,6 +168,7 @@ export function inlineJsxTransform(
|
||||
},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(devGlobalInstruction);
|
||||
@@ -220,6 +222,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
thenBlockInstructions.push(reassignElseInstruction);
|
||||
@@ -292,6 +295,7 @@ export function inlineJsxTransform(
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reactElementInstruction);
|
||||
@@ -309,6 +313,7 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reassignConditionalInstruction);
|
||||
@@ -436,6 +441,7 @@ function createSymbolProperty(
|
||||
binding: {kind: 'Global', name: 'Symbol'},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolInstruction);
|
||||
@@ -450,6 +456,7 @@ function createSymbolProperty(
|
||||
property: makePropertyLiteral('for'),
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolForInstruction);
|
||||
@@ -463,6 +470,7 @@ function createSymbolProperty(
|
||||
value: symbolName,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolValueInstruction);
|
||||
@@ -478,6 +486,7 @@ function createSymbolProperty(
|
||||
args: [symbolValueInstruction.lvalue],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
const $$typeofProperty: ObjectProperty = {
|
||||
@@ -508,6 +517,7 @@ function createTagProperty(
|
||||
value: componentTag.name,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
tagProperty = {
|
||||
@@ -634,6 +644,7 @@ function createPropsProperties(
|
||||
elements: [...children],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(childrenPropInstruction);
|
||||
@@ -657,6 +668,7 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
refProperty = {
|
||||
@@ -678,6 +690,7 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
keyProperty = {
|
||||
@@ -711,6 +724,7 @@ function createPropsProperties(
|
||||
properties: props,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: instr.loc,
|
||||
};
|
||||
propsProperty = {
|
||||
|
||||
@@ -146,6 +146,7 @@ function emitLoadLoweredContextCallee(
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: null,
|
||||
value: loadGlobal,
|
||||
};
|
||||
}
|
||||
@@ -192,6 +193,7 @@ function emitPropertyLoad(
|
||||
lvalue: object,
|
||||
value: loadObj,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
@@ -206,6 +208,7 @@ function emitPropertyLoad(
|
||||
lvalue: element,
|
||||
value: loadProp,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
@@ -237,6 +240,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
@@ -250,6 +254,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
@@ -278,6 +283,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return fnInstr;
|
||||
@@ -294,6 +300,7 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
|
||||
id: makeInstructionId(0),
|
||||
value: array,
|
||||
lvalue: arrayLvalue,
|
||||
effects: null,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return arrayInstr;
|
||||
|
||||
@@ -297,6 +297,7 @@ function emitOutlinedJsx(
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
|
||||
const jsxExpr: Instruction = {
|
||||
@@ -312,6 +313,7 @@ function emitOutlinedJsx(
|
||||
openingLoc: GeneratedSource,
|
||||
closingLoc: GeneratedSource,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
|
||||
return [loadJsx, jsxExpr];
|
||||
@@ -353,6 +355,7 @@ function emitOutlinedFn(
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: instructions.at(-1)!.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
@@ -366,6 +369,7 @@ function emitOutlinedFn(
|
||||
params: [propsObj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
@@ -517,6 +521,7 @@ function emitDestructureProps(
|
||||
loc: GeneratedSource,
|
||||
value: propsObj,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
return destructurePropsInstr;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
getHookKind,
|
||||
makeIdentifierName,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
|
||||
@@ -1000,6 +1000,14 @@ function codegenTerminal(
|
||||
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support non-trivial for..in inits',
|
||||
description: null,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
@@ -1092,6 +1100,14 @@ function codegenTerminal(
|
||||
lval = codegenLValue(cx, iterableItem.value.lvalue.pattern);
|
||||
break;
|
||||
}
|
||||
case 'StoreContext': {
|
||||
CompilerError.throwTodo({
|
||||
reason: 'Support non-trivial for..of inits',
|
||||
description: null,
|
||||
loc: terminal.init.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
|
||||
@@ -1294,7 +1310,7 @@ function codegenInstructionNullable(
|
||||
});
|
||||
CompilerError.invariant(value?.type === 'FunctionExpression', {
|
||||
reason: 'Expected a function as a function declaration value',
|
||||
description: null,
|
||||
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
@@ -1710,7 +1726,7 @@ function codegenInstructionValue(
|
||||
}
|
||||
case 'UnaryExpression': {
|
||||
value = t.unaryExpression(
|
||||
instrValue.operator as 'throw', // todo
|
||||
instrValue.operator,
|
||||
codegenPlaceToExpression(cx, instrValue.value),
|
||||
);
|
||||
break;
|
||||
@@ -2311,9 +2327,12 @@ function codegenInstructionValue(
|
||||
* u0080 to u009F: C1 control codes
|
||||
* u00A0 to uFFFF: All non-basic Latin characters
|
||||
* https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
|
||||
*
|
||||
* u010000 to u10FFFF: Astral plane characters
|
||||
* https://mathiasbynens.be/notes/javascript-unicode
|
||||
*/
|
||||
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
|
||||
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}]|"|\\/u;
|
||||
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u;
|
||||
function codegenJsxAttribute(
|
||||
cx: Context,
|
||||
attribute: JsxAttribute,
|
||||
@@ -2563,7 +2582,16 @@ function codegenValue(
|
||||
value: boolean | number | string | null | undefined,
|
||||
): t.Expression {
|
||||
if (typeof value === 'number') {
|
||||
return t.numericLiteral(value);
|
||||
if (value < 0) {
|
||||
/**
|
||||
* Babel's code generator produces invalid JS for negative numbers when
|
||||
* run with { compact: true }.
|
||||
* See repro https://codesandbox.io/p/devbox/5d47fr
|
||||
*/
|
||||
return t.unaryExpression('-', t.numericLiteral(-value), false);
|
||||
} else {
|
||||
return t.numericLiteral(value);
|
||||
}
|
||||
} else if (typeof value === 'boolean') {
|
||||
return t.booleanLiteral(value);
|
||||
} else if (typeof value === 'string') {
|
||||
|
||||
@@ -456,6 +456,7 @@ function canMergeScopes(
|
||||
new Set(
|
||||
[...current.scope.declarations.values()].map(declaration => ({
|
||||
identifier: declaration.identifier,
|
||||
reactive: true,
|
||||
path: [],
|
||||
})),
|
||||
),
|
||||
|
||||
@@ -255,6 +255,12 @@ function writeReactiveValue(writer: Writer, value: ReactiveValue): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function printReactiveTerminal(terminal: ReactiveTerminal): string {
|
||||
const writer = new Writer();
|
||||
writeTerminal(writer, terminal);
|
||||
return writer.complete();
|
||||
}
|
||||
|
||||
function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
|
||||
switch (terminal.kind) {
|
||||
case 'break': {
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
DeclarationId,
|
||||
convertHoistedLValueKind,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
Place,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
} from '../HIR';
|
||||
import {empty, Stack} from '../Utils/Stack';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
@@ -22,138 +27,144 @@ import {
|
||||
/*
|
||||
* Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
|
||||
* original instruction kind.
|
||||
*
|
||||
* Also detects and bails out on context variables which are:
|
||||
* - function declarations, which are hoisted by JS engines to the nearest block scope
|
||||
* - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
|
||||
* - declared
|
||||
*
|
||||
* This is because React Compiler converts a `function foo()` function declaration to
|
||||
* 1. a `let foo;` declaration before reactive memo blocks
|
||||
* 2. a `foo = function foo() {}` assignment within the block
|
||||
*
|
||||
* This means references before the assignment are invalid (see fixture
|
||||
* error.todo-functiondecl-hoisting)
|
||||
*/
|
||||
export function pruneHoistedContexts(fn: ReactiveFunction): void {
|
||||
const hoistedIdentifiers: HoistedIdentifiers = new Map();
|
||||
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
|
||||
visitReactiveFunction(fn, new Visitor(), {
|
||||
activeScopes: empty(),
|
||||
uninitialized: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
|
||||
'REWRITTEN_HOISTED_CONST',
|
||||
);
|
||||
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
|
||||
type VisitorState = {
|
||||
activeScopes: Stack<Set<IdentifierId>>;
|
||||
uninitialized: Map<
|
||||
IdentifierId,
|
||||
| {
|
||||
kind: 'unknown-kind';
|
||||
}
|
||||
| {
|
||||
kind: 'func';
|
||||
definition: Place | null;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type HoistedIdentifiers = Map<
|
||||
DeclarationId,
|
||||
| InstructionKind
|
||||
| typeof REWRITTEN_HOISTED_CONST
|
||||
| typeof REWRITTEN_HOISTED_LET
|
||||
>;
|
||||
|
||||
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
class Visitor extends ReactiveFunctionTransform<VisitorState> {
|
||||
override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
|
||||
state.activeScopes = state.activeScopes.push(
|
||||
new Set(scope.scope.declarations.keys()),
|
||||
);
|
||||
/**
|
||||
* Add declared but not initialized / assigned variables. This may include
|
||||
* function declarations that escape the memo block.
|
||||
*/
|
||||
for (const decl of scope.scope.declarations.values()) {
|
||||
state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});
|
||||
}
|
||||
this.traverseScope(scope, state);
|
||||
state.activeScopes.pop();
|
||||
for (const decl of scope.scope.declarations.values()) {
|
||||
state.uninitialized.delete(decl.identifier.id);
|
||||
}
|
||||
}
|
||||
override visitPlace(
|
||||
_id: InstructionId,
|
||||
place: Place,
|
||||
state: VisitorState,
|
||||
): void {
|
||||
const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
|
||||
if (
|
||||
maybeHoistedFn?.kind === 'func' &&
|
||||
maybeHoistedFn.definition !== place
|
||||
) {
|
||||
CompilerError.throwTodo({
|
||||
reason: '[PruneHoistedContexts] Rewrite hoisted function references',
|
||||
loc: place.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
override transformInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: HoistedIdentifiers,
|
||||
state: VisitorState,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitInstruction(instruction, state);
|
||||
|
||||
/**
|
||||
* Remove hoisted declarations to preserve TDZ
|
||||
*/
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedConst'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Const,
|
||||
if (instruction.value.kind === 'DeclareContext') {
|
||||
const maybeNonHoisted = convertHoistedLValueKind(
|
||||
instruction.value.lvalue.kind,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
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') {
|
||||
const kind = state.get(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
);
|
||||
if (kind != null) {
|
||||
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
|
||||
reason: 'Expected exactly one store to a hoisted const variable',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
if (maybeNonHoisted != null) {
|
||||
if (
|
||||
kind === InstructionKind.Const ||
|
||||
kind === InstructionKind.Function
|
||||
maybeNonHoisted === InstructionKind.Function &&
|
||||
state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
REWRITTEN_HOISTED_CONST,
|
||||
);
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'instruction',
|
||||
instruction: {
|
||||
...instruction,
|
||||
value: {
|
||||
...instruction.value,
|
||||
lvalue: {
|
||||
...instruction.value.lvalue,
|
||||
kind,
|
||||
},
|
||||
type: null,
|
||||
kind: 'StoreLocal',
|
||||
},
|
||||
},
|
||||
state.uninitialized.set(
|
||||
instruction.value.lvalue.place.identifier.id,
|
||||
{
|
||||
kind: 'func',
|
||||
definition: null,
|
||||
},
|
||||
};
|
||||
} 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: 'remove'};
|
||||
}
|
||||
}
|
||||
if (
|
||||
instruction.value.kind === 'StoreContext' &&
|
||||
instruction.value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
/**
|
||||
* Rewrite StoreContexts let/const that will be pre-declared in
|
||||
* codegen to reassignments.
|
||||
*/
|
||||
const lvalueId = instruction.value.lvalue.place.identifier.id;
|
||||
const isDeclaredByScope = state.activeScopes.find(scope =>
|
||||
scope.has(lvalueId),
|
||||
);
|
||||
if (isDeclaredByScope) {
|
||||
if (
|
||||
instruction.value.lvalue.kind === InstructionKind.Let ||
|
||||
instruction.value.lvalue.kind === InstructionKind.Const
|
||||
) {
|
||||
instruction.value.lvalue.kind = InstructionKind.Reassign;
|
||||
} else if (instruction.value.lvalue.kind === InstructionKind.Function) {
|
||||
const maybeHoistedFn = state.uninitialized.get(lvalueId);
|
||||
if (maybeHoistedFn != null) {
|
||||
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
|
||||
reason: '[PruneHoistedContexts] Unexpected hoisted function',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
maybeHoistedFn.definition = instruction.value.lvalue.place;
|
||||
/**
|
||||
* References to hoisted functions are now "safe" as variable assignments
|
||||
* have finished.
|
||||
*/
|
||||
state.uninitialized.delete(lvalueId);
|
||||
}
|
||||
} else {
|
||||
CompilerError.throwTodo({
|
||||
reason: '[PruneHoistedContexts] Unexpected kind',
|
||||
description: `(${instruction.value.lvalue.kind})`,
|
||||
loc: instruction.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.visitInstruction(instruction, state);
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -436,6 +436,7 @@ function makeLoadUseFireInstruction(
|
||||
value: instrValue,
|
||||
lvalue: {...useFirePlace},
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -460,6 +461,7 @@ function makeLoadFireCalleeInstruction(
|
||||
},
|
||||
lvalue: {...loadedFireCallee},
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -483,6 +485,7 @@ function makeCallUseFireInstruction(
|
||||
value: useFireCall,
|
||||
lvalue: {...useFireCallResultPlace},
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -511,6 +514,7 @@ function makeStoreUseFireInstruction(
|
||||
},
|
||||
lvalue: fireFunctionBindingLValuePlace,
|
||||
loc: GeneratedSource,
|
||||
effects: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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 {fromZodError} from 'zod-validation-error';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
defaultOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
} from '../Entrypoint';
|
||||
import {EnvironmentConfig} from '..';
|
||||
import {
|
||||
EnvironmentConfigSchema,
|
||||
PartialEnvironmentConfig,
|
||||
} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from './Result';
|
||||
import {hasOwnProperty} from './utils';
|
||||
|
||||
function tryParseTestPragmaValue(val: string): Result<unknown, unknown> {
|
||||
try {
|
||||
let parsedVal: unknown;
|
||||
const stringMatch = /^"([^"]*)"$/.exec(val);
|
||||
if (stringMatch && stringMatch.length > 1) {
|
||||
parsedVal = stringMatch[1];
|
||||
} else {
|
||||
parsedVal = JSON.parse(val);
|
||||
}
|
||||
return Ok(parsedVal);
|
||||
} catch (e) {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
validateNoCapitalizedCalls: [],
|
||||
enableChangeDetectionForDebugging: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$structuralCheck',
|
||||
},
|
||||
enableEmitFreeze: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'makeReadOnly',
|
||||
},
|
||||
enableEmitInstrumentForget: {
|
||||
fn: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useRenderCounter',
|
||||
},
|
||||
gating: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'shouldInstrument',
|
||||
},
|
||||
globalGating: 'DEV',
|
||||
},
|
||||
enableEmitHookGuards: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: '$dispatcherGuard',
|
||||
},
|
||||
inlineJsxTransform: {
|
||||
elementSymbol: 'react.transitional.element',
|
||||
globalDevVar: 'DEV',
|
||||
},
|
||||
lowerContextAccess: {
|
||||
source: 'react-compiler-runtime',
|
||||
importSpecifierName: 'useContext_withSelector',
|
||||
},
|
||||
inferEffectDependencies: [
|
||||
{
|
||||
function: {
|
||||
source: 'react',
|
||||
importSpecifierName: 'useEffect',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'shared-runtime',
|
||||
importSpecifierName: 'useSpecialEffect',
|
||||
},
|
||||
numRequiredArgs: 2,
|
||||
},
|
||||
{
|
||||
function: {
|
||||
source: 'useEffectWrapper',
|
||||
importSpecifierName: 'default',
|
||||
},
|
||||
numRequiredArgs: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function* splitPragma(
|
||||
pragma: string,
|
||||
): Generator<{key: string; value: string | null}> {
|
||||
for (const entry of pragma.split('@')) {
|
||||
const keyVal = entry.trim();
|
||||
const valIdx = keyVal.indexOf(':');
|
||||
if (valIdx === -1) {
|
||||
yield {key: keyVal.split(' ', 1)[0], value: null};
|
||||
} else {
|
||||
yield {key: keyVal.slice(0, valIdx), value: keyVal.slice(valIdx + 1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
|
||||
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
continue;
|
||||
}
|
||||
const isSet = val == null || val === 'true';
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] = testComplexConfigDefaults[key];
|
||||
} else if (isSet) {
|
||||
maybeConfig[key] = true;
|
||||
} else if (val === 'false') {
|
||||
maybeConfig[key] = false;
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
const valSplit = parsedVal.split('.');
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
props.push({type: 'wildcard'});
|
||||
} else if (elt.length > 0) {
|
||||
props.push({type: 'name', name: elt});
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
}
|
||||
}
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
if (config.success) {
|
||||
/**
|
||||
* Unless explicitly enabled, do not insert HMR handling code
|
||||
* in test fixtures or playground to reduce visual noise.
|
||||
*/
|
||||
if (config.data.enableResetCacheOnSourceFileChanges == null) {
|
||||
config.data.enableResetCacheOnSourceFileChanges = false;
|
||||
}
|
||||
return config.data;
|
||||
}
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Internal error, could not parse config from pragma string',
|
||||
description: `${fromZodError(config.error)}`,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
},
|
||||
};
|
||||
export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
compilationMode: CompilationMode;
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
for (const {key, value: val} of splitPragma(pragma)) {
|
||||
if (!hasOwnProperty(defaultOptions, key)) {
|
||||
continue;
|
||||
}
|
||||
const isSet = val == null || val === 'true';
|
||||
if (isSet && key in testComplexPluginOptionDefaults) {
|
||||
options[key] = testComplexPluginOptionDefaults[key];
|
||||
} else if (isSet) {
|
||||
options[key] = true;
|
||||
} else if (val === 'false') {
|
||||
options[key] = false;
|
||||
} else if (val != null) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'target' && parsedVal === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: parsedVal,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = parsedVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
}
|
||||
@@ -121,6 +121,21 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `a` is a superset of `b`.
|
||||
*/
|
||||
export function Set_isSuperset<T>(
|
||||
a: ReadonlySet<T>,
|
||||
b: ReadonlySet<T>,
|
||||
): boolean {
|
||||
for (const v of b) {
|
||||
if (!a.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function Iterable_some<T>(
|
||||
iter: Iterable<T>,
|
||||
pred: (item: T) => boolean,
|
||||
|
||||
@@ -452,7 +452,7 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
|
||||
reason:
|
||||
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',
|
||||
loc: callee.loc,
|
||||
description: `Cannot call ${hookKind} within a function component`,
|
||||
description: `Cannot call ${hookKind === 'Custom' ? 'hook' : hookKind} within a function expression`,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, Effect, ErrorSeverity} from '..';
|
||||
import {
|
||||
FunctionEffect,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
isMutableEffect,
|
||||
isRefOrRefLikeMutableType,
|
||||
Place,
|
||||
} from '../HIR';
|
||||
import {
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {Result} from '../Utils/Result';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
|
||||
/**
|
||||
* Validates that functions with known mutations (ie due to types) cannot be passed
|
||||
* where a frozen value is expected. Example:
|
||||
*
|
||||
* ```
|
||||
* function Component() {
|
||||
* const cache = new Map();
|
||||
* const onClick = () => {
|
||||
* cache.set(...);
|
||||
* }
|
||||
* useHook(onClick); // ERROR: cannot pass a mutable value
|
||||
* return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
|
||||
* variables. But unlike other mutables values like an array, the receiver of the function has
|
||||
* no way to avoid mutation — for example, a function can receive an array and choose not to mutate
|
||||
* it, but there's no way to know that a function is mutable and avoid calling it.
|
||||
*
|
||||
* This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
|
||||
* that are passed where a frozen value is expected and rejects them.
|
||||
*/
|
||||
export function validateNoFreezingKnownMutableFunctions(
|
||||
fn: HIRFunction,
|
||||
): Result<void, CompilerError> {
|
||||
const errors = new CompilerError();
|
||||
const contextMutationEffects: Map<
|
||||
IdentifierId,
|
||||
Extract<FunctionEffect, {kind: 'ContextMutation'}>
|
||||
> = new Map();
|
||||
|
||||
function visitOperand(operand: Place): void {
|
||||
if (operand.effect === Effect.Freeze) {
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
errors.push({
|
||||
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
||||
loc: operand.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
errors.push({
|
||||
reason: `The function modifies a local variable here`,
|
||||
loc: effect.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const instr of block.instructions) {
|
||||
const {lvalue, value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'LoadLocal': {
|
||||
const effect = contextMutationEffects.get(value.place.identifier.id);
|
||||
if (effect != null) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const effect = contextMutationEffects.get(value.value.identifier.id);
|
||||
if (effect != null) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, effect);
|
||||
contextMutationEffects.set(
|
||||
value.lvalue.place.identifier.id,
|
||||
effect,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression': {
|
||||
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
|
||||
effect => {
|
||||
return (
|
||||
effect.kind === 'ContextMutation' &&
|
||||
(effect.effect === Effect.Store ||
|
||||
effect.effect === Effect.Mutate) &&
|
||||
Iterable_some(effect.places, place => {
|
||||
return (
|
||||
isMutableEffect(place.effect, place.loc) &&
|
||||
!isRefOrRefLikeMutableType(place.identifier.type)
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
effects: for (const effect of value.loweredFunc.func
|
||||
.aliasingEffects) {
|
||||
switch (effect.kind) {
|
||||
case 'Mutate':
|
||||
case 'MutateTransitive': {
|
||||
const knownMutation = contextMutationEffects.get(
|
||||
effect.value.identifier.id,
|
||||
);
|
||||
if (knownMutation != null) {
|
||||
contextMutationEffects.set(
|
||||
lvalue.identifier.id,
|
||||
knownMutation,
|
||||
);
|
||||
} else if (
|
||||
context.has(effect.value.identifier.id) &&
|
||||
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
||||
) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, {
|
||||
kind: 'ContextMutation',
|
||||
effect: Effect.Mutate,
|
||||
loc: effect.value.loc,
|
||||
places: new Set([effect.value]),
|
||||
});
|
||||
break effects;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
const knownMutation = contextMutationEffects.get(
|
||||
effect.value.identifier.id,
|
||||
);
|
||||
if (knownMutation != null) {
|
||||
contextMutationEffects.set(
|
||||
lvalue.identifier.id,
|
||||
knownMutation,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
visitOperand(operand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
visitOperand(operand);
|
||||
}
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
@@ -141,14 +141,14 @@ function getCompareDependencyResultDescription(
|
||||
): string {
|
||||
switch (result) {
|
||||
case CompareDependencyResult.Ok:
|
||||
return 'dependencies equal';
|
||||
return 'Dependencies equal';
|
||||
case CompareDependencyResult.RootDifference:
|
||||
case CompareDependencyResult.PathDifference:
|
||||
return 'inferred different dependency than source';
|
||||
return 'Inferred different dependency than source';
|
||||
case CompareDependencyResult.RefAccessDifference:
|
||||
return 'differences in ref.current access';
|
||||
return 'Differences in ref.current access';
|
||||
case CompareDependencyResult.Subpath:
|
||||
return 'inferred less specific property than source';
|
||||
return 'Inferred less specific property than source';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,17 +279,20 @@ function validateInferredDep(
|
||||
severity: ErrorSeverity.CannotPreserveMemoization,
|
||||
reason:
|
||||
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected',
|
||||
description: DEBUG
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. Detail: ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'none'
|
||||
}`
|
||||
: null,
|
||||
description:
|
||||
DEBUG ||
|
||||
// If the dependency is a named variable then we can report it. Otherwise only print in debug mode
|
||||
(dep.identifier.name != null && dep.identifier.name.kind === 'named')
|
||||
? `The inferred dependency was \`${prettyPrintScopeDependency(
|
||||
dep,
|
||||
)}\`, but the source dependencies were [${validDepsInMemoBlock
|
||||
.map(dep => printManualMemoDependency(dep, true))
|
||||
.join(', ')}]. ${
|
||||
errorDiagnostic
|
||||
? getCompareDependencyResultDescription(errorDiagnostic)
|
||||
: 'Inferred dependency not present in source'
|
||||
}`
|
||||
: null,
|
||||
loc: memoLocation,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ export {validateContextVariableLValues} from './ValidateContextVariableLValues';
|
||||
export {validateHooksUsage} from './ValidateHooksUsage';
|
||||
export {validateMemoizedEffectDependencies} from './ValidateMemoizedEffectDependencies';
|
||||
export {validateNoCapitalizedCalls} from './ValidateNoCapitalizedCalls';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccesInRender';
|
||||
export {validateNoRefAccessInRender} from './ValidateNoRefAccessInRender';
|
||||
export {validateNoSetStateInRender} from './ValidateNoSetStateInRender';
|
||||
export {validatePreservedManualMemoization} from './ValidatePreservedManualMemoization';
|
||||
export {validateUseMemo} from './ValidateUseMemo';
|
||||
|
||||
@@ -175,21 +175,14 @@ import {
|
||||
* and mutability.
|
||||
*/
|
||||
function Component(t0) {
|
||||
const $ = _c(4);
|
||||
const $ = _c(2);
|
||||
const { prop } = t0;
|
||||
let t1;
|
||||
if ($[0] !== prop) {
|
||||
const obj = shallowCopy(prop);
|
||||
const aliasedObj = identity(obj);
|
||||
let t2;
|
||||
if ($[2] !== obj) {
|
||||
t2 = [obj.id];
|
||||
$[2] = obj;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const id = t2;
|
||||
|
||||
const id = [obj.id];
|
||||
|
||||
mutate(aliasedObj);
|
||||
setPropertyByKey(aliasedObj, "id", prop.id + 1);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
@@ -15,7 +15,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
const Test = () => {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Repro(props) {
|
||||
const MY_CONST = -2;
|
||||
return <Stringify>{props.arg - MY_CONST}</Stringify>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Repro,
|
||||
params: [
|
||||
{
|
||||
arg: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
function Repro(props) {
|
||||
const $ = _c(2);
|
||||
|
||||
const t0 = props.arg - -2;
|
||||
let t1;
|
||||
if ($[0] !== t0) {
|
||||
t1 = <Stringify>{t0}</Stringify>;
|
||||
$[0] = t0;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Repro,
|
||||
params: [
|
||||
{
|
||||
arg: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"children":5}</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
function Repro(props) {
|
||||
const MY_CONST = -2;
|
||||
return <Stringify>{props.arg - MY_CONST}</Stringify>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Repro,
|
||||
params: [
|
||||
{
|
||||
arg: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -56,7 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { makeArray, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo({value}) {
|
||||
const result = bar();
|
||||
function bar() {
|
||||
return {value};
|
||||
}
|
||||
return <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{value: 2}],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify } from "shared-runtime";
|
||||
|
||||
/**
|
||||
* Fixture currently fails with
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) <div>{"result":{"value":2},"fn":{"kind":"Function","result":{"value":2}},"shouldInvokeFns":true}</div>
|
||||
* Forget:
|
||||
* (kind: exception) bar is not a function
|
||||
*/
|
||||
function Foo(t0) {
|
||||
const $ = _c(6);
|
||||
const { value } = t0;
|
||||
let bar;
|
||||
let result;
|
||||
if ($[0] !== value) {
|
||||
result = bar();
|
||||
bar = function bar() {
|
||||
return { value };
|
||||
};
|
||||
$[0] = value;
|
||||
$[1] = bar;
|
||||
$[2] = result;
|
||||
} else {
|
||||
bar = $[1];
|
||||
result = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== bar || $[4] !== result) {
|
||||
t1 = <Stringify result={result} fn={bar} shouldInvokeFns={true} />;
|
||||
$[3] = bar;
|
||||
$[4] = result;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Foo,
|
||||
params: [{ value: 2 }],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const count = useRef(0);
|
||||
const updateCountPostfix = () => {
|
||||
const id = count.current++;
|
||||
return id;
|
||||
};
|
||||
const updateCountPrefix = () => {
|
||||
const id = ++count.current;
|
||||
return id;
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = updateCountPostfix();
|
||||
console.log(`id = ${id}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
}, []);
|
||||
return {count, updateCountPostfix, updateCountPrefix};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const $ = _c(5);
|
||||
const count = useRef(0);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = () => {
|
||||
count.current = count.current + 1;
|
||||
const id = count.current;
|
||||
return id;
|
||||
};
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const updateCountPostfix = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => {
|
||||
const id_0 = (count.current = count.current + 1);
|
||||
return id_0;
|
||||
};
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const updateCountPrefix = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
const id_1 = updateCountPostfix();
|
||||
console.log(`id = ${id_1}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
};
|
||||
t3 = [];
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = { count, updateCountPostfix, updateCountPrefix };
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {useRef, useEffect} from 'react';
|
||||
|
||||
/**
|
||||
* The postfix increment operator should return the value before incrementing.
|
||||
* ```js
|
||||
* const id = count.current; // 0
|
||||
* count.current = count.current + 1; // 1
|
||||
* return id;
|
||||
* ```
|
||||
* The bug is that we currently increment the value before the expression is evaluated.
|
||||
* This bug does not trigger when the incremented value is a plain primitive.
|
||||
*
|
||||
* Found differences in evaluator results
|
||||
* Non-forget (expected):
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 0','count = 1']
|
||||
* Forget:
|
||||
* (kind: ok) {"count":{"current":0},"updateCountPostfix":"[[ function params=0 ]]","updateCountPrefix":"[[ function params=0 ]]"}
|
||||
* logs: ['id = 1','count = 1']
|
||||
*/
|
||||
function useFoo() {
|
||||
const count = useRef(0);
|
||||
const updateCountPostfix = () => {
|
||||
const id = count.current++;
|
||||
return id;
|
||||
};
|
||||
const updateCountPrefix = () => {
|
||||
const id = ++count.current;
|
||||
return id;
|
||||
};
|
||||
useEffect(() => {
|
||||
const id = updateCountPostfix();
|
||||
console.log(`id = ${id}`);
|
||||
console.log(`count = ${count.current}`);
|
||||
}, []);
|
||||
return {count, updateCountPostfix, updateCountPrefix};
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: useFoo,
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
const Codes = {
|
||||
en: {name: 'English'},
|
||||
ja: {name: 'Japanese'},
|
||||
ko: {name: 'Korean'},
|
||||
zh: {name: 'Chinese'},
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
let keys;
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const options = keys.map(code => {
|
||||
const country = Codes[code];
|
||||
return {
|
||||
name: country.name,
|
||||
code,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: false}],
|
||||
sequentialRenders: [
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: true},
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: false},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
const Codes = {
|
||||
en: { name: "English" },
|
||||
ja: { name: "Japanese" },
|
||||
ko: { name: "Korean" },
|
||||
zh: { name: "Chinese" },
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
const $ = _c(4);
|
||||
let keys;
|
||||
if (a) {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = Object.keys(Codes);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
keys = t0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = keys.map(_temp);
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const options = t0;
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = (
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = (
|
||||
<>
|
||||
{t1}
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
function _temp(code) {
|
||||
const country = Codes[code];
|
||||
return { name: country.name, code };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: false }],
|
||||
sequentialRenders: [
|
||||
{ a: false },
|
||||
{ a: true },
|
||||
{ a: true },
|
||||
{ a: false },
|
||||
{ a: true },
|
||||
{ a: false },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
const Codes = {
|
||||
en: {name: 'English'},
|
||||
ja: {name: 'Japanese'},
|
||||
ko: {name: 'Korean'},
|
||||
zh: {name: 'Chinese'},
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
let keys;
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const options = keys.map(code => {
|
||||
const country = Codes[code];
|
||||
return {
|
||||
name: country.name,
|
||||
code,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: false}],
|
||||
sequentialRenders: [
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: true},
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: false},
|
||||
],
|
||||
};
|
||||
@@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function bar(a) {
|
||||
const $ = _c(2);
|
||||
let y;
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
const x = [a];
|
||||
t0 = [a];
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0;
|
||||
let y;
|
||||
if ($[2] !== x[0][1]) {
|
||||
y = {};
|
||||
|
||||
y = x[0][1];
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
$[2] = x[0][1];
|
||||
$[3] = y;
|
||||
} else {
|
||||
y = $[1];
|
||||
y = $[3];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -29,21 +29,29 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function bar(a, b) {
|
||||
const $ = _c(3);
|
||||
let y;
|
||||
const $ = _c(6);
|
||||
let t0;
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [a, b];
|
||||
t0 = [a, b];
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
const x = t0;
|
||||
let y;
|
||||
if ($[3] !== x[0][1] || $[4] !== x[1][0]) {
|
||||
y = {};
|
||||
let t;
|
||||
t = {};
|
||||
let t = {};
|
||||
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
$[0] = a;
|
||||
$[1] = b;
|
||||
$[2] = y;
|
||||
$[3] = x[0][1];
|
||||
$[4] = x[1][0];
|
||||
$[5] = y;
|
||||
} else {
|
||||
y = $[2];
|
||||
y = $[5];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -35,8 +35,7 @@ function bar(a, b) {
|
||||
if ($[0] !== a || $[1] !== b) {
|
||||
const x = [a, b];
|
||||
y = {};
|
||||
let t;
|
||||
t = {};
|
||||
let t = {};
|
||||
const f0 = function () {
|
||||
y = x[0][1];
|
||||
t = x[1][0];
|
||||
|
||||
@@ -25,17 +25,25 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function bar(a) {
|
||||
const $ = _c(2);
|
||||
let y;
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
const x = [a];
|
||||
t0 = [a];
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0;
|
||||
let y;
|
||||
if ($[2] !== x[0].a[1]) {
|
||||
y = {};
|
||||
|
||||
y = x[0].a[1];
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
$[2] = x[0].a[1];
|
||||
$[3] = y;
|
||||
} else {
|
||||
y = $[1];
|
||||
y = $[3];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -24,17 +24,25 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
function bar(a) {
|
||||
const $ = _c(2);
|
||||
let y;
|
||||
const $ = _c(4);
|
||||
let t0;
|
||||
if ($[0] !== a) {
|
||||
const x = [a];
|
||||
t0 = [a];
|
||||
$[0] = a;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const x = t0;
|
||||
let y;
|
||||
if ($[2] !== x[0]) {
|
||||
y = {};
|
||||
|
||||
y = x[0];
|
||||
$[0] = a;
|
||||
$[1] = y;
|
||||
$[2] = x[0];
|
||||
$[3] = y;
|
||||
} else {
|
||||
y = $[1];
|
||||
y = $[3];
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
@@ -22,7 +22,7 @@ class Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode(infer)
|
||||
// @compilationMode:"infer"
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -33,8 +33,7 @@ function useTest() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
let w;
|
||||
w = {};
|
||||
let w = {};
|
||||
|
||||
const t1 = (w = 42);
|
||||
const t2 = w;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
@@ -24,7 +24,7 @@ function Foo(props) {
|
||||
|
||||
```javascript
|
||||
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user