Compare commits
3 Commits
gh/mofeiZ/
...
sebbie/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
027534e673 | ||
|
|
f23fc95209 | ||
|
|
07b977b734 |
@@ -23,7 +23,6 @@ module.exports = {
|
||||
'babel',
|
||||
'ft-flow',
|
||||
'jest',
|
||||
'es',
|
||||
'no-for-of-loops',
|
||||
'no-function-declare-after-return',
|
||||
'react',
|
||||
@@ -48,7 +47,7 @@ module.exports = {
|
||||
'ft-flow/no-unused-expressions': ERROR,
|
||||
// 'ft-flow/no-weak-types': WARNING,
|
||||
// 'ft-flow/require-valid-file-annotation': ERROR,
|
||||
'es/no-optional-chaining': ERROR,
|
||||
|
||||
'no-cond-assign': OFF,
|
||||
'no-constant-condition': OFF,
|
||||
'no-control-regex': OFF,
|
||||
@@ -436,7 +435,6 @@ module.exports = {
|
||||
'packages/react-dom/src/test-utils/*.js',
|
||||
],
|
||||
rules: {
|
||||
'es/no-optional-chaining': OFF,
|
||||
'react-internal/no-production-logging': OFF,
|
||||
'react-internal/warning-args': OFF,
|
||||
'react-internal/safe-string-coercion': [
|
||||
@@ -498,7 +496,6 @@ module.exports = {
|
||||
__IS_CHROME__: 'readonly',
|
||||
__IS_FIREFOX__: 'readonly',
|
||||
__IS_EDGE__: 'readonly',
|
||||
__IS_NATIVE__: 'readonly',
|
||||
__IS_INTERNAL_VERSION__: 'readonly',
|
||||
},
|
||||
},
|
||||
|
||||
107
.github/dependabot.yml
vendored
107
.github/dependabot.yml
vendored
@@ -1,10 +1,107 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directories:
|
||||
- "/fixtures/*"
|
||||
directory: "/fixtures/art"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/attribute-behavior"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/concurrent"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/devtools"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/dom"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/eslint"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/expiration"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/fiber-debugger"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/fiber-triangle"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/fizz"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/fizz-ssr-browser"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/flight"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/flight-browser"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/flight-esm"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/legacy-jsx-runtimes"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/nesting"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/packaging"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/scheduler"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/ssr"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/ssr-2"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/fixtures/stacks"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 0
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
|
||||
12
.github/workflows/compiler_playground.yml
vendored
12
.github/workflows/compiler_playground.yml
vendored
@@ -15,7 +15,7 @@ env:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: compiler/apps/playground
|
||||
working-directory: compiler
|
||||
|
||||
jobs:
|
||||
playground:
|
||||
@@ -27,17 +27,13 @@ jobs:
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: compiler/**/yarn.lock
|
||||
cache-dependency-path: compiler/yarn.lock
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v4
|
||||
id: node_modules
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
|
||||
- name: yarn install compiler
|
||||
run: yarn install --frozen-lockfile
|
||||
working-directory: compiler
|
||||
- name: yarn install playground
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: yarn test
|
||||
- run: yarn workspace playground test
|
||||
|
||||
52
.github/workflows/compiler_prereleases.yml
vendored
52
.github/workflows/compiler_prereleases.yml
vendored
@@ -1,52 +0,0 @@
|
||||
name: (Compiler) Publish Prereleases
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
commit_sha:
|
||||
required: true
|
||||
default: ''
|
||||
type: string
|
||||
release_channel:
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
# https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: compiler
|
||||
|
||||
jobs:
|
||||
publish_prerelease:
|
||||
name: Publish prelease (${{ inputs.release_channel }}) ${{ inputs.commit_sha }} @${{ inputs.dist_tag }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: yarn
|
||||
cache-dependency-path: compiler/yarn.lock
|
||||
- name: Restore cached node_modules
|
||||
uses: actions/cache@v4
|
||||
id: node_modules
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('compiler/**/yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Publish packages to npm
|
||||
run: |
|
||||
cp ./scripts/release/ci-npmrc ~/.npmrc
|
||||
scripts/release/publish.js --frfr --ci --tags ${{ inputs.dist_tag }}
|
||||
@@ -1,21 +0,0 @@
|
||||
name: (Compiler) Publish Prereleases Manual
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
prerelease_commit_sha:
|
||||
required: false
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
jobs:
|
||||
publish_prerelease_experimental:
|
||||
name: Publish to Experimental channel
|
||||
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
|
||||
with:
|
||||
commit_sha: ${{ inputs.prerelease_commit_sha || github.sha }}
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -1,20 +0,0 @@
|
||||
name: (Compiler) Publish Prereleases Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
|
||||
- cron: 10 16 * * 1,2,3,4,5
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
jobs:
|
||||
publish_prerelease_experimental:
|
||||
name: Publish to Experimental channel
|
||||
uses: facebook/react/.github/workflows/compiler_prereleases.yml@main
|
||||
with:
|
||||
commit_sha: ${{ github.sha }}
|
||||
release_channel: experimental
|
||||
dist_tag: experimental
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
2
.github/workflows/compiler_typescript.yml
vendored
2
.github/workflows/compiler_typescript.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- compiler/**
|
||||
- .github/workflows/compiler_typescript.yml
|
||||
- .github/workflows/compiler-typescript.yml
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
|
||||
73
.github/workflows/runtime_commit_artifacts.yml
vendored
73
.github/workflows/runtime_commit_artifacts.yml
vendored
@@ -11,11 +11,6 @@ on:
|
||||
commit_sha:
|
||||
required: false
|
||||
type: string
|
||||
force:
|
||||
description: 'Force a commit to the builds/... branches'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
TZ: /usr/share/zoneinfo/America/Los_Angeles
|
||||
@@ -82,7 +77,7 @@ jobs:
|
||||
working-directory: scripts/release
|
||||
- name: Download artifacts for base revision
|
||||
run: |
|
||||
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha || github.sha }}
|
||||
GH_TOKEN=${{ github.token }} scripts/release/download-experimental-build.js --commit=${{ inputs.commit_sha || github.event.workflow_run.head_sha }}
|
||||
- name: Display structure of build
|
||||
run: ls -R build
|
||||
- name: Strip @license from eslint plugin and react-refresh
|
||||
@@ -90,11 +85,6 @@ jobs:
|
||||
sed -i -e 's/ @license React*//' \
|
||||
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
|
||||
- name: Insert @headers into eslint plugin and react-refresh
|
||||
run: |
|
||||
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
|
||||
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
|
||||
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
|
||||
- name: Move relevant files for React in www into compiled
|
||||
run: |
|
||||
# Move the facebook-www folder into compiled
|
||||
@@ -123,14 +113,13 @@ jobs:
|
||||
run: |
|
||||
BASE_FOLDER='compiled-rn/facebook-fbsource/xplat/js'
|
||||
mkdir -p ${BASE_FOLDER}/react-native-github/Libraries/Renderer/
|
||||
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-dom,react-is,react-test-renderer}/
|
||||
mkdir -p ${BASE_FOLDER}/RKJSModules/vendor/react/{scheduler,react,react-is,react-test-renderer}/
|
||||
|
||||
# Move React Native renderer
|
||||
mv build/react-native/implementations/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
|
||||
mv build/react-native/shims/ $BASE_FOLDER/react-native-github/Libraries/Renderer/
|
||||
mv build/facebook-react-native/scheduler/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/scheduler/
|
||||
mv build/facebook-react-native/react/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react/
|
||||
mv build/facebook-react-native/react-dom/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-dom/
|
||||
mv build/facebook-react-native/react-is/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-is/
|
||||
mv build/facebook-react-native/react-test-renderer/cjs/ $BASE_FOLDER/RKJSModules/vendor/react/react-test-renderer/
|
||||
|
||||
@@ -145,9 +134,9 @@ jobs:
|
||||
ls -R ./compiled-rn
|
||||
- name: Add REVISION files
|
||||
run: |
|
||||
echo ${{ github.sha }} >> ./compiled/facebook-www/REVISION
|
||||
echo ${{ github.event.workflow_run.head_sha }} >> ./compiled/facebook-www/REVISION
|
||||
cp ./compiled/facebook-www/REVISION ./compiled/facebook-www/REVISION_TRANSFORMS
|
||||
echo ${{ github.sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
|
||||
echo ${{ github.event.workflow_run.head_sha}} >> ./compiled-rn/facebook-fbsource/xplat/js/react-native-github/Libraries/Renderer/REVISION
|
||||
- name: "Get current version string"
|
||||
id: get_current_version
|
||||
run: |
|
||||
@@ -171,7 +160,7 @@ jobs:
|
||||
|
||||
commit_www_artifacts:
|
||||
needs: download_artifacts
|
||||
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0')
|
||||
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -201,7 +190,6 @@ jobs:
|
||||
grep -rl "$CURRENT_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$CURRENT_VERSION_MODERN/$LAST_VERSION_MODERN/g"
|
||||
grep -rl "$CURRENT_VERSION_MODERN" ./compiled || echo "Modern version reverted"
|
||||
- name: Check for changes
|
||||
if: inputs.force != true
|
||||
id: check_should_commit
|
||||
run: |
|
||||
echo "Full git status"
|
||||
@@ -219,7 +207,7 @@ jobs:
|
||||
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Re-apply version changes
|
||||
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != '')
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_classic != '' && needs.download_artifacts.outputs.last_version_modern != ''
|
||||
env:
|
||||
CURRENT_VERSION_CLASSIC: ${{ needs.download_artifacts.outputs.current_version_classic }}
|
||||
CURRENT_VERSION_MODERN: ${{ needs.download_artifacts.outputs.current_version_modern }}
|
||||
@@ -236,26 +224,26 @@ jobs:
|
||||
grep -rl "$LAST_VERSION_MODERN" ./compiled | xargs -r sed -i -e "s/$LAST_VERSION_MODERN/$CURRENT_VERSION_MODERN/g"
|
||||
grep -rl "$LAST_VERSION_MODERN" ./compiled || echo "Classic version re-applied"
|
||||
- name: Will commit these changes
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
run: |
|
||||
echo ":"
|
||||
git status -u
|
||||
- name: Commit changes to branch
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: |
|
||||
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
|
||||
${{ github.event.workflow_run.head_commit.message }}
|
||||
|
||||
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
|
||||
DiffTrain build for [${{ github.event.workflow_run.head_sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }})
|
||||
branch: builds/facebook-www
|
||||
commit_user_name: ${{ github.triggering_actor }}
|
||||
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
|
||||
commit_user_name: ${{ github.event.workflow_run.triggering_actor.login }}
|
||||
commit_user_email: ${{ github.event.workflow_run.triggering_actor.email || format('{0}@users.noreply.github.com', github.event.workflow_run.triggering_actor.login) }}
|
||||
create_branch: true
|
||||
|
||||
commit_fbsource_artifacts:
|
||||
needs: download_artifacts
|
||||
if: inputs.force == true || (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0')
|
||||
if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -278,7 +266,6 @@ jobs:
|
||||
grep -rl "$CURRENT_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$CURRENT_VERSION/$LAST_VERSION/g"
|
||||
grep -rl "$CURRENT_VERSION" ./compiled-rn || echo "Version reverted"
|
||||
- name: Check for changes
|
||||
if: inputs.force != 'true'
|
||||
id: check_should_commit
|
||||
run: |
|
||||
echo "Full git status"
|
||||
@@ -297,7 +284,7 @@ jobs:
|
||||
echo "should_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Re-apply version changes
|
||||
if: inputs.force == true || (steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != '')
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true' && needs.download_artifacts.outputs.last_version_rn != ''
|
||||
env:
|
||||
CURRENT_VERSION: ${{ needs.download_artifacts.outputs.current_version_rn }}
|
||||
LAST_VERSION: ${{ needs.download_artifacts.outputs.last_version_rn }}
|
||||
@@ -307,12 +294,12 @@ jobs:
|
||||
grep -rl "$LAST_VERSION" ./compiled-rn | xargs -r sed -i -e "s/$LAST_VERSION/$CURRENT_VERSION/g"
|
||||
grep -rl "$LAST_VERSION" ./compiled-rn || echo "Version re-applied"
|
||||
- name: Add files for signing
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
run: |
|
||||
echo ":"
|
||||
git add .
|
||||
- name: Signing files
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
@@ -366,9 +353,8 @@ jobs:
|
||||
console.log('Signing files in directory:', directory);
|
||||
try {
|
||||
const result = execSync(`git status --porcelain ${directory}`, {encoding: 'utf8'});
|
||||
console.log(result);
|
||||
|
||||
// Parse the git status output to get file paths!
|
||||
// Parse the git status output to get file paths
|
||||
const files = result.split('\n').filter(file => file.endsWith('.js'));
|
||||
|
||||
if (files.length === 0) {
|
||||
@@ -377,14 +363,7 @@ jobs:
|
||||
);
|
||||
} else {
|
||||
files.forEach(line => {
|
||||
let file = null;
|
||||
if (line.startsWith('D ')) {
|
||||
return;
|
||||
} else if (line.startsWith('R ')) {
|
||||
file = line.slice(line.indexOf('->') + 3);
|
||||
} else {
|
||||
file = line.slice(3).trim();
|
||||
}
|
||||
const file = line.slice(3).trim();
|
||||
if (file) {
|
||||
console.log(' Signing file:', file);
|
||||
const originalContents = fs.readFileSync(file, 'utf8');
|
||||
@@ -403,19 +382,19 @@ jobs:
|
||||
console.error('Error signing files:', e);
|
||||
}
|
||||
- name: Will commit these changes
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
run: |
|
||||
git add .
|
||||
git status
|
||||
- name: Commit changes to branch
|
||||
if: inputs.force == true || steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
if: steps.check_should_commit.outputs.should_commit == 'true'
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: |
|
||||
${{ github.event.workflow_run.head_commit.message || format('Manual build of {0}', github.event.workflow_run.head_sha || github.sha) }}
|
||||
${{ github.event.workflow_run.head_commit.message }}
|
||||
|
||||
DiffTrain build for [${{ github.event.workflow_run.head_sha || github.sha }}](https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha || github.sha }})
|
||||
DiffTrain build for commit https://github.com/facebook/react/commit/${{ github.event.workflow_run.head_sha }}.
|
||||
branch: builds/facebook-fbsource
|
||||
commit_user_name: ${{ github.triggering_actor }}
|
||||
commit_user_email: ${{ format('{0}@users.noreply.github.com', github.triggering_actor) }}
|
||||
commit_user_name: ${{ github.event.workflow_run.triggering_actor.login }}
|
||||
commit_user_email: ${{ github.event.workflow_run.triggering_actor.email || format('{0}@users.noreply.github.com', github.event.workflow_run.triggering_actor.login) }}
|
||||
create_branch: true
|
||||
|
||||
6
.github/workflows/shared_lint.yml
vendored
6
.github/workflows/shared_lint.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: node ./scripts/tasks/eslint
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: ./scripts/ci/check_license.sh
|
||||
|
||||
@@ -79,6 +79,6 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||
key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: ./scripts/ci/test_print_warnings.sh
|
||||
|
||||
@@ -52,7 +52,7 @@ const stablePackages = {
|
||||
// These packages do not exist in the @canary or @latest channel, only
|
||||
// @experimental. We don't use semver, just the commit sha, so this is just a
|
||||
// list of package names instead of a map.
|
||||
const experimentalPackages = ['react-markup'];
|
||||
const experimentalPackages = [];
|
||||
|
||||
module.exports = {
|
||||
ReactVersion,
|
||||
|
||||
0
compiler/.gitmodules
vendored
Normal file
0
compiler/.gitmodules
vendored
Normal file
1
compiler/.watchmanconfig
Normal file
1
compiler/.watchmanconfig
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
80
compiler/CODE_OF_CONDUCT.md
Normal file
80
compiler/CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to make participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when
|
||||
an individual is representing the project or its community in public spaces.
|
||||
Examples of representing a project or community include using an official
|
||||
project e-mail address, posting via an official social media account, or acting
|
||||
as an appointed representative at an online or offline event. Representation of
|
||||
a project may be further defined and clarified by project maintainers.
|
||||
|
||||
This Code of Conduct also applies outside the project spaces when there is a
|
||||
reasonable belief that an individual's behavior may have a negative impact on
|
||||
the project or its community.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at <opensource-conduct@fb.com>. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
31
compiler/CONTRIBUTING.md
Normal file
31
compiler/CONTRIBUTING.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Contributing to react-forget
|
||||
We want to make contributing to this project as easy and transparent as
|
||||
possible.
|
||||
|
||||
## Pull Requests
|
||||
We actively welcome your pull requests.
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've changed APIs, update the documentation.
|
||||
4. Ensure the test suite passes.
|
||||
5. Make sure your code lints.
|
||||
6. If you haven't already, complete the Contributor License Agreement ("CLA").
|
||||
|
||||
## Contributor License Agreement ("CLA")
|
||||
In order to accept your pull request, we need you to submit a CLA. You only need
|
||||
to do this once to work on any of Facebook's open source projects.
|
||||
|
||||
Complete your CLA here: <https://code.facebook.com/cla>
|
||||
|
||||
## Issues
|
||||
We use GitHub issues to track public bugs. Please ensure your description is
|
||||
clear and has sufficient instructions to be able to reproduce the issue.
|
||||
|
||||
Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
|
||||
disclosure of security bugs. In those cases, please go through the process
|
||||
outlined on that page and do not file a public issue.
|
||||
|
||||
## License
|
||||
By contributing to react-forget, you agree that your contributions will be licensed
|
||||
under the LICENSE file in the root directory of this source tree.
|
||||
21
compiler/LICENSE
Normal file
21
compiler/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -7,11 +7,7 @@
|
||||
|
||||
import '../styles/globals.css';
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
export default function RootLayout({children}: {children: React.ReactNode}) {
|
||||
'use no memo';
|
||||
return (
|
||||
<html lang="en">
|
||||
|
||||
@@ -11,7 +11,7 @@ import {SnackbarProvider} from 'notistack';
|
||||
import {Editor, Header, StoreProvider} from '../components';
|
||||
import MessageSnackbar from '../components/Message';
|
||||
|
||||
export default function Page(): JSX.Element {
|
||||
export default function Hoot() {
|
||||
return (
|
||||
<StoreProvider>
|
||||
<SnackbarProvider
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {parse as babelParse} from '@babel/parser';
|
||||
import {parse as babelParse, ParserPlugin} from '@babel/parser';
|
||||
import * as HermesParser from 'hermes-parser';
|
||||
import traverse, {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
Effect,
|
||||
ErrorSeverity,
|
||||
parseConfigPragma,
|
||||
printHIR,
|
||||
printReactiveFunction,
|
||||
run,
|
||||
ValueKind,
|
||||
runPlayground,
|
||||
type Hook,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
|
||||
@@ -43,7 +45,7 @@ import {
|
||||
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
|
||||
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
|
||||
|
||||
function parseInput(input: string, language: 'flow' | 'typescript'): any {
|
||||
function parseInput(input: string, language: 'flow' | 'typescript') {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
if (language === 'flow') {
|
||||
return HermesParser.parse(input, {
|
||||
@@ -64,14 +66,14 @@ function parseFunctions(
|
||||
source: string,
|
||||
language: 'flow' | 'typescript',
|
||||
): Array<
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>
|
||||
NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>
|
||||
> {
|
||||
const items: Array<
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>
|
||||
NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>
|
||||
> = [];
|
||||
try {
|
||||
const ast = parseInput(source, language);
|
||||
@@ -153,37 +155,26 @@ function isHookName(s: string): boolean {
|
||||
return /^use[A-Z0-9]/.test(s);
|
||||
}
|
||||
|
||||
function getReactFunctionType(id: t.Identifier | null): ReactFunctionType {
|
||||
if (id != null) {
|
||||
if (isHookName(id.name)) {
|
||||
function getReactFunctionType(
|
||||
id: NodePath<t.Identifier | null | undefined>,
|
||||
): ReactFunctionType {
|
||||
if (id && id.node && id.isIdentifier()) {
|
||||
if (isHookName(id.node.name)) {
|
||||
return 'Hook';
|
||||
}
|
||||
|
||||
const isPascalCaseNameSpace = /^[A-Z].*/;
|
||||
if (isPascalCaseNameSpace.test(id.name)) {
|
||||
if (isPascalCaseNameSpace.test(id.node.name)) {
|
||||
return 'Component';
|
||||
}
|
||||
}
|
||||
return 'Other';
|
||||
}
|
||||
|
||||
function getFunctionIdentifier(
|
||||
fn:
|
||||
| NodePath<t.FunctionDeclaration>
|
||||
| NodePath<t.ArrowFunctionExpression>
|
||||
| NodePath<t.FunctionExpression>,
|
||||
): t.Identifier | null {
|
||||
if (fn.isArrowFunctionExpression()) {
|
||||
return null;
|
||||
}
|
||||
const id = fn.get('id');
|
||||
return Array.isArray(id) === false && id.isIdentifier() ? id.node : null;
|
||||
}
|
||||
|
||||
function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
||||
const results = new Map<string, PrintedCompilerPipelineValue[]>();
|
||||
const error = new CompilerError();
|
||||
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
||||
const upsert = (result: PrintedCompilerPipelineValue) => {
|
||||
const entry = results.get(result.name);
|
||||
if (Array.isArray(entry)) {
|
||||
entry.push(result);
|
||||
@@ -197,30 +188,40 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
} else {
|
||||
language = 'typescript';
|
||||
}
|
||||
let count = 0;
|
||||
const withIdentifier = (id: t.Identifier | null): t.Identifier => {
|
||||
if (id != null && id.name != null) {
|
||||
return id;
|
||||
} else {
|
||||
return t.identifier(`anonymous_${count++}`);
|
||||
}
|
||||
};
|
||||
try {
|
||||
// Extract the first line to quickly check for custom test directives
|
||||
const pragma = source.substring(0, source.indexOf('\n'));
|
||||
const config = parseConfigPragma(pragma);
|
||||
|
||||
for (const fn of parseFunctions(source, language)) {
|
||||
const id = withIdentifier(getFunctionIdentifier(fn));
|
||||
for (const result of runPlayground(
|
||||
if (!fn.isFunctionDeclaration()) {
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason: `Unexpected function type ${fn.node.type}`,
|
||||
description:
|
||||
'Playground only supports parsing function declarations',
|
||||
severity: ErrorSeverity.Todo,
|
||||
loc: fn.node.loc ?? null,
|
||||
suggestions: null,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const id = fn.get('id');
|
||||
for (const result of run(
|
||||
fn,
|
||||
{
|
||||
...config,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
},
|
||||
getReactFunctionType(id),
|
||||
'_c',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)) {
|
||||
const fnName = id.name;
|
||||
const fnName = fn.node.id?.name ?? null;
|
||||
switch (result.kind) {
|
||||
case 'ast': {
|
||||
upsert({
|
||||
@@ -229,7 +230,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
name: result.name,
|
||||
value: {
|
||||
type: 'FunctionDeclaration',
|
||||
id: withIdentifier(result.value.id),
|
||||
id: result.value.id,
|
||||
async: result.value.async,
|
||||
generator: result.value.generator,
|
||||
body: result.value.body,
|
||||
@@ -273,17 +274,13 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
/**
|
||||
* error might be an invariant violation or other runtime error
|
||||
* (i.e. object shape that is not CompilerError)
|
||||
*/
|
||||
// error might be an invariant violation or other runtime error
|
||||
// (i.e. object shape that is not CompilerError)
|
||||
if (err instanceof CompilerError && err.details.length > 0) {
|
||||
error.details.push(...err.details);
|
||||
} else {
|
||||
/**
|
||||
* Handle unexpected failures by logging (to get a stack trace)
|
||||
* and reporting
|
||||
*/
|
||||
// Handle unexpected failures by logging (to get a stack trace)
|
||||
// and reporting
|
||||
console.error(err);
|
||||
error.details.push(
|
||||
new CompilerErrorDetail({
|
||||
@@ -301,7 +298,7 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
|
||||
return [{kind: 'ok', results}, language];
|
||||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
export default function Editor() {
|
||||
const store = useStore();
|
||||
const deferredStore = useDeferredValue(store);
|
||||
const dispatchStore = useStoreDispatch();
|
||||
|
||||
@@ -15,17 +15,18 @@ import {useEffect, useState} from 'react';
|
||||
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
|
||||
import {useStore, useStoreDispatch} from '../StoreContext';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
// TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
|
||||
// @ts-ignore
|
||||
import React$Types from '../../node_modules/@types/react/index.d.ts';
|
||||
|
||||
loader.config({monaco});
|
||||
|
||||
type Props = {
|
||||
errors: Array<CompilerErrorDetail>;
|
||||
errors: CompilerErrorDetail[];
|
||||
language: 'flow' | 'typescript';
|
||||
};
|
||||
|
||||
export default function Input({errors, language}: Props): JSX.Element {
|
||||
export default function Input({errors, language}: Props) {
|
||||
const [monaco, setMonaco] = useState<Monaco | null>(null);
|
||||
const store = useStore();
|
||||
const dispatchStore = useStoreDispatch();
|
||||
@@ -37,19 +38,18 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
const model = monaco.editor.getModel(uri);
|
||||
invariant(model, 'Model must exist for the selected input file.');
|
||||
renderReactCompilerMarkers({monaco, model, details: errors});
|
||||
/**
|
||||
* N.B. that `tabSize` is a model property, not an editor property.
|
||||
* So, the tab size has to be set per model.
|
||||
*/
|
||||
// N.B. that `tabSize` is a model property, not an editor property.
|
||||
// So, the tab size has to be set per model.
|
||||
model.updateOptions({tabSize: 2});
|
||||
}, [monaco, errors]);
|
||||
|
||||
const flowDiagnosticDisable = [
|
||||
7028 /* unused label */, 6133 /* var declared but not read */,
|
||||
];
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Ignore "can only be used in TypeScript files." errors, since
|
||||
* we want to support syntax highlighting for Flow (*.js) files
|
||||
* and Flow is not a built-in language.
|
||||
*/
|
||||
// Ignore "can only be used in TypeScript files." errors, since
|
||||
// we want to support syntax highlighting for Flow (*.js) files
|
||||
// and Flow is not a built-in language.
|
||||
if (!monaco) return;
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
||||
diagnosticCodesToIgnore: [
|
||||
@@ -64,9 +64,7 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
8011,
|
||||
8012,
|
||||
8013,
|
||||
...(language === 'flow'
|
||||
? [7028 /* unused label */, 6133 /* var declared but not read */]
|
||||
: []),
|
||||
...(language === 'flow' ? flowDiagnosticDisable : []),
|
||||
],
|
||||
noSemanticValidation: true,
|
||||
// Monaco can't validate Flow component syntax
|
||||
@@ -74,7 +72,7 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
}, [monaco, language]);
|
||||
|
||||
const handleChange: (value: string | undefined) => void = value => {
|
||||
const handleChange = (value: string | undefined) => {
|
||||
if (!value) return;
|
||||
|
||||
dispatchStore({
|
||||
@@ -85,10 +83,7 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMount: (
|
||||
_: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => void = (_, monaco) => {
|
||||
const handleMount = (_: editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||
setMonaco(monaco);
|
||||
|
||||
const tscOptions = {
|
||||
@@ -116,12 +111,10 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(...reactLib);
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(...reactLib);
|
||||
|
||||
/**
|
||||
* Remeasure the font in case the custom font is loaded only after
|
||||
* Monaco Editor is mounted.
|
||||
* N.B. that this applies also to the output editor as it seems
|
||||
* Monaco Editor instances share the same font config.
|
||||
*/
|
||||
// Remeasure the font in case the custom font is loaded only after
|
||||
// Monaco Editor is mounted.
|
||||
// N.B. that this applies also to the output editor as it seems
|
||||
// Monaco Editor instances share the same font config.
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
@@ -132,18 +125,14 @@ export default function Input({errors, language}: Props): JSX.Element {
|
||||
<Resizable
|
||||
minWidth={650}
|
||||
enable={{right: true}}
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
// Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
// will grow the editor to fit within parent element
|
||||
className="!h-[calc(100vh_-_3.5rem)]">
|
||||
<MonacoEditor
|
||||
path={'index.js'}
|
||||
/**
|
||||
* .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
* check their syntax using its TS language service. They are still JS files
|
||||
* due to their extensions, so TS language features don't work.
|
||||
*/
|
||||
// .js and .jsx files are specified to be TS so that Monaco can actually
|
||||
// check their syntax using its TS language service. They are still JS files
|
||||
// due to their extensions, so TS language features don't work.
|
||||
language={'javascript'}
|
||||
value={store.source}
|
||||
onMount={handleMount}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {type CompilerError} from 'babel-plugin-react-compiler/src';
|
||||
import parserBabel from 'prettier/plugins/babel';
|
||||
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
||||
import * as prettier from 'prettier/standalone';
|
||||
import {memo, ReactNode, useEffect, useState} from 'react';
|
||||
import {memo, useEffect, useState} from 'react';
|
||||
import {type Store} from '../../lib/stores';
|
||||
import TabbedWindow from '../TabbedWindow';
|
||||
import {monacoOptions} from './monacoOptions';
|
||||
@@ -42,10 +42,10 @@ export type PrintedCompilerPipelineValue =
|
||||
| {kind: 'debug'; name: string; fnName: string | null; value: string};
|
||||
|
||||
export type CompilerOutput =
|
||||
| {kind: 'ok'; results: Map<string, Array<PrintedCompilerPipelineValue>>}
|
||||
| {kind: 'ok'; results: Map<string, PrintedCompilerPipelineValue[]>}
|
||||
| {
|
||||
kind: 'err';
|
||||
results: Map<string, Array<PrintedCompilerPipelineValue>>;
|
||||
results: Map<string, PrintedCompilerPipelineValue[]>;
|
||||
error: CompilerError;
|
||||
};
|
||||
|
||||
@@ -54,10 +54,7 @@ type Props = {
|
||||
compilerOutput: CompilerOutput;
|
||||
};
|
||||
|
||||
async function tabify(
|
||||
source: string,
|
||||
compilerOutput: CompilerOutput,
|
||||
): Promise<Map<string, ReactNode>> {
|
||||
async function tabify(source: string, compilerOutput: CompilerOutput) {
|
||||
const tabs = new Map<string, React.ReactNode>();
|
||||
const reorderedTabs = new Map<string, React.ReactNode>();
|
||||
const concattedResults = new Map<string, string>();
|
||||
@@ -115,10 +112,8 @@ async function tabify(
|
||||
}
|
||||
// Ensure that JS and the JS source map come first
|
||||
if (topLevelFnDecls.length > 0) {
|
||||
/**
|
||||
* Make a synthetic Program so we can have a single AST with all the top level
|
||||
* FunctionDeclarations
|
||||
*/
|
||||
// Make a synthetic Program so we can have a single AST with all the top level
|
||||
// FunctionDeclarations
|
||||
const ast = t.program(topLevelFnDecls);
|
||||
const {code, sourceMapUrl} = await codegen(ast, source);
|
||||
reorderedTabs.set(
|
||||
@@ -180,7 +175,7 @@ function getSourceMapUrl(code: string, map: string): string | null {
|
||||
)}`;
|
||||
}
|
||||
|
||||
function Output({store, compilerOutput}: Props): JSX.Element {
|
||||
function Output({store, compilerOutput}: Props) {
|
||||
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set(['JS']));
|
||||
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
|
||||
() => new Map(),
|
||||
@@ -241,13 +236,11 @@ function TextTabContent({
|
||||
output: string;
|
||||
diff: string | null;
|
||||
showInfoPanel: boolean;
|
||||
}): JSX.Element {
|
||||
}) {
|
||||
const [diffMode, setDiffMode] = useState(false);
|
||||
return (
|
||||
/**
|
||||
* Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
* will grow the editor to fit within parent element
|
||||
*/
|
||||
// Restrict MonacoEditor's height, since the config autoLayout:true
|
||||
// will grow the editor to fit within parent element
|
||||
<div className="w-full h-monaco_small sm:h-monaco">
|
||||
{showInfoPanel ? (
|
||||
<div className="flex items-center gap-1 bg-amber-50 p-2">
|
||||
|
||||
@@ -7,10 +7,8 @@
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
/**
|
||||
* monaco-editor is currently not compatible with ssr
|
||||
* https://github.com/vercel/next.js/issues/31692
|
||||
*/
|
||||
// monaco-editor is currently not compatible with ssr
|
||||
// https://github.com/vercel/next.js/issues/31692
|
||||
const Editor = dynamic(() => import('./EditorImpl'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
@@ -16,26 +16,26 @@ import {IconGitHub} from './Icons/IconGitHub';
|
||||
import Logo from './Logo';
|
||||
import {useStoreDispatch} from './StoreContext';
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
export default function Header() {
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
const dispatchStore = useStoreDispatch();
|
||||
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
|
||||
|
||||
const handleReset: () => void = () => {
|
||||
const handleReset = () => {
|
||||
if (confirm('Are you sure you want to reset the playground?')) {
|
||||
/**
|
||||
* Close open snackbars if any. This is necessary because when displaying
|
||||
* outputs (Preview or not), we only close previous snackbars if we received
|
||||
* new messages, which is needed in order to display "Bad URL" or success
|
||||
* messages when loading Playground for the first time. Otherwise, messages
|
||||
* such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
|
||||
*/
|
||||
/*
|
||||
Close open snackbars if any. This is necessary because when displaying
|
||||
outputs (Preview or not), we only close previous snackbars if we received
|
||||
new messages, which is needed in order to display "Bad URL" or success
|
||||
messages when loading Playground for the first time. Otherwise, messages
|
||||
such as "Bad URL" will be closed by the outputs calling `closeSnackbar`.
|
||||
*/
|
||||
closeSnackbar();
|
||||
dispatchStore({type: 'setStore', payload: {store: defaultStore}});
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare: () => void = () => {
|
||||
const handleShare = () => {
|
||||
navigator.clipboard.writeText(location.href).then(() => {
|
||||
enqueueSnackbar('URL copied to clipboard');
|
||||
setShowCheck(true);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
// https://github.com/reactjs/reactjs.org/blob/main/beta/src/components/Logo.tsx
|
||||
|
||||
export default function Logo(props: JSX.IntrinsicElements['svg']): JSX.Element {
|
||||
export default function Logo(props: JSX.IntrinsicElements['svg']) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 410 369"
|
||||
|
||||
@@ -29,7 +29,7 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
|
||||
/**
|
||||
* Make Store and dispatch function available to all sub-components in children.
|
||||
*/
|
||||
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
|
||||
export function StoreProvider({children}: {children: ReactNode}) {
|
||||
const [store, dispatch] = useReducer(storeReducer, emptyStore);
|
||||
|
||||
return (
|
||||
|
||||
@@ -69,9 +69,6 @@ function TabbedWindowItem({
|
||||
setTabsOpen(nextState);
|
||||
}, [tabsOpen, name, setTabsOpen]);
|
||||
|
||||
// Replace spaces with non-breaking spaces
|
||||
const displayName = name.replace(/ /g, '\u00A0');
|
||||
|
||||
return (
|
||||
<div key={name} className="flex flex-row">
|
||||
{isShow ? (
|
||||
@@ -83,7 +80,7 @@ function TabbedWindowItem({
|
||||
className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
- {displayName}
|
||||
- {name}
|
||||
</h2>
|
||||
{tabs.get(name) ?? <div>No output for {name}</div>}
|
||||
</Resizable>
|
||||
@@ -97,7 +94,7 @@ function TabbedWindowItem({
|
||||
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
|
||||
hasChanged ? 'font-bold' : 'font-light'
|
||||
} text-secondary hover:text-link`}>
|
||||
{displayName}
|
||||
{name}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,13 +23,10 @@ import React from 'react';
|
||||
* Instead, it throws an error when `useContext` is not called within a
|
||||
* Provider with a value.
|
||||
*/
|
||||
export default function createContext<T>(): {
|
||||
useContext: () => NonNullable<T>;
|
||||
Provider: React.Provider<T | null>;
|
||||
} {
|
||||
export default function createContext<T>() {
|
||||
const context = React.createContext<T | null>(null);
|
||||
|
||||
function useContext(): NonNullable<T> {
|
||||
function useContext() {
|
||||
const c = React.useContext(context);
|
||||
if (!c)
|
||||
throw new Error('useContext must be within a Provider with a value');
|
||||
|
||||
@@ -46,9 +46,9 @@ function mapReactCompilerDiagnosticToMonacoMarker(
|
||||
type ReactCompilerMarkerConfig = {
|
||||
monaco: Monaco;
|
||||
model: editor.ITextModel;
|
||||
details: Array<CompilerErrorDetail>;
|
||||
details: CompilerErrorDetail[];
|
||||
};
|
||||
let decorations: Array<string> = [];
|
||||
let decorations: string[] = [];
|
||||
export function renderReactCompilerMarkers({
|
||||
monaco,
|
||||
model,
|
||||
|
||||
@@ -28,7 +28,7 @@ export function decodeStore(hash: string): Store {
|
||||
/**
|
||||
* Serialize, encode, and save @param store to localStorage and update URL.
|
||||
*/
|
||||
export function saveStore(store: Store): void {
|
||||
export function saveStore(store: Store) {
|
||||
const hash = encodeStore(store);
|
||||
localStorage.setItem('playgroundStore', hash);
|
||||
history.replaceState({}, '', `#${hash}`);
|
||||
@@ -56,10 +56,8 @@ export function initStoreFromUrlOrLocalStorage(): Store {
|
||||
const encodedSourceFromLocal = localStorage.getItem('playgroundStore');
|
||||
const encodedSource = encodedSourceFromUrl || encodedSourceFromLocal;
|
||||
|
||||
/**
|
||||
* No data in the URL and no data in the localStorage to fallback to.
|
||||
* Initialize with the default store.
|
||||
*/
|
||||
// No data in the URL and no data in the localStorage to fallback to.
|
||||
// Initialize with the default store.
|
||||
if (!encodedSource) return defaultStore;
|
||||
|
||||
const raw = decodeStore(encodedSource);
|
||||
|
||||
@@ -3,27 +3,24 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cd ../.. && concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && cd apps/playground && NODE_ENV=development next dev\"",
|
||||
"build:compiler": "cd ../.. && concurrently -n compiler,runtime \"yarn workspace babel-plugin-react-compiler run build\" \"yarn workspace react-compiler-runtime run build\"",
|
||||
"build": "yarn build:compiler && next build",
|
||||
"postbuild": "node ./scripts/downloadFonts.js",
|
||||
"postinstall": "./scripts/link-compiler.sh",
|
||||
"vercel-build": "yarn build",
|
||||
"dev": "NODE_ENV=development next dev",
|
||||
"build": "next build && node ./scripts/downloadFonts.js",
|
||||
"vercel-build": "yarn workspaces run build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.9",
|
||||
"@babel/generator": "^7.18.9",
|
||||
"@babel/parser": "^7.18.9",
|
||||
"@babel/plugin-syntax-typescript": "^7.18.9",
|
||||
"@babel/core": "^7.19.1",
|
||||
"@babel/generator": "^7.19.1",
|
||||
"@babel/parser": "^7.19.1",
|
||||
"@babel/plugin-syntax-typescript": "^7.18.6",
|
||||
"@babel/plugin-transform-block-scoping": "^7.18.9",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.9",
|
||||
"@babel/preset-react": "^7.18.9",
|
||||
"@babel/preset-typescript": "^7.18.9",
|
||||
"@babel/traverse": "^7.18.9",
|
||||
"@babel/types": "7.18.9",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@babel/traverse": "^7.19.1",
|
||||
"@babel/types": "^7.19.0",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@playwright/test": "^1.42.1",
|
||||
@@ -39,23 +36,25 @@
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-format": "^29.3.1",
|
||||
"re-resizable": "^6.9.16",
|
||||
"react": "18.3.1",
|
||||
"react": "18.2.0",
|
||||
"react-compiler-runtime": "*",
|
||||
"react-dom": "18.3.1"
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "18.3.9",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"concurrently": "^7.4.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"hermes-parser": "^0.22.0",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"postcss": "^8.4.31",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"wait-on": "^7.2.0"
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"./**/@babel/parser": "7.7.4",
|
||||
"./**/@babel/types": "7.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ export default defineConfig({
|
||||
// Run your local dev server before starting the tests:
|
||||
// https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests
|
||||
webServer: {
|
||||
command: 'yarn dev',
|
||||
command:
|
||||
'yarn workspace babel-plugin-react-compiler build && yarn workspace react-compiler-runtime build && yarn dev',
|
||||
url: baseURL,
|
||||
timeout: 300 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
yarn --silent link babel-plugin-react-compiler
|
||||
yarn --silent link react-compiler-runtime
|
||||
@@ -31,7 +31,6 @@
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"../../../**"
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -151,7 +151,6 @@ impl MergedBlocks {
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Expected predecessor {predecessor} to exist")]
|
||||
#[allow(dead_code)]
|
||||
pub struct ExpectedPredecessorToExist {
|
||||
predecessor: BlockId,
|
||||
}
|
||||
|
||||
@@ -159,7 +159,6 @@ impl<'m> std::fmt::Debug for ScopeView<'m> {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub struct LabelView<'m> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) manager: &'m ScopeManager,
|
||||
|
||||
@@ -11,7 +11,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar
|
||||
* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts.
|
||||
* "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc).
|
||||
* Support typical debugging and profiling tools and workflows.
|
||||
* Be predictable and understandable enough by React developers — i.e. developers should be able to quickly develop a rough intuition of how React Compiler works.
|
||||
* Be predictable and understandable enough by React developers — ie developers should be able to quickly develop a rough intuition of how React Compiler works.
|
||||
* Not require explicit annotations (types or otherwise) for typical product code. We may provide features that allow developers to opt-in to using type information to enable additional optimizations, but the compiler should work well without type information or other annotations.
|
||||
|
||||
## Non-Goals
|
||||
@@ -24,15 +24,15 @@ The following are explicitly *not* goals for React Compiler:
|
||||
* The amount of code may regress startup times, which would conflict with our goal of neutral startup performance.
|
||||
* Support code that violates React's rules. React's rules exist to help developers build robust, scalable applications and form a contract that allows us to continue improving React without breaking applications. React Compiler depends on these rules to safely transform code, and violations of rules will therefore break React Compiler's optimizations.
|
||||
* Support legacy React features. Notably we will not support class components due to their inherent mutable state being shared across multiple methods with complex lifetimes and data flow.
|
||||
* Support 100% of the JavaScript language. In particular, we will not support rarely used features and/or features which are known to be unsafe or which cannot be modeled soundly. For example, nested classes that capture values from their closure are difficult to model accurately because of mutability, and `eval()` is unsafe. We aim to support the vast majority of JavaScript code (and the TypeScript and Flow dialects)
|
||||
* Support 100% of the JavaScript language. In particular, we will not support rarely used features and/or features which are known to be unsafe or which cannot be modeled soundly. For example, nested classes that capture values from their closure are difficult to model accurately bc of mutability, and `eval()` is unsafe. We aim to support the vast majority of JavaScript code (and the TypeScript and Flow dialects)
|
||||
|
||||
## Design Principles
|
||||
|
||||
Many aspects of the design follow naturally from the above goals:
|
||||
|
||||
* The compiler output must be high-level code that retains not just the semantics of the input but also is expressed using similar constructs to the input. For example, rather than convert logical expressions (`a ?? b`) into an `if` statement, we retain the high-level form of the logical expression. Rather than convert all looping constructs to a single form, we retain the original form of the loop. This follows from our goals:
|
||||
* High-level code is more compact, and helps reduce the impact of compilation on application size.
|
||||
* High-level constructs that match what the developer wrote are easier to debug.
|
||||
* High-level code is more compact, and helps reduce the impact of compilation on application size
|
||||
* High-level constructs that match what the developer wrote are easier to debug
|
||||
* From the above, it then follows that the compiler's internal representation must also be high-level enough to be able to output the original high-level constructs. The internal representation is what we call a High-level Intermediate Representation (HIR) — a name borrowed from Rust Compiler. However, React Compiler's HIR is perhaps even more suited to this name, as it retains high-level information (distinguishing if vs logical vs ternary, or for vs while vs for..of) but also represents code as a control-flow graph with no nesting.
|
||||
|
||||
## Architecture
|
||||
@@ -47,7 +47,7 @@ The core of the compiler is largely decoupled from Babel, using its own intermed
|
||||
- Validation: We run various validation passes to check that the input is valid React, ie that it does not break the rules. This includes looking for conditional hook calls, unconditional setState calls, etc.
|
||||
- **Optimization**: Various passes such as dead code elimination and constant propagation can generally improve performance and reduce the amount of instructions to be optimized later.
|
||||
- **Type Inference** (InferTypes): We run a conservative type inference pass to identify certain key types of data that may appear in the program that are relevant for further analysis, such as which values are hooks, primitives, etc.
|
||||
- **Inferring Reactive Scopes**: Several passes are involved in determining groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).
|
||||
- **Inferring Reactive Scopes**: Several passes are involved in determing groups of values that are created/mutated together and the set of instructions involved in creating/mutating those values. We call these groups "reactive scopes", and each can have one or more declarations (or occasionally a reassignment).
|
||||
- **Constructing/Optimizing Reactive Scopes**: Once the compiler determines the set of reactive scopes, it then transforms the program to make these scopes explicit in the HIR. The code is later converted to a ReactiveFunction, which is a hybrid of the HIR and an AST. Scopes are further pruned and transformed. For example, the compiler cannot make hook calls conditional, so any reactive scopes that contain a hook call must be pruned. If two consecutive scopes will always invalidate together, we attempt to merge them to reduce overhead, etc.
|
||||
- **Codegen**: Finally, the ReactiveFunction hybrid HIR/AST is converted back to a raw Babel AST node, and returned to the Babel plugin.
|
||||
- **Babel Plugin**: The Babel plugin replaces the original node with the new version.
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"**/next",
|
||||
"**/next/**"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
@@ -15,16 +20,14 @@
|
||||
"start": "yarn workspace playground run start",
|
||||
"next": "yarn workspace playground run dev",
|
||||
"build": "yarn workspaces run build",
|
||||
"dev": "echo 'DEPRECATED: use `cd apps/playground && yarn dev` instead!' && sleep 5 && cd apps/playground && yarn dev",
|
||||
"dev": "concurrently --kill-others -n compiler,runtime,playground \"yarn workspace babel-plugin-react-compiler run build --watch\" \"yarn workspace react-compiler-runtime run build --watch\" \"wait-on packages/babel-plugin-react-compiler/dist/index.js && yarn workspace playground run dev\"",
|
||||
"test": "yarn workspaces run test",
|
||||
"snap": "yarn workspace babel-plugin-react-compiler run snap",
|
||||
"snap:build": "yarn workspace snap run build",
|
||||
"postinstall": "perl -p -i -e 's/react\\.element/react.transitional.element/' node_modules/fbt/lib/FbtReactUtil.js && perl -p -i -e 's/didWarnAboutUsingAct = false;/didWarnAboutUsingAct = true;/' node_modules/react-dom/cjs/react-dom-test-utils.development.js",
|
||||
"npm:publish": "node scripts/release/publish"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^4.0.2"
|
||||
"postinstall": "perl -p -i -e 's/react\\.element/react.transitional.element/' packages/snap/node_modules/fbt/lib/FbtReactUtil.js && perl -p -i -e 's/didWarnAboutUsingAct = false;/didWarnAboutUsingAct = true;/' packages/babel-plugin-react-compiler/node_modules/react-dom/cjs/react-dom-test-utils.development.js",
|
||||
"npm:publish": "node scripts/release/publish-manual"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^25.0.7",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
@@ -34,12 +37,11 @@
|
||||
"@tsconfig/strictest": "^2.0.5",
|
||||
"concurrently": "^7.4.0",
|
||||
"folder-hash": "^4.0.4",
|
||||
"object-assign": "^4.1.1",
|
||||
"ora": "5.4.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-hermes-parser": "^0.23.0",
|
||||
"prompt-promise": "^1.0.3",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup": "^4.13.2",
|
||||
"rollup-plugin-banner2": "^1.2.3",
|
||||
"rollup-plugin-prettier": "^4.1.1",
|
||||
"typescript": "^5.4.3",
|
||||
|
||||
@@ -41,12 +41,12 @@
|
||||
"@types/invariant": "^2.2.35",
|
||||
"@types/jest": "^29.0.3",
|
||||
"@types/node": "^18.7.18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"babel-jest": "^29.0.3",
|
||||
"babel-plugin-fbt": "^1.0.0",
|
||||
"babel-plugin-fbt-runtime": "^1.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint": "8.27.0",
|
||||
"glob": "^7.1.6",
|
||||
"jest": "^29.0.3",
|
||||
"jest-environment-jsdom": "^29.0.3",
|
||||
|
||||
@@ -7,41 +7,10 @@
|
||||
|
||||
import {NodePath} from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
import {CompilerError, ErrorSeverity} from '../CompilerError';
|
||||
import {EnvironmentConfig, ExternalFunction, GeneratedSource} from '../HIR';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {ExternalFunction, GeneratedSource} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
path: NodePath<t.Program>,
|
||||
{validateBlocklistedImports}: EnvironmentConfig,
|
||||
): CompilerError | null {
|
||||
if (
|
||||
validateBlocklistedImports == null ||
|
||||
validateBlocklistedImports.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const error = new CompilerError();
|
||||
const restrictedImports = new Set(validateBlocklistedImports);
|
||||
path.traverse({
|
||||
ImportDeclaration(importDeclPath) {
|
||||
if (restrictedImports.has(importDeclPath.node.source.value)) {
|
||||
error.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Bailing out due to blocklisted import',
|
||||
description: `Import from module ${importDeclPath.node.source.value}`,
|
||||
loc: importDeclPath.node.loc ?? null,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
if (error.hasErrors()) {
|
||||
return error;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addImportsToProgram(
|
||||
path: NodePath<t.Program>,
|
||||
importList: Array<ExternalFunction>,
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
|
||||
import * as t from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
ExternalFunction,
|
||||
parseEnvironmentConfig,
|
||||
} from '../HIR/Environment';
|
||||
import {CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {ExternalFunction, PartialEnvironmentConfig} from '../HIR/Environment';
|
||||
import {hasOwnProperty} from '../Utils/utils';
|
||||
|
||||
const PanicThresholdOptionsSchema = z.enum([
|
||||
@@ -36,7 +32,7 @@ const PanicThresholdOptionsSchema = z.enum([
|
||||
export type PanicThresholdOptions = z.infer<typeof PanicThresholdOptionsSchema>;
|
||||
|
||||
export type PluginOptions = {
|
||||
environment: EnvironmentConfig;
|
||||
environment: PartialEnvironmentConfig | null;
|
||||
|
||||
logger: Logger | null;
|
||||
|
||||
@@ -169,12 +165,6 @@ export type LoggerEvent =
|
||||
fnLoc: t.SourceLocation | null;
|
||||
detail: Omit<Omit<CompilerErrorDetailOptions, 'severity'>, 'suggestions'>;
|
||||
}
|
||||
| {
|
||||
kind: 'CompileSkip';
|
||||
fnLoc: t.SourceLocation | null;
|
||||
reason: string;
|
||||
loc: t.SourceLocation | null;
|
||||
}
|
||||
| {
|
||||
kind: 'CompileSuccess';
|
||||
fnLoc: t.SourceLocation | null;
|
||||
@@ -198,13 +188,13 @@ export type Logger = {
|
||||
export const defaultOptions: PluginOptions = {
|
||||
compilationMode: 'infer',
|
||||
panicThreshold: 'none',
|
||||
environment: parseEnvironmentConfig({}).unwrap(),
|
||||
environment: {},
|
||||
logger: null,
|
||||
gating: null,
|
||||
noEmit: false,
|
||||
runtimeModule: null,
|
||||
eslintSuppressionRules: null,
|
||||
flowSuppressions: true,
|
||||
flowSuppressions: false,
|
||||
ignoreUseNoForget: false,
|
||||
sources: filename => {
|
||||
return filename.indexOf('node_modules') === -1;
|
||||
@@ -222,19 +212,7 @@ export function parsePluginOptions(obj: unknown): PluginOptions {
|
||||
// normalize string configs to be case insensitive
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
if (key === 'environment') {
|
||||
const environmentResult = parseEnvironmentConfig(value);
|
||||
if (environmentResult.isErr()) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason:
|
||||
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
|
||||
description: environmentResult.unwrapErr().toString(),
|
||||
suggestions: null,
|
||||
loc: null,
|
||||
});
|
||||
}
|
||||
parsedOptions[key] = environmentResult.unwrap();
|
||||
} else if (isCompilerFlag(key)) {
|
||||
if (isCompilerFlag(key)) {
|
||||
parsedOptions[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,19 +41,23 @@ import {
|
||||
constantPropagation,
|
||||
deadCodeElimination,
|
||||
pruneMaybeThrows,
|
||||
inlineJsxTransform,
|
||||
} from '../Optimization';
|
||||
import {instructionReordering} from '../Optimization/InstructionReordering';
|
||||
import {
|
||||
CodegenFunction,
|
||||
alignObjectMethodScopes,
|
||||
alignReactiveScopesToBlockScopes,
|
||||
assertScopeInstructionsWithinScopes,
|
||||
assertWellFormedBreakTargets,
|
||||
buildReactiveBlocks,
|
||||
buildReactiveFunction,
|
||||
codegenFunction,
|
||||
extractScopeDeclarationsFromDestructuring,
|
||||
flattenReactiveLoops,
|
||||
flattenScopesWithHooksOrUse,
|
||||
inferReactiveScopeVariables,
|
||||
memoizeFbtAndMacroOperandsInSameScope,
|
||||
memoizeFbtOperandsInSameScope,
|
||||
mergeOverlappingReactiveScopes,
|
||||
mergeReactiveScopesThatInvalidateTogether,
|
||||
promoteUsedTemporaries,
|
||||
propagateEarlyReturns,
|
||||
@@ -73,11 +77,7 @@ import {flattenScopesWithHooksOrUseHIR} from '../ReactiveScopes/FlattenScopesWit
|
||||
import {pruneAlwaysInvalidatingScopes} from '../ReactiveScopes/PruneAlwaysInvalidatingScopes';
|
||||
import pruneInitializationDependencies from '../ReactiveScopes/PruneInitializationDependencies';
|
||||
import {stabilizeBlockIds} from '../ReactiveScopes/StabilizeBlockIds';
|
||||
import {
|
||||
eliminateRedundantPhi,
|
||||
enterSSA,
|
||||
rewriteInstructionKindsBasedOnReassignment,
|
||||
} from '../SSA';
|
||||
import {eliminateRedundantPhi, enterSSA, leaveSSA} from '../SSA';
|
||||
import {inferTypes} from '../TypeInference';
|
||||
import {
|
||||
logCodegenFunction,
|
||||
@@ -98,11 +98,6 @@ import {
|
||||
} from '../Validation';
|
||||
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
|
||||
import {outlineFunctions} from '../Optimization/OutlineFunctions';
|
||||
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
|
||||
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
|
||||
import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects';
|
||||
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
|
||||
import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -132,11 +127,11 @@ export function* run(
|
||||
code,
|
||||
useMemoCacheIdentifier,
|
||||
);
|
||||
yield log({
|
||||
yield {
|
||||
kind: 'debug',
|
||||
name: 'EnvironmentConfig',
|
||||
value: prettyFormat(env.config),
|
||||
});
|
||||
};
|
||||
const ast = yield* runWithEnvironment(func, env);
|
||||
return ast;
|
||||
}
|
||||
@@ -204,10 +199,6 @@ function* runWithEnvironment(
|
||||
validateNoCapitalizedCalls(hir);
|
||||
}
|
||||
|
||||
if (env.config.lowerContextAccess) {
|
||||
lowerContextAccess(hir, env.config.lowerContextAccess);
|
||||
}
|
||||
|
||||
analyseFunctions(hir);
|
||||
yield log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
@@ -243,43 +234,17 @@ function* runWithEnvironment(
|
||||
validateNoSetStateInRender(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoSetStateInPassiveEffects) {
|
||||
validateNoSetStateInPassiveEffects(hir);
|
||||
}
|
||||
|
||||
if (env.config.validateNoJSXInTryStatements) {
|
||||
validateNoJSXInTryStatement(hir);
|
||||
}
|
||||
|
||||
inferReactivePlaces(hir);
|
||||
yield log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
|
||||
|
||||
rewriteInstructionKindsBasedOnReassignment(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'RewriteInstructionKindsBasedOnReassignment',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
propagatePhiTypes(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'PropagatePhiTypes',
|
||||
value: hir,
|
||||
});
|
||||
leaveSSA(hir);
|
||||
yield log({kind: 'hir', name: 'LeaveSSA', value: hir});
|
||||
|
||||
inferReactiveScopeVariables(hir);
|
||||
yield log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
|
||||
|
||||
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'MemoizeFbtAndMacroOperandsInSameScope',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
if (env.config.enableFunctionOutlining) {
|
||||
outlineFunctions(hir, fbtOperands);
|
||||
outlineFunctions(hir);
|
||||
yield log({kind: 'hir', name: 'OutlineFunctions', value: hir});
|
||||
}
|
||||
|
||||
@@ -297,68 +262,60 @@ function* runWithEnvironment(
|
||||
value: hir,
|
||||
});
|
||||
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
const fbtOperands = memoizeFbtOperandsInSameScope(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'PruneUnusedLabelsHIR',
|
||||
name: 'MemoizeFbtAndMacroOperandsInSameScope',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'AlignReactiveScopesToBlockScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'MergeOverlappingReactiveScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenReactiveLoopsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenScopesWithHooksOrUseHIR',
|
||||
value: hir,
|
||||
});
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
if (env.config.enablePropagateDepsInHIR) {
|
||||
propagateScopeDependenciesHIR(hir);
|
||||
if (env.config.enableReactiveScopesInHIR) {
|
||||
pruneUnusedLabelsHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'PropagateScopeDependenciesHIR',
|
||||
name: 'PruneUnusedLabelsHIR',
|
||||
value: hir,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.config.inlineJsxTransform) {
|
||||
inlineJsxTransform(hir, env.config.inlineJsxTransform);
|
||||
alignReactiveScopesToBlockScopesHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'inlineJsxTransform',
|
||||
name: 'AlignReactiveScopesToBlockScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
mergeOverlappingReactiveScopesHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'MergeOverlappingReactiveScopesHIR',
|
||||
value: hir,
|
||||
});
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
buildReactiveScopeTerminalsHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'BuildReactiveScopeTerminalsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
assertValidBlockNesting(hir);
|
||||
|
||||
flattenReactiveLoopsHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenReactiveLoopsHIR',
|
||||
value: hir,
|
||||
});
|
||||
|
||||
flattenScopesWithHooksOrUseHIR(hir);
|
||||
yield log({
|
||||
kind: 'hir',
|
||||
name: 'FlattenScopesWithHooksOrUseHIR',
|
||||
value: hir,
|
||||
});
|
||||
assertTerminalSuccessorsExist(hir);
|
||||
assertTerminalPredsExist(hir);
|
||||
}
|
||||
|
||||
const reactiveFunction = buildReactiveFunction(hir);
|
||||
@@ -376,17 +333,53 @@ function* runWithEnvironment(
|
||||
name: 'PruneUnusedLabels',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
assertScopeInstructionsWithinScopes(reactiveFunction);
|
||||
|
||||
if (!env.config.enablePropagateDepsInHIR) {
|
||||
propagateScopeDependencies(reactiveFunction);
|
||||
if (!env.config.enableReactiveScopesInHIR) {
|
||||
alignReactiveScopesToBlockScopes(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'PropagateScopeDependencies',
|
||||
name: 'AlignReactiveScopesToBlockScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
mergeOverlappingReactiveScopes(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'MergeOverlappingReactiveScopes',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
buildReactiveBlocks(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'BuildReactiveBlocks',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
flattenReactiveLoops(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'FlattenReactiveLoops',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
flattenScopesWithHooksOrUse(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'FlattenScopesWithHooks',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
}
|
||||
|
||||
assertScopeInstructionsWithinScopes(reactiveFunction);
|
||||
|
||||
propagateScopeDependencies(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'PropagateScopeDependencies',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneNonEscapingScopes(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
@@ -438,13 +431,6 @@ function* runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLValues',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
promoteUsedTemporaries(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
@@ -452,6 +438,13 @@ function* runWithEnvironment(
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
pruneUnusedLValues(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
name: 'PruneUnusedLValues',
|
||||
value: reactiveFunction,
|
||||
});
|
||||
|
||||
extractScopeDeclarationsFromDestructuring(reactiveFunction);
|
||||
yield log({
|
||||
kind: 'reactive',
|
||||
@@ -564,14 +557,3 @@ export function log(value: CompilerPipelineValue): CompilerPipelineValue {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function* runPlayground(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
config: EnvironmentConfig,
|
||||
fnType: ReactFunctionType,
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
const ast = yield* run(func, config, fnType, '_c', null, null, null);
|
||||
return ast;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {
|
||||
EnvironmentConfig,
|
||||
ExternalFunction,
|
||||
ReactFunctionType,
|
||||
parseEnvironmentConfig,
|
||||
tryParseExternalFunction,
|
||||
} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
@@ -23,11 +23,7 @@ import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
|
||||
import {isHookDeclaration} from '../Utils/HookDeclaration';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {insertGatedFunctionDeclaration} from './Gating';
|
||||
import {
|
||||
addImportsToProgram,
|
||||
updateMemoCacheFunctionImport,
|
||||
validateRestrictedImports,
|
||||
} from './Imports';
|
||||
import {addImportsToProgram, updateMemoCacheFunctionImport} from './Imports';
|
||||
import {PluginOptions} from './Options';
|
||||
import {compileFn} from './Pipeline';
|
||||
import {
|
||||
@@ -42,23 +38,34 @@ export type CompilerPass = {
|
||||
comments: Array<t.CommentBlock | t.CommentLine>;
|
||||
code: string | null;
|
||||
};
|
||||
const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
|
||||
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
|
||||
|
||||
function findDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
);
|
||||
): t.Directive | null {
|
||||
for (const directive of directives) {
|
||||
const directiveValue = directive.value.value;
|
||||
if (directiveValue === 'use forget' || directiveValue === 'use memo') {
|
||||
return directive;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findDirectiveDisablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
);
|
||||
options: PluginOptions,
|
||||
): t.Directive | null {
|
||||
for (const directive of directives) {
|
||||
const directiveValue = directive.value.value;
|
||||
if (
|
||||
(directiveValue === 'use no forget' ||
|
||||
directiveValue === 'use no memo') &&
|
||||
!options.ignoreUseNoForget
|
||||
) {
|
||||
return directive;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCriticalError(err: unknown): boolean {
|
||||
@@ -90,7 +97,7 @@ export type CompileResult = {
|
||||
compiledFn: CodegenFunction;
|
||||
};
|
||||
|
||||
function logError(
|
||||
function handleError(
|
||||
err: unknown,
|
||||
pass: CompilerPass,
|
||||
fnLoc: t.SourceLocation | null,
|
||||
@@ -119,13 +126,6 @@ function logError(
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleError(
|
||||
err: unknown,
|
||||
pass: CompilerPass,
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
logError(err, pass, fnLoc);
|
||||
if (
|
||||
pass.opts.panicThreshold === 'all_errors' ||
|
||||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
|
||||
@@ -263,7 +263,7 @@ function isFilePartOfSources(
|
||||
return sources(filename);
|
||||
}
|
||||
|
||||
for (const prefix of sources) {
|
||||
for (const prefix in sources) {
|
||||
if (filename.indexOf(prefix) !== -1) {
|
||||
return true;
|
||||
}
|
||||
@@ -272,33 +272,45 @@ function isFilePartOfSources(
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* `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.
|
||||
*/
|
||||
export function compileProgram(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): void {
|
||||
if (shouldSkipCompilation(program, pass)) {
|
||||
if (pass.opts.sources) {
|
||||
if (pass.filename === null) {
|
||||
const error = new CompilerError();
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason: `Expected a filename but found none.`,
|
||||
description:
|
||||
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
handleError(error, pass, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFilePartOfSources(pass.opts.sources, pass.filename)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Top level "use no forget", skip this file entirely
|
||||
if (
|
||||
findDirectiveDisablingMemoization(program.node.directives, pass.opts) !=
|
||||
null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = pass.opts.environment;
|
||||
const restrictedImportsErr = validateRestrictedImports(program, environment);
|
||||
if (restrictedImportsErr) {
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return;
|
||||
}
|
||||
const environment = parseEnvironmentConfig(pass.opts.environment ?? {});
|
||||
const useMemoCacheIdentifier = program.scope.generateUidIdentifier('c');
|
||||
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
|
||||
if (hasMemoCacheFunctionImport(program, moduleName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Record lint errors and critical errors as depending on Forget's config,
|
||||
@@ -310,6 +322,8 @@ export function compileProgram(
|
||||
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
|
||||
pass.opts.flowSuppressions,
|
||||
);
|
||||
const lintError = suppressionsToCompilerError(suppressions);
|
||||
let hasCriticalError = lintError != null;
|
||||
const queue: Array<{
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
@@ -318,7 +332,7 @@ export function compileProgram(
|
||||
const compiledFns: Array<CompileResult> = [];
|
||||
|
||||
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
|
||||
const fnType = getReactFunctionType(fn, pass, environment);
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
if (fnType === null || ALREADY_COMPILED.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
@@ -372,19 +386,7 @@ export function compileProgram(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
try {
|
||||
if (lintError != null) {
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
@@ -395,18 +397,30 @@ export function compileProgram(
|
||||
fn,
|
||||
);
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
const lintError = suppressionsToCompilerError(suppressionsInFunction);
|
||||
if (optOutDirectives.length > 0) {
|
||||
logError(lintError, pass, fn.node.loc ?? null);
|
||||
} else {
|
||||
handleError(lintError, pass, fn.node.loc ?? null);
|
||||
}
|
||||
return null;
|
||||
handleError(lintError, pass, fn.node.loc ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
try {
|
||||
/*
|
||||
* TODO(lauren): Remove pass.opts.environment nullcheck once PluginOptions
|
||||
* is validated
|
||||
*/
|
||||
if (environment.isErr()) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason:
|
||||
'Error in validating environment config. This is an advanced setting and not meant to be used directly',
|
||||
description: environment.unwrapErr().toString(),
|
||||
suggestions: null,
|
||||
loc: null,
|
||||
});
|
||||
}
|
||||
const config = environment.unwrap();
|
||||
|
||||
compiledFn = compileFn(
|
||||
fn,
|
||||
environment,
|
||||
config,
|
||||
fnType,
|
||||
useMemoCacheIdentifier.name,
|
||||
pass.opts.logger,
|
||||
@@ -424,50 +438,12 @@ export function compileProgram(
|
||||
prunedMemoValues: compiledFn.prunedMemoValues,
|
||||
});
|
||||
} catch (err) {
|
||||
/**
|
||||
* If an opt out directive is present, log only instead of throwing and don't mark as
|
||||
* containing a critical error.
|
||||
*/
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
if (optOutDirectives.length > 0) {
|
||||
logError(err, pass, fn.node.loc ?? null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
hasCriticalError ||= isCriticalError(err);
|
||||
handleError(err, pass, fn.node.loc ?? null);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always compile functions with opt in directives.
|
||||
*/
|
||||
if (optInDirectives.length > 0) {
|
||||
return compiledFn;
|
||||
} else if (pass.opts.compilationMode === 'annotation') {
|
||||
/**
|
||||
* No opt-in directive in annotation mode, so don't insert the compiled function.
|
||||
*/
|
||||
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,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pass.opts.noEmit) {
|
||||
if (!pass.opts.noEmit && !hasCriticalError) {
|
||||
return compiledFn;
|
||||
}
|
||||
return null;
|
||||
@@ -513,16 +489,6 @@ export function compileProgram(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not modify source if there is a module scope level opt out directive.
|
||||
*/
|
||||
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
|
||||
program.node.directives,
|
||||
);
|
||||
if (moduleScopeOptOutDirectives.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pass.opts.gating != null) {
|
||||
const error = checkFunctionReferencedBeforeDeclarationAtTopLevel(
|
||||
program,
|
||||
@@ -536,9 +502,6 @@ export function compileProgram(
|
||||
}
|
||||
}
|
||||
|
||||
const hasLoweredContextAccess = compiledFns.some(
|
||||
c => c.compiledFn.hasLoweredContextAccess,
|
||||
);
|
||||
const externalFunctions: Array<ExternalFunction> = [];
|
||||
let gating: null | ExternalFunction = null;
|
||||
try {
|
||||
@@ -548,29 +511,38 @@ export function compileProgram(
|
||||
externalFunctions.push(gating);
|
||||
}
|
||||
|
||||
const lowerContextAccess = environment.lowerContextAccess;
|
||||
if (lowerContextAccess && hasLoweredContextAccess) {
|
||||
externalFunctions.push(lowerContextAccess);
|
||||
}
|
||||
|
||||
const enableEmitInstrumentForget = environment.enableEmitInstrumentForget;
|
||||
const enableEmitInstrumentForget =
|
||||
pass.opts.environment?.enableEmitInstrumentForget;
|
||||
if (enableEmitInstrumentForget != null) {
|
||||
externalFunctions.push(enableEmitInstrumentForget.fn);
|
||||
externalFunctions.push(
|
||||
tryParseExternalFunction(enableEmitInstrumentForget.fn),
|
||||
);
|
||||
if (enableEmitInstrumentForget.gating != null) {
|
||||
externalFunctions.push(enableEmitInstrumentForget.gating);
|
||||
externalFunctions.push(
|
||||
tryParseExternalFunction(enableEmitInstrumentForget.gating),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (environment.enableEmitFreeze != null) {
|
||||
externalFunctions.push(environment.enableEmitFreeze);
|
||||
if (pass.opts.environment?.enableEmitFreeze != null) {
|
||||
const enableEmitFreeze = tryParseExternalFunction(
|
||||
pass.opts.environment.enableEmitFreeze,
|
||||
);
|
||||
externalFunctions.push(enableEmitFreeze);
|
||||
}
|
||||
|
||||
if (environment.enableEmitHookGuards != null) {
|
||||
externalFunctions.push(environment.enableEmitHookGuards);
|
||||
if (pass.opts.environment?.enableEmitHookGuards != null) {
|
||||
const enableEmitHookGuards = tryParseExternalFunction(
|
||||
pass.opts.environment.enableEmitHookGuards,
|
||||
);
|
||||
externalFunctions.push(enableEmitHookGuards);
|
||||
}
|
||||
|
||||
if (environment.enableChangeDetectionForDebugging != null) {
|
||||
externalFunctions.push(environment.enableChangeDetectionForDebugging);
|
||||
if (pass.opts.environment?.enableChangeDetectionForDebugging != null) {
|
||||
const enableChangeDetectionForDebugging = tryParseExternalFunction(
|
||||
pass.opts.environment.enableChangeDetectionForDebugging,
|
||||
);
|
||||
externalFunctions.push(enableChangeDetectionForDebugging);
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err, pass, null);
|
||||
@@ -613,50 +585,34 @@ export function compileProgram(
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipCompilation(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): boolean {
|
||||
if (pass.opts.sources) {
|
||||
if (pass.filename === null) {
|
||||
const error = new CompilerError();
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason: `Expected a filename but found none.`,
|
||||
description:
|
||||
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
|
||||
severity: ErrorSeverity.InvalidConfig,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
handleError(error, pass, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isFilePartOfSources(pass.opts.sources, pass.filename)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const moduleName = pass.opts.runtimeModule ?? 'react/compiler-runtime';
|
||||
if (hasMemoCacheFunctionImport(program, moduleName)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 ?? null;
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
|
||||
// Opt-outs disable compilation regardless of mode
|
||||
const useNoForget = findDirectiveDisablingMemoization(
|
||||
fn.node.body.directives,
|
||||
pass.opts,
|
||||
);
|
||||
if (useNoForget != null) {
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
detail: {
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Skipped due to "use no forget" directive.',
|
||||
loc: useNoForget.loc ?? null,
|
||||
suggestions: null,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
// Otherwise opt-ins enable compilation regardless of mode
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null) {
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
// Component and hook declarations are known components/hooks
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
|
||||
/**
|
||||
* Captures the start and end range of a pair of eslint-disable ... eslint-enable comments. In the
|
||||
@@ -149,11 +148,10 @@ export function findProgramSuppressions(
|
||||
|
||||
export function suppressionsToCompilerError(
|
||||
suppressionRanges: Array<SuppressionRange>,
|
||||
): CompilerError {
|
||||
CompilerError.invariant(suppressionRanges.length !== 0, {
|
||||
reason: `Expected at least suppression comment source range`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
): CompilerError | null {
|
||||
if (suppressionRanges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const error = new CompilerError();
|
||||
for (const suppressionRange of suppressionRanges) {
|
||||
if (
|
||||
|
||||
@@ -215,8 +215,7 @@ export function lower(
|
||||
id,
|
||||
params,
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
returnType: null, // TODO: extract the actual return type node if present
|
||||
body: builder.build(),
|
||||
context,
|
||||
generator: func.node.generator === true,
|
||||
@@ -420,19 +419,7 @@ function lowerStatement(
|
||||
// Already hoisted
|
||||
continue;
|
||||
}
|
||||
|
||||
let kind:
|
||||
| InstructionKind.Let
|
||||
| InstructionKind.HoistedConst
|
||||
| InstructionKind.HoistedLet
|
||||
| InstructionKind.HoistedFunction;
|
||||
if (binding.kind === 'const' || binding.kind === 'var') {
|
||||
kind = InstructionKind.HoistedConst;
|
||||
} else if (binding.kind === 'let') {
|
||||
kind = InstructionKind.HoistedLet;
|
||||
} else if (binding.path.isFunctionDeclaration()) {
|
||||
kind = InstructionKind.HoistedFunction;
|
||||
} else if (!binding.path.isVariableDeclarator()) {
|
||||
if (!binding.path.isVariableDeclarator()) {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Unsupported declaration type for hoisting',
|
||||
@@ -441,7 +428,19 @@ function lowerStatement(
|
||||
loc: id.parentPath.node.loc ?? GeneratedSource,
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
} else if (!binding.path.get('id').isIdentifier()) {
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Unsupported variable declaration type for hoisting',
|
||||
description: `variable "${
|
||||
binding.identifier.name
|
||||
}" declared with ${binding.path.get('id').type}`,
|
||||
suggestions: null,
|
||||
loc: id.parentPath.node.loc ?? GeneratedSource,
|
||||
});
|
||||
continue;
|
||||
} else if (binding.kind !== 'const' && binding.kind !== 'var') {
|
||||
// Avoid double errors on var declarations, which we do not plan to support anyways
|
||||
builder.errors.push({
|
||||
severity: ErrorSeverity.Todo,
|
||||
reason: 'Handle non-const declarations for hoisting',
|
||||
@@ -451,7 +450,6 @@ function lowerStatement(
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const identifier = builder.resolveIdentifier(id);
|
||||
CompilerError.invariant(identifier.kind === 'Identifier', {
|
||||
reason:
|
||||
@@ -468,7 +466,7 @@ function lowerStatement(
|
||||
lowerValueToTemporary(builder, {
|
||||
kind: 'DeclareContext',
|
||||
lvalue: {
|
||||
kind,
|
||||
kind: InstructionKind.HoistedConst,
|
||||
place,
|
||||
},
|
||||
loc: id.node.loc ?? GeneratedSource,
|
||||
@@ -609,7 +607,6 @@ function lowerStatement(
|
||||
),
|
||||
consequent: bodyBlock,
|
||||
alternate: continuationBlock.id,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
},
|
||||
@@ -659,13 +656,16 @@ function lowerStatement(
|
||||
},
|
||||
conditionalBlock,
|
||||
);
|
||||
/*
|
||||
* The conditional block is empty and exists solely as conditional for
|
||||
* (re)entering or exiting the loop
|
||||
*/
|
||||
const test = lowerExpressionToTemporary(builder, stmt.get('test'));
|
||||
const terminal: BranchTerminal = {
|
||||
kind: 'branch',
|
||||
test,
|
||||
consequent: loopBlock,
|
||||
alternate: continuationBlock.id,
|
||||
fallthrough: conditionalBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
};
|
||||
@@ -975,7 +975,6 @@ function lowerStatement(
|
||||
test,
|
||||
consequent: loopBlock,
|
||||
alternate: continuationBlock.id,
|
||||
fallthrough: conditionalBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc,
|
||||
};
|
||||
@@ -1001,7 +1000,7 @@ function lowerStatement(
|
||||
lowerAssignment(
|
||||
builder,
|
||||
stmt.node.loc ?? GeneratedSource,
|
||||
InstructionKind.Function,
|
||||
InstructionKind.Let,
|
||||
id,
|
||||
fn,
|
||||
'Assignment',
|
||||
@@ -1119,7 +1118,6 @@ function lowerStatement(
|
||||
consequent: loopBlock,
|
||||
alternate: continuationBlock.id,
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
fallthrough: continuationBlock.id,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
@@ -1205,7 +1203,6 @@ function lowerStatement(
|
||||
test,
|
||||
consequent: loopBlock,
|
||||
alternate: continuationBlock.id,
|
||||
fallthrough: continuationBlock.id,
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
},
|
||||
continuationBlock,
|
||||
@@ -1803,7 +1800,6 @@ function lowerExpression(
|
||||
test: {...testPlace},
|
||||
consequent: consequentBlock,
|
||||
alternate: alternateBlock,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: exprLoc,
|
||||
},
|
||||
@@ -1882,7 +1878,6 @@ function lowerExpression(
|
||||
test: {...leftPlace},
|
||||
consequent,
|
||||
alternate,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc: exprLoc,
|
||||
},
|
||||
@@ -2391,57 +2386,6 @@ function lowerExpression(
|
||||
case 'UpdateExpression': {
|
||||
let expr = exprPath as NodePath<t.UpdateExpression>;
|
||||
const argument = expr.get('argument');
|
||||
if (argument.isMemberExpression()) {
|
||||
const binaryOperator = expr.node.operator === '++' ? '+' : '-';
|
||||
const leftExpr = argument as NodePath<t.MemberExpression>;
|
||||
const {object, property, value} = lowerMemberExpression(
|
||||
builder,
|
||||
leftExpr,
|
||||
);
|
||||
|
||||
// Store the previous value to a temporary
|
||||
const previousValuePlace = lowerValueToTemporary(builder, value);
|
||||
// Store the new value to a temporary
|
||||
const updatedValue = lowerValueToTemporary(builder, {
|
||||
kind: 'BinaryExpression',
|
||||
operator: binaryOperator,
|
||||
left: {...previousValuePlace},
|
||||
right: lowerValueToTemporary(builder, {
|
||||
kind: 'Primitive',
|
||||
value: 1,
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
loc: leftExpr.node.loc ?? GeneratedSource,
|
||||
});
|
||||
|
||||
// Save the result back to the property
|
||||
let newValuePlace;
|
||||
if (typeof property === 'string') {
|
||||
newValuePlace = lowerValueToTemporary(builder, {
|
||||
kind: 'PropertyStore',
|
||||
object: {...object},
|
||||
property,
|
||||
value: {...updatedValue},
|
||||
loc: leftExpr.node.loc ?? GeneratedSource,
|
||||
});
|
||||
} else {
|
||||
newValuePlace = lowerValueToTemporary(builder, {
|
||||
kind: 'ComputedStore',
|
||||
object: {...object},
|
||||
property: {...property},
|
||||
value: {...updatedValue},
|
||||
loc: leftExpr.node.loc ?? GeneratedSource,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'LoadLocal',
|
||||
place: expr.node.prefix
|
||||
? {...newValuePlace}
|
||||
: {...previousValuePlace},
|
||||
loc: exprLoc,
|
||||
};
|
||||
}
|
||||
if (!argument.isIdentifier()) {
|
||||
builder.errors.push({
|
||||
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
|
||||
@@ -2616,7 +2560,6 @@ function lowerOptionalMemberExpression(
|
||||
test: {...object},
|
||||
consequent: consequent.id,
|
||||
alternate,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc,
|
||||
};
|
||||
@@ -2756,7 +2699,6 @@ function lowerOptionalCallExpression(
|
||||
test: {...testPlace},
|
||||
consequent: consequent.id,
|
||||
alternate,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc,
|
||||
};
|
||||
@@ -2895,21 +2837,6 @@ function isReorderableExpression(
|
||||
allowLocalIdentifiers,
|
||||
);
|
||||
}
|
||||
case 'LogicalExpression': {
|
||||
const logical = expr as NodePath<t.LogicalExpression>;
|
||||
return (
|
||||
isReorderableExpression(
|
||||
builder,
|
||||
logical.get('left'),
|
||||
allowLocalIdentifiers,
|
||||
) &&
|
||||
isReorderableExpression(
|
||||
builder,
|
||||
logical.get('right'),
|
||||
allowLocalIdentifiers,
|
||||
)
|
||||
);
|
||||
}
|
||||
case 'ConditionalExpression': {
|
||||
const conditional = expr as NodePath<t.ConditionalExpression>;
|
||||
return (
|
||||
@@ -3343,7 +3270,7 @@ function lowerFunctionToValue(
|
||||
return {
|
||||
kind: 'FunctionExpression',
|
||||
name,
|
||||
type: expr.node.type,
|
||||
expr: expr.node,
|
||||
loc: exprLoc,
|
||||
loweredFunc,
|
||||
};
|
||||
@@ -4032,7 +3959,6 @@ function lowerAssignment(
|
||||
test: {...test},
|
||||
consequent,
|
||||
alternate,
|
||||
fallthrough: continuationBlock.id,
|
||||
id: makeInstructionId(0),
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
ScopeId,
|
||||
} from './HIR';
|
||||
import {
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
@@ -177,7 +176,20 @@ export function buildReactiveScopeTerminalsHIR(fn: HIRFunction): void {
|
||||
* Step 5:
|
||||
* Fix scope and identifier ranges to account for renumbered instructions
|
||||
*/
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
const terminal = block.terminal;
|
||||
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
|
||||
/*
|
||||
* Scope ranges should always align to start at the 'scope' terminal
|
||||
* and end at the first instruction of the fallthrough block
|
||||
*/
|
||||
const fallthroughBlock = fn.body.blocks.get(terminal.fallthrough)!;
|
||||
const firstId =
|
||||
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
|
||||
terminal.scope.range.start = terminal.id;
|
||||
terminal.scope.range.end = firstId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TerminalRewriteInfo =
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import {
|
||||
Set_equal,
|
||||
Set_filter,
|
||||
Set_intersect,
|
||||
Set_union,
|
||||
getOrInsertDefault,
|
||||
} from '../Utils/utils';
|
||||
import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
DependencyPathEntry,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionValue,
|
||||
ReactiveScopeDependency,
|
||||
ScopeId,
|
||||
} from './HIR';
|
||||
|
||||
/**
|
||||
* Helper function for `PropagateScopeDependencies`. Uses control flow graph
|
||||
* analysis to determine which `Identifier`s can be assumed to be non-null
|
||||
* objects, on a per-block basis.
|
||||
*
|
||||
* Here is an example:
|
||||
* ```js
|
||||
* function useFoo(x, y, z) {
|
||||
* // NOT safe to hoist PropertyLoads here
|
||||
* if (...) {
|
||||
* // safe to hoist loads from x
|
||||
* read(x.a);
|
||||
* return;
|
||||
* }
|
||||
* // safe to hoist loads from y, z
|
||||
* read(y.b);
|
||||
* if (...) {
|
||||
* // safe to hoist loads from y, z
|
||||
* read(z.a);
|
||||
* } else {
|
||||
* // safe to hoist loads from y, z
|
||||
* read(z.b);
|
||||
* }
|
||||
* // safe to hoist loads from y, z
|
||||
* return;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Note that we currently do NOT account for mutable / declaration range when
|
||||
* doing the CFG-based traversal, producing results that are technically
|
||||
* incorrect but filtered by PropagateScopeDeps (which only takes dependencies
|
||||
* on constructed value -- i.e. a scope's dependencies must have mutable ranges
|
||||
* ending earlier than the scope start).
|
||||
*
|
||||
* Take this example, this function will infer x.foo.bar as non-nullable for
|
||||
* bb0, via the intersection of bb1 & bb2 which in turn comes from bb3. This is
|
||||
* technically incorrect bb0 is before / during x's mutable range.
|
||||
* ```
|
||||
* bb0:
|
||||
* const x = ...;
|
||||
* if cond then bb1 else bb2
|
||||
* bb1:
|
||||
* ...
|
||||
* goto bb3
|
||||
* bb2:
|
||||
* ...
|
||||
* goto bb3:
|
||||
* bb3:
|
||||
* x.foo.bar
|
||||
* ```
|
||||
*
|
||||
* @param fn
|
||||
* @param temporaries sidemap of identifier -> baseObject.a.b paths. Does not
|
||||
* contain optional chains.
|
||||
* @param hoistableFromOptionals sidemap of optionalBlock -> baseObject?.a
|
||||
* optional paths for which it's safe to evaluate non-optional loads (see
|
||||
* CollectOptionalChainDependencies).
|
||||
* @returns
|
||||
*/
|
||||
export function collectHoistablePropertyLoads(
|
||||
fn: HIRFunction,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
|
||||
): ReadonlyMap<ScopeId, BlockInfo> {
|
||||
const registry = new PropertyPathRegistry();
|
||||
|
||||
const nodes = collectNonNullsInBlocks(
|
||||
fn,
|
||||
temporaries,
|
||||
hoistableFromOptionals,
|
||||
registry,
|
||||
);
|
||||
propagateNonNull(fn, nodes, registry);
|
||||
|
||||
const nodesKeyedByScopeId = new Map<ScopeId, BlockInfo>();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (block.terminal.kind === 'scope') {
|
||||
nodesKeyedByScopeId.set(
|
||||
block.terminal.scope.id,
|
||||
nodes.get(block.terminal.block)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return nodesKeyedByScopeId;
|
||||
}
|
||||
|
||||
export type BlockInfo = {
|
||||
block: BasicBlock;
|
||||
assumedNonNullObjects: ReadonlySet<PropertyPathNode>;
|
||||
};
|
||||
|
||||
/**
|
||||
* PropertyLoadRegistry data structure to dedupe property loads (e.g. a.b.c)
|
||||
* and make computing sets intersections simpler.
|
||||
*/
|
||||
type RootNode = {
|
||||
properties: Map<string, PropertyPathNode>;
|
||||
optionalProperties: Map<string, PropertyPathNode>;
|
||||
parent: null;
|
||||
// Recorded to make later computations simpler
|
||||
fullPath: ReactiveScopeDependency;
|
||||
hasOptional: boolean;
|
||||
root: IdentifierId;
|
||||
};
|
||||
|
||||
type PropertyPathNode =
|
||||
| {
|
||||
properties: Map<string, PropertyPathNode>;
|
||||
optionalProperties: Map<string, PropertyPathNode>;
|
||||
parent: PropertyPathNode;
|
||||
fullPath: ReactiveScopeDependency;
|
||||
hasOptional: boolean;
|
||||
}
|
||||
| RootNode;
|
||||
|
||||
class PropertyPathRegistry {
|
||||
roots: Map<IdentifierId, RootNode> = new Map();
|
||||
|
||||
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
|
||||
/**
|
||||
* Reads from a statically scoped variable are always safe in JS,
|
||||
* with the exception of TDZ (not addressed by this pass).
|
||||
*/
|
||||
let rootNode = this.roots.get(identifier.id);
|
||||
|
||||
if (rootNode === undefined) {
|
||||
rootNode = {
|
||||
root: identifier.id,
|
||||
properties: new Map(),
|
||||
optionalProperties: new Map(),
|
||||
fullPath: {
|
||||
identifier,
|
||||
path: [],
|
||||
},
|
||||
hasOptional: false,
|
||||
parent: null,
|
||||
};
|
||||
this.roots.set(identifier.id, rootNode);
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
static getOrCreatePropertyEntry(
|
||||
parent: PropertyPathNode,
|
||||
entry: DependencyPathEntry,
|
||||
): PropertyPathNode {
|
||||
const map = entry.optional ? parent.optionalProperties : parent.properties;
|
||||
let child = map.get(entry.property);
|
||||
if (child == null) {
|
||||
child = {
|
||||
properties: new Map(),
|
||||
optionalProperties: new Map(),
|
||||
parent: parent,
|
||||
fullPath: {
|
||||
identifier: parent.fullPath.identifier,
|
||||
path: parent.fullPath.path.concat(entry),
|
||||
},
|
||||
hasOptional: parent.hasOptional || entry.optional,
|
||||
};
|
||||
map.set(entry.property, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
getOrCreateProperty(n: ReactiveScopeDependency): PropertyPathNode {
|
||||
/**
|
||||
* We add ReactiveScopeDependencies according to instruction ordering,
|
||||
* 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);
|
||||
if (n.path.length === 0) {
|
||||
return currNode;
|
||||
}
|
||||
for (let i = 0; i < n.path.length - 1; i++) {
|
||||
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
|
||||
currNode,
|
||||
n.path[i],
|
||||
);
|
||||
}
|
||||
|
||||
return PropertyPathRegistry.getOrCreatePropertyEntry(
|
||||
currNode,
|
||||
n.path.at(-1)!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getMaybeNonNullInInstruction(
|
||||
instr: InstructionValue,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
registry: PropertyPathRegistry,
|
||||
): PropertyPathNode | null {
|
||||
let path = null;
|
||||
if (instr.kind === 'PropertyLoad') {
|
||||
path = temporaries.get(instr.object.identifier.id) ?? {
|
||||
identifier: instr.object.identifier,
|
||||
path: [],
|
||||
};
|
||||
} else if (instr.kind === 'Destructure') {
|
||||
path = temporaries.get(instr.value.identifier.id) ?? null;
|
||||
} else if (instr.kind === 'ComputedLoad') {
|
||||
path = temporaries.get(instr.object.identifier.id) ?? null;
|
||||
}
|
||||
return path != null ? registry.getOrCreateProperty(path) : null;
|
||||
}
|
||||
|
||||
function collectNonNullsInBlocks(
|
||||
fn: HIRFunction,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
hoistableFromOptionals: ReadonlyMap<BlockId, ReactiveScopeDependency>,
|
||||
registry: PropertyPathRegistry,
|
||||
): ReadonlyMap<BlockId, BlockInfo> {
|
||||
/**
|
||||
* Due to current limitations of mutable range inference, there are edge cases in
|
||||
* which we infer known-immutable values (e.g. props or hook params) to have a
|
||||
* mutable range and scope.
|
||||
* (see `destructure-array-declaration-to-context-var` fixture)
|
||||
* We track known immutable identifiers to reduce regressions (as PropagateScopeDeps
|
||||
* is being rewritten to HIR).
|
||||
*/
|
||||
const knownImmutableIdentifiers = new Set<IdentifierId>();
|
||||
if (fn.fnType === 'Component' || fn.fnType === 'Hook') {
|
||||
for (const p of fn.params) {
|
||||
if (p.kind === 'Identifier') {
|
||||
knownImmutableIdentifiers.add(p.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Known non-null objects such as functional component props can be safely
|
||||
* read from any block.
|
||||
*/
|
||||
const knownNonNullIdentifiers = new Set<PropertyPathNode>();
|
||||
if (
|
||||
fn.fnType === 'Component' &&
|
||||
fn.params.length > 0 &&
|
||||
fn.params[0].kind === 'Identifier'
|
||||
) {
|
||||
const identifier = fn.params[0].identifier;
|
||||
knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier));
|
||||
}
|
||||
const nodes = new Map<BlockId, BlockInfo>();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
const assumedNonNullObjects = new Set<PropertyPathNode>(
|
||||
knownNonNullIdentifiers,
|
||||
);
|
||||
|
||||
const maybeOptionalChain = hoistableFromOptionals.get(block.id);
|
||||
if (maybeOptionalChain != null) {
|
||||
assumedNonNullObjects.add(
|
||||
registry.getOrCreateProperty(maybeOptionalChain),
|
||||
);
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const maybeNonNull = getMaybeNonNullInInstruction(
|
||||
instr.value,
|
||||
temporaries,
|
||||
registry,
|
||||
);
|
||||
if (maybeNonNull != null) {
|
||||
const baseIdentifier = maybeNonNull.fullPath.identifier;
|
||||
/**
|
||||
* Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges
|
||||
* are not valid with respect to current instruction id numbering.
|
||||
* We use attached reactive scope ranges as a proxy for mutable range, but this
|
||||
* is an overestimate as (1) scope ranges merge and align to form valid program
|
||||
* blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to
|
||||
* non-mutable identifiers.
|
||||
*
|
||||
* See comment at top of function for why we track known immutable identifiers.
|
||||
*/
|
||||
const isMutableAtInstr =
|
||||
baseIdentifier.mutableRange.end >
|
||||
baseIdentifier.mutableRange.start + 1 &&
|
||||
baseIdentifier.scope != null &&
|
||||
inRange(
|
||||
{
|
||||
id: instr.id,
|
||||
},
|
||||
baseIdentifier.scope.range,
|
||||
);
|
||||
if (
|
||||
!isMutableAtInstr ||
|
||||
knownImmutableIdentifiers.has(baseIdentifier.id)
|
||||
) {
|
||||
assumedNonNullObjects.add(maybeNonNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes.set(block.id, {
|
||||
block,
|
||||
assumedNonNullObjects,
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function propagateNonNull(
|
||||
fn: HIRFunction,
|
||||
nodes: ReadonlyMap<BlockId, BlockInfo>,
|
||||
registry: PropertyPathRegistry,
|
||||
): void {
|
||||
const blockSuccessors = new Map<BlockId, Set<BlockId>>();
|
||||
const terminalPreds = new Set<BlockId>();
|
||||
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
for (const pred of block.preds) {
|
||||
getOrInsertDefault(blockSuccessors, pred, new Set()).add(blockId);
|
||||
}
|
||||
if (block.terminal.kind === 'throw' || block.terminal.kind === 'return') {
|
||||
terminalPreds.add(blockId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In the context of a control flow graph, the identifiers that a block
|
||||
* can assume are non-null can be calculated from the following:
|
||||
* X = Union(Intersect(X_neighbors), X)
|
||||
*/
|
||||
function recursivelyPropagateNonNull(
|
||||
nodeId: BlockId,
|
||||
direction: 'forward' | 'backward',
|
||||
traversalState: Map<BlockId, 'active' | 'done'>,
|
||||
): boolean {
|
||||
/**
|
||||
* Avoid re-visiting computed or currently active nodes, which can
|
||||
* occur when the control flow graph has backedges.
|
||||
*/
|
||||
if (traversalState.has(nodeId)) {
|
||||
return false;
|
||||
}
|
||||
traversalState.set(nodeId, 'active');
|
||||
|
||||
const node = nodes.get(nodeId);
|
||||
if (node == null) {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Bad node ${nodeId}, kind: ${direction}`,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
}
|
||||
const neighbors = Array.from(
|
||||
direction === 'backward'
|
||||
? (blockSuccessors.get(nodeId) ?? [])
|
||||
: node.block.preds,
|
||||
);
|
||||
|
||||
let changed = false;
|
||||
for (const pred of neighbors) {
|
||||
if (!traversalState.has(pred)) {
|
||||
const neighborChanged = recursivelyPropagateNonNull(
|
||||
pred,
|
||||
direction,
|
||||
traversalState,
|
||||
);
|
||||
changed ||= neighborChanged;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Note that a predecessor / successor can only be active (status != 'done')
|
||||
* if it is a self-loop or other transitive cycle. Active neighbors can be
|
||||
* filtered out (i.e. not included in the intersection)
|
||||
* Example: self loop.
|
||||
* X = Union(Intersect(X, ...X_other_neighbors), X)
|
||||
*
|
||||
* Example: transitive cycle through node Y, for some Y that is a
|
||||
* predecessor / successor of X.
|
||||
* X = Union(
|
||||
* Intersect(
|
||||
* Union(Intersect(X, ...Y_other_neighbors), Y),
|
||||
* ...X_neighbors
|
||||
* ),
|
||||
* X
|
||||
* )
|
||||
*
|
||||
* Non-active neighbors with no recorded results can occur due to backedges.
|
||||
* it's not safe to assume they can be filtered out (e.g. not included in
|
||||
* the intersection)
|
||||
*/
|
||||
const neighborAccesses = Set_intersect(
|
||||
Array.from(neighbors)
|
||||
.filter(n => traversalState.get(n) === 'done')
|
||||
.map(n => assertNonNull(nodes.get(n)).assumedNonNullObjects),
|
||||
);
|
||||
|
||||
const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects;
|
||||
const mergedObjects = Set_union(prevObjects, neighborAccesses);
|
||||
reduceMaybeOptionalChains(mergedObjects, registry);
|
||||
|
||||
assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects;
|
||||
traversalState.set(nodeId, 'done');
|
||||
/**
|
||||
* Note that it's not sufficient to compare set sizes since
|
||||
* reduceMaybeOptionalChains may replace optional-chain loads with
|
||||
* unconditional loads. This could in turn change `assumedNonNullObjects` of
|
||||
* downstream blocks and backedges.
|
||||
*/
|
||||
changed ||= !Set_equal(prevObjects, mergedObjects);
|
||||
return changed;
|
||||
}
|
||||
const traversalState = new Map<BlockId, 'done' | 'active'>();
|
||||
const reversedBlocks = [...fn.body.blocks];
|
||||
reversedBlocks.reverse();
|
||||
|
||||
let changed;
|
||||
let i = 0;
|
||||
do {
|
||||
CompilerError.invariant(i++ < 100, {
|
||||
reason:
|
||||
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
|
||||
changed = false;
|
||||
for (const [blockId] of fn.body.blocks) {
|
||||
const forwardChanged = recursivelyPropagateNonNull(
|
||||
blockId,
|
||||
'forward',
|
||||
traversalState,
|
||||
);
|
||||
changed ||= forwardChanged;
|
||||
}
|
||||
traversalState.clear();
|
||||
for (const [blockId] of reversedBlocks) {
|
||||
const backwardChanged = recursivelyPropagateNonNull(
|
||||
blockId,
|
||||
'backward',
|
||||
traversalState,
|
||||
);
|
||||
changed ||= backwardChanged;
|
||||
}
|
||||
traversalState.clear();
|
||||
} while (changed);
|
||||
}
|
||||
|
||||
export function assertNonNull<T extends NonNullable<U>, U>(
|
||||
value: T | null | undefined,
|
||||
source?: string,
|
||||
): T {
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Unexpected null',
|
||||
description: source != null ? `(from ${source})` : null,
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Any two optional chains with different operations . vs ?. but the same set of
|
||||
* property strings paths de-duplicates.
|
||||
*
|
||||
* Intuitively: given <base>?.b, we know <base> to be either hoistable or not.
|
||||
* If unconditional reads from <base> are hoistable, we can replace all
|
||||
* <base>?.PROPERTY_STRING subpaths with <base>.PROPERTY_STRING
|
||||
*/
|
||||
function reduceMaybeOptionalChains(
|
||||
nodes: Set<PropertyPathNode>,
|
||||
registry: PropertyPathRegistry,
|
||||
): void {
|
||||
let optionalChainNodes = Set_filter(nodes, n => n.hasOptional);
|
||||
if (optionalChainNodes.size === 0) {
|
||||
return;
|
||||
}
|
||||
let changed: boolean;
|
||||
do {
|
||||
changed = false;
|
||||
|
||||
for (const original of optionalChainNodes) {
|
||||
let {identifier, path: origPath} = original.fullPath;
|
||||
let currNode: PropertyPathNode =
|
||||
registry.getOrCreateIdentifier(identifier);
|
||||
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
|
||||
const nextEntry: DependencyPathEntry =
|
||||
entry.optional && nodes.has(currNode)
|
||||
? {property: entry.property, optional: false}
|
||||
: entry;
|
||||
currNode = PropertyPathRegistry.getOrCreatePropertyEntry(
|
||||
currNode,
|
||||
nextEntry,
|
||||
);
|
||||
}
|
||||
if (currNode !== original) {
|
||||
changed = true;
|
||||
optionalChainNodes.delete(original);
|
||||
optionalChainNodes.add(currNode);
|
||||
nodes.delete(original);
|
||||
nodes.add(currNode);
|
||||
}
|
||||
}
|
||||
} while (changed);
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
import {CompilerError} from '..';
|
||||
import {assertNonNull} from './CollectHoistablePropertyLoads';
|
||||
import {
|
||||
BlockId,
|
||||
BasicBlock,
|
||||
InstructionId,
|
||||
IdentifierId,
|
||||
ReactiveScopeDependency,
|
||||
BranchTerminal,
|
||||
TInstruction,
|
||||
PropertyLoad,
|
||||
StoreLocal,
|
||||
GotoVariant,
|
||||
TBasicBlock,
|
||||
OptionalTerminal,
|
||||
HIRFunction,
|
||||
DependencyPathEntry,
|
||||
} from './HIR';
|
||||
import {printIdentifier} from './PrintHIR';
|
||||
|
||||
export function collectOptionalChainSidemap(
|
||||
fn: HIRFunction,
|
||||
): OptionalChainSidemap {
|
||||
const context: OptionalTraversalContext = {
|
||||
blocks: fn.body.blocks,
|
||||
seenOptionals: new Set(),
|
||||
processedInstrsInOptional: new Set(),
|
||||
temporariesReadInOptional: new Map(),
|
||||
hoistableObjects: new Map(),
|
||||
};
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
if (
|
||||
block.terminal.kind === 'optional' &&
|
||||
!context.seenOptionals.has(block.id)
|
||||
) {
|
||||
traverseOptionalBlock(
|
||||
block as TBasicBlock<OptionalTerminal>,
|
||||
context,
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
temporariesReadInOptional: context.temporariesReadInOptional,
|
||||
processedInstrsInOptional: context.processedInstrsInOptional,
|
||||
hoistableObjects: context.hoistableObjects,
|
||||
};
|
||||
}
|
||||
export type OptionalChainSidemap = {
|
||||
/**
|
||||
* Stores the correct property mapping (e.g. `a?.b` instead of `a.b`) for
|
||||
* dependency calculation. Note that we currently do not store anything on
|
||||
* outer phi nodes.
|
||||
*/
|
||||
temporariesReadInOptional: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
|
||||
/**
|
||||
* Records instructions (PropertyLoads, StoreLocals, and test terminals)
|
||||
* processed in this pass. When extracting dependencies in
|
||||
* PropagateScopeDependencies, these instructions are skipped.
|
||||
*
|
||||
* E.g. given a?.b
|
||||
* ```
|
||||
* bb0
|
||||
* $0 = LoadLocal 'a'
|
||||
* test $0 then=bb1 <- Avoid adding dependencies from these instructions, as
|
||||
* bb1 the sidemap produced by readOptionalBlock already maps
|
||||
* $1 = PropertyLoad $0.'b' <- $1 and $2 back to a?.b. Instead, we want to add a?.b
|
||||
* StoreLocal $2 = $1 <- as a dependency when $1 or $2 are later used in either
|
||||
* - an unhoistable expression within an outer optional
|
||||
* block e.g. MethodCall
|
||||
* - a phi node (if the entire optional value is hoistable)
|
||||
* ```
|
||||
*
|
||||
* Note that mapping blockIds to their evaluated dependency path does not
|
||||
* work, since values produced by inner optional chains may be referenced in
|
||||
* outer ones
|
||||
* ```
|
||||
* a?.b.c()
|
||||
* ->
|
||||
* bb0
|
||||
* $0 = LoadLocal 'a'
|
||||
* test $0 then=bb1
|
||||
* bb1
|
||||
* $1 = PropertyLoad $0.'b'
|
||||
* StoreLocal $2 = $1
|
||||
* goto bb2
|
||||
* bb2
|
||||
* test $2 then=bb3
|
||||
* bb3:
|
||||
* $3 = PropertyLoad $2.'c'
|
||||
* StoreLocal $4 = $3
|
||||
* goto bb4
|
||||
* bb4
|
||||
* test $4 then=bb5
|
||||
* bb5:
|
||||
* $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4!
|
||||
* ```
|
||||
*/
|
||||
processedInstrsInOptional: ReadonlySet<InstructionId>;
|
||||
/**
|
||||
* Records optional chains for which we can safely evaluate non-optional
|
||||
* PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
|
||||
* the optional terminal in bb1.
|
||||
* ```js
|
||||
* bb1:
|
||||
* ...
|
||||
* Optional optional=false test=bb2 fallth=...
|
||||
* bb2:
|
||||
* Optional optional=true test=bb3 fallth=...
|
||||
* ...
|
||||
* ```
|
||||
*/
|
||||
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
|
||||
};
|
||||
|
||||
type OptionalTraversalContext = {
|
||||
blocks: ReadonlyMap<BlockId, BasicBlock>;
|
||||
|
||||
// Track optional blocks to avoid outer calls into nested optionals
|
||||
seenOptionals: Set<BlockId>;
|
||||
|
||||
processedInstrsInOptional: Set<InstructionId>;
|
||||
temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
|
||||
hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Match the consequent and alternate blocks of an optional.
|
||||
* @returns propertyload computed by the consequent block, or null if the
|
||||
* consequent block is not a simple PropertyLoad.
|
||||
*/
|
||||
function matchOptionalTestBlock(
|
||||
terminal: BranchTerminal,
|
||||
blocks: ReadonlyMap<BlockId, BasicBlock>,
|
||||
): {
|
||||
consequentId: IdentifierId;
|
||||
property: string;
|
||||
propertyId: IdentifierId;
|
||||
storeLocalInstrId: InstructionId;
|
||||
consequentGoto: BlockId;
|
||||
} | null {
|
||||
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
|
||||
if (
|
||||
consequentBlock.instructions.length === 2 &&
|
||||
consequentBlock.instructions[0].value.kind === 'PropertyLoad' &&
|
||||
consequentBlock.instructions[1].value.kind === 'StoreLocal'
|
||||
) {
|
||||
const propertyLoad: TInstruction<PropertyLoad> = consequentBlock
|
||||
.instructions[0] as TInstruction<PropertyLoad>;
|
||||
const storeLocal: StoreLocal = consequentBlock.instructions[1].value;
|
||||
const storeLocalInstrId = consequentBlock.instructions[1].id;
|
||||
CompilerError.invariant(
|
||||
propertyLoad.value.object.identifier.id === terminal.test.identifier.id,
|
||||
{
|
||||
reason:
|
||||
'[OptionalChainDeps] Inconsistent optional chaining property load',
|
||||
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
|
||||
loc: propertyLoad.loc,
|
||||
},
|
||||
);
|
||||
|
||||
CompilerError.invariant(
|
||||
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected storeLocal',
|
||||
loc: propertyLoad.loc,
|
||||
},
|
||||
);
|
||||
if (
|
||||
consequentBlock.terminal.kind !== 'goto' ||
|
||||
consequentBlock.terminal.variant !== GotoVariant.Break
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const alternate = assertNonNull(blocks.get(terminal.alternate));
|
||||
|
||||
CompilerError.invariant(
|
||||
alternate.instructions.length === 2 &&
|
||||
alternate.instructions[0].value.kind === 'Primitive' &&
|
||||
alternate.instructions[1].value.kind === 'StoreLocal',
|
||||
{
|
||||
reason: 'Unexpected alternate structure',
|
||||
loc: terminal.loc,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
consequentId: storeLocal.lvalue.place.identifier.id,
|
||||
property: propertyLoad.value.property,
|
||||
propertyId: propertyLoad.lvalue.identifier.id,
|
||||
storeLocalInstrId,
|
||||
consequentGoto: consequentBlock.terminal.block,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse into the optional block and all transitively referenced blocks to
|
||||
* collect sidemaps of optional chain dependencies.
|
||||
*
|
||||
* @returns the IdentifierId representing the optional block if the block and
|
||||
* all transitively referenced optional blocks precisely represent a chain of
|
||||
* property loads. If any part of the optional chain is not hoistable, returns
|
||||
* null.
|
||||
*/
|
||||
function traverseOptionalBlock(
|
||||
optional: TBasicBlock<OptionalTerminal>,
|
||||
context: OptionalTraversalContext,
|
||||
outerAlternate: BlockId | null,
|
||||
): IdentifierId | null {
|
||||
context.seenOptionals.add(optional.id);
|
||||
const maybeTest = context.blocks.get(optional.terminal.test)!;
|
||||
let test: BranchTerminal;
|
||||
let baseObject: ReactiveScopeDependency;
|
||||
if (maybeTest.terminal.kind === 'branch') {
|
||||
CompilerError.invariant(optional.terminal.optional, {
|
||||
reason: '[OptionalChainDeps] Expect base case to be always optional',
|
||||
loc: optional.terminal.loc,
|
||||
});
|
||||
/**
|
||||
* Optional base expressions are currently within value blocks which cannot
|
||||
* be interrupted by scope boundaries. As such, the only dependencies we can
|
||||
* hoist out of optional chains are property load chains with no intervening
|
||||
* instructions.
|
||||
*
|
||||
* Ideally, we would be able to flatten base instructions out of optional
|
||||
* blocks, but this would require changes to HIR.
|
||||
*
|
||||
* For now, only match base expressions that are straightforward
|
||||
* PropertyLoad chains
|
||||
*/
|
||||
if (
|
||||
maybeTest.instructions.length === 0 ||
|
||||
maybeTest.instructions[0].value.kind !== 'LoadLocal'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const path: Array<DependencyPathEntry> = [];
|
||||
for (let i = 1; i < maybeTest.instructions.length; i++) {
|
||||
const instrVal = maybeTest.instructions[i].value;
|
||||
const prevInstr = maybeTest.instructions[i - 1];
|
||||
if (
|
||||
instrVal.kind === 'PropertyLoad' &&
|
||||
instrVal.object.identifier.id === prevInstr.lvalue.identifier.id
|
||||
) {
|
||||
path.push({property: instrVal.property, optional: false});
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
CompilerError.invariant(
|
||||
maybeTest.terminal.test.identifier.id ===
|
||||
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected test expression',
|
||||
loc: maybeTest.terminal.loc,
|
||||
},
|
||||
);
|
||||
baseObject = {
|
||||
identifier: maybeTest.instructions[0].value.place.identifier,
|
||||
path,
|
||||
};
|
||||
test = maybeTest.terminal;
|
||||
} else if (maybeTest.terminal.kind === 'optional') {
|
||||
/**
|
||||
* This is either
|
||||
* - <inner_optional>?.property (optional=true)
|
||||
* - <inner_optional>.property (optional=false)
|
||||
* - <inner_optional> <other operation>
|
||||
* - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d)
|
||||
*/
|
||||
const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!;
|
||||
if (testBlock!.terminal.kind !== 'branch') {
|
||||
/**
|
||||
* Fallthrough of the inner optional should be a block with no
|
||||
* instructions, terminating with Test($<temporary written to from
|
||||
* StoreLocal>)
|
||||
*/
|
||||
CompilerError.throwTodo({
|
||||
reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`,
|
||||
loc: maybeTest.terminal.loc,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Recurse into inner optional blocks to collect inner optional-chain
|
||||
* expressions, regardless of whether we can match the outer one to a
|
||||
* PropertyLoad.
|
||||
*/
|
||||
const innerOptional = traverseOptionalBlock(
|
||||
maybeTest as TBasicBlock<OptionalTerminal>,
|
||||
context,
|
||||
testBlock.terminal.alternate,
|
||||
);
|
||||
if (innerOptional == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the inner optional is part of the same optional-chain as the
|
||||
* outer one. This is not guaranteed, e.g. given a(c?.d)?.d
|
||||
* ```
|
||||
* bb0:
|
||||
* Optional test=bb1
|
||||
* bb1:
|
||||
* $0 = LoadLocal a <-- part 1 of the outer optional-chaining base
|
||||
* Optional test=bb2 fallth=bb5 <-- start of optional chain for c?.d
|
||||
* bb2:
|
||||
* ... (optional chain for c?.d)
|
||||
* ...
|
||||
* bb5:
|
||||
* $1 = phi(c.d, undefined) <-- part 2 (continuation) of the outer optional-base
|
||||
* $2 = Call $0($1)
|
||||
* Branch $2 ...
|
||||
* ```
|
||||
*/
|
||||
if (testBlock.terminal.test.identifier.id !== innerOptional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!optional.terminal.optional) {
|
||||
/**
|
||||
* If this is an non-optional load participating in an optional chain
|
||||
* (e.g. loading the `c` property in `a?.b.c`), record that PropertyLoads
|
||||
* from the inner optional value are hoistable.
|
||||
*/
|
||||
context.hoistableObjects.set(
|
||||
optional.id,
|
||||
assertNonNull(context.temporariesReadInOptional.get(innerOptional)),
|
||||
);
|
||||
}
|
||||
baseObject = assertNonNull(
|
||||
context.temporariesReadInOptional.get(innerOptional),
|
||||
);
|
||||
test = testBlock.terminal;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (test.alternate === outerAlternate) {
|
||||
CompilerError.invariant(optional.instructions.length === 0, {
|
||||
reason:
|
||||
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
|
||||
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
|
||||
loc: optional.terminal.loc,
|
||||
});
|
||||
}
|
||||
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
|
||||
if (!matchConsequentResult) {
|
||||
// Optional chain consequent is not hoistable e.g. a?.[computed()]
|
||||
return null;
|
||||
}
|
||||
CompilerError.invariant(
|
||||
matchConsequentResult.consequentGoto === optional.terminal.fallthrough,
|
||||
{
|
||||
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
|
||||
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
|
||||
loc: optional.terminal.loc,
|
||||
},
|
||||
);
|
||||
const load = {
|
||||
identifier: baseObject.identifier,
|
||||
path: [
|
||||
...baseObject.path,
|
||||
{
|
||||
property: matchConsequentResult.property,
|
||||
optional: optional.terminal.optional,
|
||||
},
|
||||
],
|
||||
};
|
||||
context.processedInstrsInOptional.add(
|
||||
matchConsequentResult.storeLocalInstrId,
|
||||
);
|
||||
context.processedInstrsInOptional.add(test.id);
|
||||
context.temporariesReadInOptional.set(
|
||||
matchConsequentResult.consequentId,
|
||||
load,
|
||||
);
|
||||
context.temporariesReadInOptional.set(matchConsequentResult.propertyId, load);
|
||||
return matchConsequentResult.consequentId;
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DependencyPathEntry,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
ReactiveScopeDependency,
|
||||
} from '../HIR';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies';
|
||||
|
||||
/**
|
||||
* Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR
|
||||
* for detailed explanation.
|
||||
*/
|
||||
export class ReactiveScopeDependencyTreeHIR {
|
||||
/**
|
||||
* Paths from which we can hoist PropertyLoads. If an `identifier`,
|
||||
* `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();
|
||||
|
||||
/**
|
||||
* @param hoistableObjects a set of paths from which we can safely evaluate
|
||||
* PropertyLoads. Note that we expect these to not contain duplicates (e.g.
|
||||
* both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges
|
||||
* duplicates when traversing the CFG.
|
||||
*/
|
||||
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
|
||||
for (const {path, identifier} of hoistableObjects) {
|
||||
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
this.#hoistableObjects,
|
||||
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
|
||||
);
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const prevAccessType = currNode.properties.get(
|
||||
path[i].property,
|
||||
)?.accessType;
|
||||
const accessType =
|
||||
i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull';
|
||||
CompilerError.invariant(
|
||||
prevAccessType == null || prevAccessType === accessType,
|
||||
{
|
||||
reason: 'Conflicting access types',
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
);
|
||||
let nextNode = currNode.properties.get(path[i].property);
|
||||
if (nextNode == null) {
|
||||
nextNode = {
|
||||
properties: new Map(),
|
||||
accessType,
|
||||
};
|
||||
currNode.properties.set(path[i].property, nextNode);
|
||||
}
|
||||
currNode = nextNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static #getOrCreateRoot<T extends string>(
|
||||
identifier: Identifier,
|
||||
roots: Map<Identifier, TreeNode<T>>,
|
||||
defaultAccessType: T,
|
||||
): TreeNode<T> {
|
||||
// roots can always be accessed unconditionally in JS
|
||||
let rootNode = roots.get(identifier);
|
||||
|
||||
if (rootNode === undefined) {
|
||||
rootNode = {
|
||||
properties: new Map(),
|
||||
accessType: defaultAccessType,
|
||||
};
|
||||
roots.set(identifier, rootNode);
|
||||
}
|
||||
return rootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a dependency with `#hoistableObjects` to record the hoistable
|
||||
* dependency. This effectively truncates @param dep to its maximal
|
||||
* safe-to-evaluate subpath
|
||||
*/
|
||||
addDependency(dep: ReactiveScopePropertyDependency): void {
|
||||
const {identifier, path} = dep;
|
||||
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
|
||||
identifier,
|
||||
this.#deps,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
/**
|
||||
* hoistableCursor is null if depCursor is not an object we can hoist
|
||||
* property reads from otherwise, it represents the same node in the
|
||||
* hoistable / cfg-informed tree
|
||||
*/
|
||||
let hoistableCursor: HoistableNode | undefined =
|
||||
this.#hoistableObjects.get(identifier);
|
||||
|
||||
// All properties read 'on the way' to a dependency are marked as 'access'
|
||||
for (const entry of path) {
|
||||
let nextHoistableCursor: HoistableNode | undefined;
|
||||
let nextDepCursor: DependencyNode;
|
||||
if (entry.optional) {
|
||||
/**
|
||||
* No need to check the access type since we can match both optional or non-optionals
|
||||
* in the hoistable
|
||||
* e.g. a?.b<rest> is hoistable if a.b<rest> is hoistable
|
||||
*/
|
||||
if (hoistableCursor != null) {
|
||||
nextHoistableCursor = hoistableCursor?.properties.get(entry.property);
|
||||
}
|
||||
|
||||
let accessType;
|
||||
if (
|
||||
hoistableCursor != null &&
|
||||
hoistableCursor.accessType === 'NonNull'
|
||||
) {
|
||||
/**
|
||||
* For an optional chain dep `a?.b`: if the hoistable tree only
|
||||
* contains `a`, we can keep either `a?.b` or 'a.b' as a dependency.
|
||||
* (note that we currently do the latter for perf)
|
||||
*/
|
||||
accessType = PropertyAccessType.UnconditionalAccess;
|
||||
} else {
|
||||
/**
|
||||
* Given that it's safe to evaluate `depCursor` and optional load
|
||||
* never throws, it's also safe to evaluate `depCursor?.entry`
|
||||
*/
|
||||
accessType = PropertyAccessType.OptionalAccess;
|
||||
}
|
||||
nextDepCursor = makeOrMergeProperty(
|
||||
depCursor,
|
||||
entry.property,
|
||||
accessType,
|
||||
);
|
||||
} else if (
|
||||
hoistableCursor != null &&
|
||||
hoistableCursor.accessType === 'NonNull'
|
||||
) {
|
||||
nextHoistableCursor = hoistableCursor.properties.get(entry.property);
|
||||
nextDepCursor = makeOrMergeProperty(
|
||||
depCursor,
|
||||
entry.property,
|
||||
PropertyAccessType.UnconditionalAccess,
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from
|
||||
*/
|
||||
break;
|
||||
}
|
||||
depCursor = nextDepCursor;
|
||||
hoistableCursor = nextHoistableCursor;
|
||||
}
|
||||
// mark the final node as a dependency
|
||||
depCursor.accessType = merge(
|
||||
depCursor.accessType,
|
||||
PropertyAccessType.OptionalDependency,
|
||||
);
|
||||
}
|
||||
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prints dependency tree to string for debugging.
|
||||
* @param includeAccesses
|
||||
* @returns string representation of DependencyTree
|
||||
*/
|
||||
printDeps(includeAccesses: boolean): string {
|
||||
let res: Array<Array<string>> = [];
|
||||
|
||||
for (const [rootId, rootNode] of this.#deps.entries()) {
|
||||
const rootResults = printSubtree(rootNode, includeAccesses).map(
|
||||
result => `${printIdentifier(rootId)}.${result}`,
|
||||
);
|
||||
res.push(rootResults);
|
||||
}
|
||||
return res.flat().join('\n');
|
||||
}
|
||||
|
||||
static debug<T extends string>(roots: Map<Identifier, TreeNode<T>>): string {
|
||||
const buf: Array<string> = [`tree() [`];
|
||||
for (const [rootId, rootNode] of roots) {
|
||||
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
|
||||
this.#debugImpl(buf, rootNode, 1);
|
||||
}
|
||||
buf.push(']');
|
||||
return buf.length > 2 ? buf.join('\n') : buf.join('');
|
||||
}
|
||||
|
||||
static #debugImpl<T extends string>(
|
||||
buf: Array<string>,
|
||||
node: TreeNode<T>,
|
||||
depth: number = 0,
|
||||
): void {
|
||||
for (const [property, childNode] of node.properties) {
|
||||
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
|
||||
this.#debugImpl(buf, childNode, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Enum representing the access type of single property on a parent object.
|
||||
* We distinguish on two independent axes:
|
||||
* Optional / Unconditional:
|
||||
* - whether this property is an optional load (within an optional chain)
|
||||
* Access / Dependency:
|
||||
* - Access: this property is read on the path of a dependency. We do not
|
||||
* need to track change variables for accessed properties. Tracking accesses
|
||||
* helps Forget do more granular dependency tracking.
|
||||
* - Dependency: this property is read as a dependency and we must track changes
|
||||
* to it for correctness.
|
||||
* ```javascript
|
||||
* // props.a is a dependency here and must be tracked
|
||||
* deps: {props.a, props.a.b} ---> minimalDeps: {props.a}
|
||||
* // props.a is just an access here and does not need to be tracked
|
||||
* deps: {props.a.b} ---> minimalDeps: {props.a.b}
|
||||
* ```
|
||||
*/
|
||||
enum PropertyAccessType {
|
||||
OptionalAccess = 'OptionalAccess',
|
||||
UnconditionalAccess = 'UnconditionalAccess',
|
||||
OptionalDependency = 'OptionalDependency',
|
||||
UnconditionalDependency = 'UnconditionalDependency',
|
||||
}
|
||||
|
||||
function isOptional(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.OptionalAccess ||
|
||||
access === PropertyAccessType.OptionalDependency
|
||||
);
|
||||
}
|
||||
function isDependency(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.OptionalDependency ||
|
||||
access === PropertyAccessType.UnconditionalDependency
|
||||
);
|
||||
}
|
||||
|
||||
function merge(
|
||||
access1: PropertyAccessType,
|
||||
access2: PropertyAccessType,
|
||||
): PropertyAccessType {
|
||||
const resultIsUnconditional = !(isOptional(access1) && isOptional(access2));
|
||||
const resultIsDependency = isDependency(access1) || isDependency(access2);
|
||||
|
||||
/*
|
||||
* Straightforward merge.
|
||||
* This can be represented as bitwise OR, but is written out for readability
|
||||
*
|
||||
* Observe that `UnconditionalAccess | ConditionalDependency` produces an
|
||||
* unconditionally accessed conditional dependency. We currently use these
|
||||
* as we use unconditional dependencies. (i.e. to codegen change variables)
|
||||
*/
|
||||
if (resultIsUnconditional) {
|
||||
if (resultIsDependency) {
|
||||
return PropertyAccessType.UnconditionalDependency;
|
||||
} else {
|
||||
return PropertyAccessType.UnconditionalAccess;
|
||||
}
|
||||
} else {
|
||||
// result is optional
|
||||
if (resultIsDependency) {
|
||||
return PropertyAccessType.OptionalDependency;
|
||||
} else {
|
||||
return PropertyAccessType.OptionalAccess;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TreeNode<T extends string> = {
|
||||
properties: Map<string, TreeNode<T>>;
|
||||
accessType: T;
|
||||
};
|
||||
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,
|
||||
rootIdentifier: Identifier,
|
||||
path: Array<DependencyPathEntry>,
|
||||
results: Set<ReactiveScopeDependency>,
|
||||
): void {
|
||||
if (isDependency(node.accessType)) {
|
||||
results.add({identifier: rootIdentifier, path});
|
||||
} else {
|
||||
for (const [childName, childNode] of node.properties) {
|
||||
collectMinimalDependenciesInSubtree(
|
||||
childNode,
|
||||
rootIdentifier,
|
||||
[
|
||||
...path,
|
||||
{
|
||||
property: childName,
|
||||
optional: isOptional(childNode.accessType),
|
||||
},
|
||||
],
|
||||
results,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printSubtree(
|
||||
node: DependencyNode,
|
||||
includeAccesses: boolean,
|
||||
): Array<string> {
|
||||
const results: Array<string> = [];
|
||||
for (const [propertyName, propertyNode] of node.properties) {
|
||||
if (includeAccesses || isDependency(propertyNode.accessType)) {
|
||||
results.push(`${propertyName} (${propertyNode.accessType})`);
|
||||
}
|
||||
const propertyResults = printSubtree(propertyNode, includeAccesses);
|
||||
results.push(...propertyResults.map(result => `${propertyName}.${result}`));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function makeOrMergeProperty(
|
||||
node: DependencyNode,
|
||||
property: string,
|
||||
accessType: PropertyAccessType,
|
||||
): DependencyNode {
|
||||
let child = node.properties.get(property);
|
||||
if (child == null) {
|
||||
child = {
|
||||
properties: new Map(),
|
||||
accessType,
|
||||
};
|
||||
node.properties.set(property, child);
|
||||
} else {
|
||||
child.accessType = merge(child.accessType, accessType);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
Global,
|
||||
GlobalRegistry,
|
||||
installReAnimatedTypes,
|
||||
installTypeConfig,
|
||||
} from './Globals';
|
||||
import {
|
||||
BlockId,
|
||||
@@ -29,11 +28,9 @@ import {
|
||||
NonLocalBinding,
|
||||
PolyType,
|
||||
ScopeId,
|
||||
SourceLocation,
|
||||
Type,
|
||||
ValidatedIdentifier,
|
||||
ValueKind,
|
||||
getHookKindForType,
|
||||
makeBlockId,
|
||||
makeIdentifierId,
|
||||
makeIdentifierName,
|
||||
@@ -48,14 +45,6 @@ import {
|
||||
addHook,
|
||||
} from './ObjectShape';
|
||||
import {Scope as BabelScope} from '@babel/traverse';
|
||||
import {TypeSchema} from './TypeSchema';
|
||||
|
||||
export const ReactElementSymbolSchema = z.object({
|
||||
elementSymbol: z.union([
|
||||
z.literal('react.element'),
|
||||
z.literal('react.transitional.element'),
|
||||
]),
|
||||
});
|
||||
|
||||
export const ExternalFunctionSchema = z.object({
|
||||
// Source for the imported module that exports the `importSpecifierName` functions
|
||||
@@ -78,20 +67,6 @@ export const InstrumentationSchema = z
|
||||
|
||||
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
|
||||
export const MacroMethodSchema = z.union([
|
||||
z.object({type: z.literal('wildcard')}),
|
||||
z.object({type: z.literal('name'), name: z.string()}),
|
||||
]);
|
||||
|
||||
// Would like to change this to drop the string option, but breaks compatibility with existing configs
|
||||
export const MacroSchema = z.union([
|
||||
z.string(),
|
||||
z.tuple([z.string(), z.array(MacroMethodSchema)]),
|
||||
]);
|
||||
|
||||
export type Macro = z.infer<typeof MacroSchema>;
|
||||
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
/*
|
||||
* The effect of arguments to this hook. Describes whether the hook may or may
|
||||
@@ -148,12 +123,6 @@ export type Hook = z.infer<typeof HookSchema>;
|
||||
const EnvironmentConfigSchema = z.object({
|
||||
customHooks: z.map(z.string(), HookSchema).optional().default(new Map()),
|
||||
|
||||
/**
|
||||
* A function that, given the name of a module, can optionally return a description
|
||||
* of that module's type signature.
|
||||
*/
|
||||
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* A list of functions which the application compiles as macros, where
|
||||
* the compiler must ensure they are not compiled to rename the macro or separate the
|
||||
@@ -164,7 +133,7 @@ const EnvironmentConfigSchema = z.object({
|
||||
* plugin since it looks specifically for the name of the function being invoked, not
|
||||
* following aliases.
|
||||
*/
|
||||
customMacros: z.nullable(z.array(MacroSchema)).default(null),
|
||||
customMacros: z.nullable(z.array(z.string())).default(null),
|
||||
|
||||
/**
|
||||
* Enable a check that resets the memoization cache when the source code of the file changes.
|
||||
@@ -230,24 +199,7 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
enablePropagateDepsInHIR: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
* just `props`. With this flag enabled, we'll infer that full path as
|
||||
* the dependency.
|
||||
*/
|
||||
enableOptionalDependencies: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Enables inlining ReactElement object literals in place of JSX
|
||||
* An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime
|
||||
* Currently a prod-only optimization, requiring Fast JSX dependencies
|
||||
*
|
||||
* The symbol configuration is set for backwards compatability with pre-React 19 transforms
|
||||
*/
|
||||
inlineJsxTransform: ReactElementSymbolSchema.nullish(),
|
||||
enableReactiveScopesInHIR: z.boolean().default(true),
|
||||
|
||||
/*
|
||||
* Enable validation of hooks to partially check that the component honors the rules of hooks.
|
||||
@@ -257,7 +209,7 @@ const EnvironmentConfigSchema = z.object({
|
||||
validateHooksUsage: z.boolean().default(true),
|
||||
|
||||
// Validate that ref values (`ref.current`) are not accessed during render.
|
||||
validateRefAccessDuringRender: z.boolean().default(true),
|
||||
validateRefAccessDuringRender: z.boolean().default(false),
|
||||
|
||||
/*
|
||||
* Validates that setState is not unconditionally called during render, as it can lead to
|
||||
@@ -265,18 +217,6 @@ const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
validateNoSetStateInRender: z.boolean().default(true),
|
||||
|
||||
/**
|
||||
* Validates that setState is not called directly within a passive effect (useEffect).
|
||||
* Scheduling a setState (with an event listener, subscription, etc) is valid.
|
||||
*/
|
||||
validateNoSetStateInPassiveEffects: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates against creating JSX within a try block and recommends using an error boundary
|
||||
* instead.
|
||||
*/
|
||||
validateNoJSXInTryStatements: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Validates that the dependencies of all effect hooks are memoized. This helps ensure
|
||||
* that Forget does not introduce infinite renders caused by a dependency changing,
|
||||
@@ -298,7 +238,6 @@ const EnvironmentConfigSchema = z.object({
|
||||
* this option to the empty array.
|
||||
*/
|
||||
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
|
||||
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
|
||||
|
||||
/*
|
||||
* When enabled, the compiler assumes that hooks follow the Rules of React:
|
||||
@@ -495,28 +434,6 @@ const EnvironmentConfigSchema = z.object({
|
||||
* Here the variables `ref` and `myRef` will be typed as Refs.
|
||||
*/
|
||||
enableTreatRefLikeIdentifiersAsRefs: z.boolean().nullable().default(false),
|
||||
|
||||
/*
|
||||
* If specified a value, the compiler lowers any calls to `useContext` to use
|
||||
* this value as the callee.
|
||||
*
|
||||
* A selector function is compiled and passed as an argument along with the
|
||||
* context to this function call.
|
||||
*
|
||||
* The compiler automatically figures out the keys by looking for the immediate
|
||||
* destructuring of the return value from the useContext call. In the future,
|
||||
* this can be extended to different kinds of context access like property
|
||||
* loads and accesses over multiple statements as well.
|
||||
*
|
||||
* ```
|
||||
* // input
|
||||
* const {foo, bar} = useContext(MyContext);
|
||||
*
|
||||
* // output
|
||||
* const {foo, bar} = useCompiledContext(MyContext, (c) => [c.foo, c.bar]);
|
||||
* ```
|
||||
*/
|
||||
lowerContextAccess: ExternalFunctionSchema.nullish(),
|
||||
});
|
||||
|
||||
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
|
||||
@@ -549,23 +466,6 @@ export function parseConfigPragma(pragma: string): EnvironmentConfig {
|
||||
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});
|
||||
}
|
||||
}
|
||||
console.log([valSplit[0], props.map(x => x.name ?? '*').join('.')]);
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean') {
|
||||
// skip parsing non-boolean properties
|
||||
continue;
|
||||
@@ -611,7 +511,6 @@ export function printFunctionType(type: ReactFunctionType): string {
|
||||
export class Environment {
|
||||
#globals: GlobalRegistry;
|
||||
#shapes: ShapeRegistry;
|
||||
#moduleTypes: Map<string, Global | null> = new Map();
|
||||
#nextIdentifer: number = 0;
|
||||
#nextBlock: number = 0;
|
||||
#nextScope: number = 0;
|
||||
@@ -626,7 +525,6 @@ export class Environment {
|
||||
config: EnvironmentConfig;
|
||||
fnType: ReactFunctionType;
|
||||
useMemoCacheIdentifier: string;
|
||||
hasLoweredContextAccess: boolean;
|
||||
|
||||
#contextIdentifiers: Set<t.Identifier>;
|
||||
#hoistedIdentifiers: Set<t.Identifier>;
|
||||
@@ -650,7 +548,6 @@ export class Environment {
|
||||
this.useMemoCacheIdentifier = useMemoCacheIdentifier;
|
||||
this.#shapes = new Map(DEFAULT_SHAPES);
|
||||
this.#globals = new Map(DEFAULT_GLOBALS);
|
||||
this.hasLoweredContextAccess = false;
|
||||
|
||||
if (
|
||||
config.disableMemoizationForDebugging &&
|
||||
@@ -733,42 +630,7 @@ export class Environment {
|
||||
return this.#outlinedFunctions;
|
||||
}
|
||||
|
||||
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
|
||||
if (this.config.moduleTypeProvider == null) {
|
||||
return null;
|
||||
}
|
||||
let moduleType = this.#moduleTypes.get(moduleName);
|
||||
if (moduleType === undefined) {
|
||||
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
|
||||
if (unparsedModuleConfig != null) {
|
||||
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
|
||||
if (!parsedModuleConfig.success) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Could not parse module type, the configured \`moduleTypeProvider\` function returned an invalid module description`,
|
||||
description: parsedModuleConfig.error.toString(),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const moduleConfig = parsedModuleConfig.data;
|
||||
moduleType = installTypeConfig(
|
||||
this.#globals,
|
||||
this.#shapes,
|
||||
moduleConfig,
|
||||
moduleName,
|
||||
loc,
|
||||
);
|
||||
} else {
|
||||
moduleType = null;
|
||||
}
|
||||
this.#moduleTypes.set(moduleName, moduleType);
|
||||
}
|
||||
return moduleType;
|
||||
}
|
||||
|
||||
getGlobalDeclaration(
|
||||
binding: NonLocalBinding,
|
||||
loc: SourceLocation,
|
||||
): Global | null {
|
||||
getGlobalDeclaration(binding: NonLocalBinding): Global | null {
|
||||
if (this.config.hookPattern != null) {
|
||||
const match = new RegExp(this.config.hookPattern).exec(binding.name);
|
||||
if (
|
||||
@@ -806,32 +668,6 @@ export class Environment {
|
||||
(isHookName(binding.imported) ? this.#getCustomHookType() : null)
|
||||
);
|
||||
} else {
|
||||
const moduleType = this.#resolveModuleType(binding.module, loc);
|
||||
if (moduleType !== null) {
|
||||
const importedType = this.getPropertyType(
|
||||
moduleType,
|
||||
binding.imported,
|
||||
);
|
||||
if (importedType != null) {
|
||||
/*
|
||||
* Check that hook-like export names are hook types, and non-hook names are non-hook types.
|
||||
* The user-assigned alias isn't decidable by the type provider, so we ignore that for the check.
|
||||
* Thus we allow `import {fooNonHook as useFoo} from ...` because the name and type both say
|
||||
* that it's not a hook.
|
||||
*/
|
||||
const expectHook = isHookName(binding.imported);
|
||||
const isHook = getHookKindForType(this, importedType) != null;
|
||||
if (expectHook !== isHook) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected type for \`import {${binding.imported}} from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the exported name`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return importedType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For modules we don't own, we look at whether the original name or import alias
|
||||
* are hook-like. Both of the following are likely hooks so we would return a hook
|
||||
@@ -854,34 +690,6 @@ export class Environment {
|
||||
(isHookName(binding.name) ? this.#getCustomHookType() : null)
|
||||
);
|
||||
} else {
|
||||
const moduleType = this.#resolveModuleType(binding.module, loc);
|
||||
if (moduleType !== null) {
|
||||
let importedType: Type | null = null;
|
||||
if (binding.kind === 'ImportDefault') {
|
||||
const defaultType = this.getPropertyType(moduleType, 'default');
|
||||
if (defaultType !== null) {
|
||||
importedType = defaultType;
|
||||
}
|
||||
} else {
|
||||
importedType = moduleType;
|
||||
}
|
||||
if (importedType !== null) {
|
||||
/*
|
||||
* Check that the hook-like modules are defined as types, and non hook-like modules are not typed as hooks.
|
||||
* So `import Foo from 'useFoo'` is expected to be a hook based on the module name
|
||||
*/
|
||||
const expectHook = isHookName(binding.module);
|
||||
const isHook = getHookKindForType(this, importedType) != null;
|
||||
if (expectHook !== isHook) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected type for \`import ... from '${binding.module}'\` ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the module name`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return importedType;
|
||||
}
|
||||
}
|
||||
return isHookName(binding.name) ? this.#getCustomHookType() : null;
|
||||
}
|
||||
}
|
||||
@@ -891,7 +699,9 @@ export class Environment {
|
||||
#isKnownReactModule(moduleName: string): boolean {
|
||||
return (
|
||||
moduleName.toLowerCase() === 'react' ||
|
||||
moduleName.toLowerCase() === 'react-dom'
|
||||
moduleName.toLowerCase() === 'react-dom' ||
|
||||
(this.config.enableSharedRuntime__testonly &&
|
||||
moduleName === 'shared-runtime')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
BuiltInMixedReadonlyId,
|
||||
BuiltInUseActionStateId,
|
||||
BuiltInUseContextHookId,
|
||||
BuiltInUseEffectHookId,
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
BuiltInUseLayoutEffectHookId,
|
||||
@@ -19,17 +17,12 @@ import {
|
||||
BuiltInUseReducerId,
|
||||
BuiltInUseRefId,
|
||||
BuiltInUseStateId,
|
||||
BuiltInUseTransitionId,
|
||||
ShapeRegistry,
|
||||
addFunction,
|
||||
addHook,
|
||||
addObject,
|
||||
} from './ObjectShape';
|
||||
import {BuiltInType, PolyType} from './Types';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {isHookName} from './Environment';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
|
||||
/*
|
||||
* This file exports types and defaults for JavaScript global objects.
|
||||
@@ -142,57 +135,6 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'min',
|
||||
// Math.min(value0, ..., valueN)
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'trunc',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'ceil',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'floor',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'pow',
|
||||
addFunction(DEFAULT_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
}),
|
||||
],
|
||||
]),
|
||||
],
|
||||
['Infinity', {kind: 'Primitive'}],
|
||||
@@ -303,19 +245,15 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
|
||||
const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
[
|
||||
'useContext',
|
||||
addHook(
|
||||
DEFAULT_SHAPES,
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useContext',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.Context,
|
||||
},
|
||||
BuiltInUseContextHookId,
|
||||
),
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Poly'},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useContext',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
returnValueReason: ValueReason.Context,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'useState',
|
||||
@@ -364,17 +302,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'useImperativeHandle',
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Freeze,
|
||||
returnType: {kind: 'Primitive'},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useImperativeHandle',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'useMemo',
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
@@ -442,17 +369,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
BuiltInUseInsertionEffectHookId,
|
||||
),
|
||||
],
|
||||
[
|
||||
'useTransition',
|
||||
addHook(DEFAULT_SHAPES, {
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInUseTransitionId},
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useTransition',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'use',
|
||||
addFunction(
|
||||
@@ -534,123 +450,11 @@ for (const [name, type_] of TYPED_GLOBALS) {
|
||||
DEFAULT_GLOBALS.set(name, type_);
|
||||
}
|
||||
|
||||
// Recursive global types
|
||||
// Recursive global type
|
||||
DEFAULT_GLOBALS.set(
|
||||
'globalThis',
|
||||
addObject(DEFAULT_SHAPES, 'globalThis', TYPED_GLOBALS),
|
||||
);
|
||||
DEFAULT_GLOBALS.set(
|
||||
'global',
|
||||
addObject(DEFAULT_SHAPES, 'global', TYPED_GLOBALS),
|
||||
);
|
||||
|
||||
export function installTypeConfig(
|
||||
globals: GlobalRegistry,
|
||||
shapes: ShapeRegistry,
|
||||
typeConfig: TypeConfig,
|
||||
moduleName: string,
|
||||
loc: SourceLocation,
|
||||
): Global {
|
||||
switch (typeConfig.kind) {
|
||||
case 'type': {
|
||||
switch (typeConfig.name) {
|
||||
case 'Array': {
|
||||
return {kind: 'Object', shapeId: BuiltInArrayId};
|
||||
}
|
||||
case 'MixedReadonly': {
|
||||
return {kind: 'Object', shapeId: BuiltInMixedReadonlyId};
|
||||
}
|
||||
case 'Primitive': {
|
||||
return {kind: 'Primitive'};
|
||||
}
|
||||
case 'Ref': {
|
||||
return {kind: 'Object', shapeId: BuiltInUseRefId};
|
||||
}
|
||||
case 'Any': {
|
||||
return {kind: 'Poly'};
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
typeConfig.name,
|
||||
`Unexpected type '${(typeConfig as any).name}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'function': {
|
||||
return addFunction(shapes, [], {
|
||||
positionalParams: typeConfig.positionalParams,
|
||||
restParam: typeConfig.restParam,
|
||||
calleeEffect: typeConfig.calleeEffect,
|
||||
returnType: installTypeConfig(
|
||||
globals,
|
||||
shapes,
|
||||
typeConfig.returnType,
|
||||
moduleName,
|
||||
loc,
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
mutableOnlyIfOperandsAreMutable:
|
||||
typeConfig.mutableOnlyIfOperandsAreMutable === true,
|
||||
});
|
||||
}
|
||||
case 'hook': {
|
||||
return addHook(shapes, {
|
||||
hookKind: 'Custom',
|
||||
positionalParams: typeConfig.positionalParams ?? [],
|
||||
restParam: typeConfig.restParam ?? Effect.Freeze,
|
||||
calleeEffect: Effect.Read,
|
||||
returnType: installTypeConfig(
|
||||
globals,
|
||||
shapes,
|
||||
typeConfig.returnType,
|
||||
moduleName,
|
||||
loc,
|
||||
),
|
||||
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
|
||||
noAlias: typeConfig.noAlias === true,
|
||||
});
|
||||
}
|
||||
case 'object': {
|
||||
return addObject(
|
||||
shapes,
|
||||
null,
|
||||
Object.entries(typeConfig.properties ?? {}).map(([key, value]) => {
|
||||
const type = installTypeConfig(
|
||||
globals,
|
||||
shapes,
|
||||
value,
|
||||
moduleName,
|
||||
loc,
|
||||
);
|
||||
const expectHook = isHookName(key);
|
||||
let isHook = false;
|
||||
if (type.kind === 'Function' && type.shapeId !== null) {
|
||||
const functionType = shapes.get(type.shapeId);
|
||||
if (functionType?.functionType?.hookKind !== null) {
|
||||
isHook = true;
|
||||
}
|
||||
}
|
||||
if (expectHook !== isHook) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: `Invalid type configuration for module`,
|
||||
description: `Expected type for object property '${key}' from module '${moduleName}' ${expectHook ? 'to be a hook' : 'not to be a hook'} based on the property name`,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return [key, type];
|
||||
}),
|
||||
);
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
typeConfig,
|
||||
`Unexpected type kind '${(typeConfig as any).kind}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function installReAnimatedTypes(
|
||||
globals: GlobalRegistry,
|
||||
|
||||
@@ -11,8 +11,7 @@ import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {Environment, ReactFunctionType} from './Environment';
|
||||
import {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import {Type} from './Types';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -285,8 +284,7 @@ export type HIRFunction = {
|
||||
fnType: ReactFunctionType;
|
||||
env: Environment;
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returnType: Type;
|
||||
returnType: t.FlowType | t.TSType | null;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
@@ -367,7 +365,6 @@ export type BasicBlock = {
|
||||
preds: Set<BlockId>;
|
||||
phis: Set<Phi>;
|
||||
};
|
||||
export type TBasicBlock<T extends Terminal> = BasicBlock & {terminal: T};
|
||||
|
||||
/*
|
||||
* Terminal nodes generally represent statements that affect control flow, such as
|
||||
@@ -492,7 +489,7 @@ export type BranchTerminal = {
|
||||
alternate: BlockId;
|
||||
id: InstructionId;
|
||||
loc: SourceLocation;
|
||||
fallthrough: BlockId;
|
||||
fallthrough?: never;
|
||||
};
|
||||
|
||||
export type SwitchTerminal = {
|
||||
@@ -744,12 +741,6 @@ export enum InstructionKind {
|
||||
|
||||
// hoisted const declarations
|
||||
HoistedConst = 'HoistedConst',
|
||||
|
||||
// hoisted const declarations
|
||||
HoistedLet = 'HoistedLet',
|
||||
|
||||
HoistedFunction = 'HoistedFunction',
|
||||
Function = 'Function',
|
||||
}
|
||||
|
||||
function _staticInvariantInstructionValueHasLocation(
|
||||
@@ -763,6 +754,7 @@ export type Phi = {
|
||||
kind: 'Phi';
|
||||
id: Identifier;
|
||||
operands: Map<BlockId, Identifier>;
|
||||
type: Type;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -780,7 +772,7 @@ export type ManualMemoDependency = {
|
||||
value: Place;
|
||||
}
|
||||
| {kind: 'Global'; identifierName: string};
|
||||
path: DependencyPath;
|
||||
path: Array<string>;
|
||||
};
|
||||
|
||||
export type StartMemoize = {
|
||||
@@ -866,16 +858,18 @@ export type InstructionValue =
|
||||
| {
|
||||
kind: 'DeclareContext';
|
||||
lvalue: {
|
||||
kind:
|
||||
| InstructionKind.Let
|
||||
| InstructionKind.HoistedConst
|
||||
| InstructionKind.HoistedLet
|
||||
| InstructionKind.HoistedFunction;
|
||||
kind: InstructionKind.Let | InstructionKind.HoistedConst;
|
||||
place: Place;
|
||||
};
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| StoreLocal
|
||||
| {
|
||||
kind: 'StoreLocal';
|
||||
lvalue: LValue;
|
||||
value: Place;
|
||||
type: t.FlowType | t.TSType | null;
|
||||
loc: SourceLocation;
|
||||
}
|
||||
| {
|
||||
kind: 'StoreContext';
|
||||
lvalue: {
|
||||
@@ -1082,10 +1076,10 @@ export type FunctionExpression = {
|
||||
kind: 'FunctionExpression';
|
||||
name: string | null;
|
||||
loweredFunc: LoweredFunction;
|
||||
type:
|
||||
| 'ArrowFunctionExpression'
|
||||
| 'FunctionExpression'
|
||||
| 'FunctionDeclaration';
|
||||
expr:
|
||||
| t.ArrowFunctionExpression
|
||||
| t.FunctionExpression
|
||||
| t.FunctionDeclaration;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -1118,13 +1112,6 @@ export type Primitive = {
|
||||
|
||||
export type JSXText = {kind: 'JSXText'; value: string; loc: SourceLocation};
|
||||
|
||||
export type StoreLocal = {
|
||||
kind: 'StoreLocal';
|
||||
lvalue: LValue;
|
||||
value: Place;
|
||||
type: t.FlowType | t.TSType | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
export type PropertyLoad = {
|
||||
kind: 'PropertyLoad';
|
||||
object: Place;
|
||||
@@ -1187,19 +1174,11 @@ export type NonLocalBinding =
|
||||
|
||||
// Represents a user-defined variable (has a name) or a temporary variable (no name).
|
||||
export type Identifier = {
|
||||
/**
|
||||
* After EnterSSA, `id` uniquely identifies an SSA instance of a variable.
|
||||
* Before EnterSSA, `id` matches `declarationId`.
|
||||
/*
|
||||
* unique value to distinguish a variable, since name is not guaranteed to
|
||||
* exist or be unique
|
||||
*/
|
||||
id: IdentifierId;
|
||||
|
||||
/**
|
||||
* Uniquely identifies a given variable in the original program. If a value is
|
||||
* reassigned in the original program each reassigned value will have a distinct
|
||||
* `id` (after EnterSSA), but they will still have the same `declarationId`.
|
||||
*/
|
||||
declarationId: DeclarationId;
|
||||
|
||||
// null for temporaries. name is primarily used for debugging.
|
||||
name: IdentifierName | null;
|
||||
// The range for which this variable is mutable
|
||||
@@ -1226,21 +1205,6 @@ export type ValidIdentifierName = string & {
|
||||
[opaqueValidIdentifierName]: 'ValidIdentifierName';
|
||||
};
|
||||
|
||||
export function makeTemporaryIdentifier(
|
||||
id: IdentifierId,
|
||||
loc: SourceLocation,
|
||||
): Identifier {
|
||||
return {
|
||||
id,
|
||||
name: null,
|
||||
declarationId: makeDeclarationId(id),
|
||||
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid identifier name. This should *not* be used for synthesizing
|
||||
* identifier names: only call this method for identifier names that appear in the
|
||||
@@ -1261,9 +1225,6 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
|
||||
|
||||
/**
|
||||
* Given an unnamed identifier, promote it to a named identifier.
|
||||
*
|
||||
* Note: this uses the identifier's DeclarationId to ensure that all
|
||||
* instances of the same declaration will have the same name.
|
||||
*/
|
||||
export function promoteTemporary(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
@@ -1274,7 +1235,7 @@ export function promoteTemporary(identifier: Identifier): void {
|
||||
});
|
||||
identifier.name = {
|
||||
kind: 'promoted',
|
||||
value: `#t${identifier.declarationId}`,
|
||||
value: `#t${identifier.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1285,9 +1246,6 @@ export function isPromotedTemporary(name: string): boolean {
|
||||
/**
|
||||
* Given an unnamed identifier, promote it to a named identifier, distinguishing
|
||||
* it as a value that needs to be capitalized since it appears in JSX element tag position
|
||||
*
|
||||
* Note: this uses the identifier's DeclarationId to ensure that all
|
||||
* instances of the same declaration will have the same name.
|
||||
*/
|
||||
export function promoteTemporaryJsxTag(identifier: Identifier): void {
|
||||
CompilerError.invariant(identifier.name === null, {
|
||||
@@ -1298,7 +1256,7 @@ export function promoteTemporaryJsxTag(identifier: Identifier): void {
|
||||
});
|
||||
identifier.name = {
|
||||
kind: 'promoted',
|
||||
value: `#T${identifier.declarationId}`,
|
||||
value: `#T${identifier.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1367,15 +1325,6 @@ export enum ValueKind {
|
||||
Context = 'context',
|
||||
}
|
||||
|
||||
export const ValueKindSchema = z.enum([
|
||||
ValueKind.MaybeFrozen,
|
||||
ValueKind.Frozen,
|
||||
ValueKind.Primitive,
|
||||
ValueKind.Global,
|
||||
ValueKind.Mutable,
|
||||
ValueKind.Context,
|
||||
]);
|
||||
|
||||
// The effect with which a value is modified.
|
||||
export enum Effect {
|
||||
// Default value: not allowed after lifetime inference
|
||||
@@ -1405,15 +1354,6 @@ export enum Effect {
|
||||
Store = 'store',
|
||||
}
|
||||
|
||||
export const EffectSchema = z.enum([
|
||||
Effect.Read,
|
||||
Effect.Mutate,
|
||||
Effect.ConditionallyMutate,
|
||||
Effect.Capture,
|
||||
Effect.Store,
|
||||
Effect.Freeze,
|
||||
]);
|
||||
|
||||
export function isMutableEffect(
|
||||
effect: Effect,
|
||||
location: SourceLocation,
|
||||
@@ -1498,38 +1438,11 @@ export type ReactiveScopeDeclaration = {
|
||||
scope: ReactiveScope; // the scope in which the variable was originally declared
|
||||
};
|
||||
|
||||
export type DependencyPathEntry = {property: string; optional: boolean};
|
||||
export type DependencyPath = Array<DependencyPathEntry>;
|
||||
export type ReactiveScopeDependency = {
|
||||
identifier: Identifier;
|
||||
path: DependencyPath;
|
||||
path: Array<string>;
|
||||
};
|
||||
|
||||
export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
|
||||
return (
|
||||
a.length === b.length &&
|
||||
a.every(
|
||||
(item, ix) =>
|
||||
item.property === b[ix].property && item.optional === b[ix].optional,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlaceScope(
|
||||
id: InstructionId,
|
||||
place: Place,
|
||||
): ReactiveScope | null {
|
||||
const scope = place.identifier.scope;
|
||||
if (scope !== null && isScopeActive(scope, id)) {
|
||||
return scope;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
|
||||
return id >= scope.range.start && id < scope.range.end;
|
||||
}
|
||||
|
||||
/*
|
||||
* Simulated opaque type for BlockIds to prevent using normal numbers as block ids
|
||||
* accidentally.
|
||||
@@ -1581,23 +1494,6 @@ export function makeIdentifierId(id: number): IdentifierId {
|
||||
return id as IdentifierId;
|
||||
}
|
||||
|
||||
/*
|
||||
* Simulated opaque type for IdentifierId to prevent using normal numbers as ids
|
||||
* accidentally.
|
||||
*/
|
||||
const opageDeclarationId = Symbol();
|
||||
export type DeclarationId = number & {[opageDeclarationId]: 'DeclarationId'};
|
||||
|
||||
export function makeDeclarationId(id: number): DeclarationId {
|
||||
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
|
||||
reason: 'Expected declaration id to be a non-negative integer',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
return id as DeclarationId;
|
||||
}
|
||||
|
||||
/*
|
||||
* Simulated opaque type for InstructionId to prevent using normal numbers as ids
|
||||
* accidentally.
|
||||
@@ -1643,10 +1539,6 @@ export function isUseStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseState';
|
||||
}
|
||||
|
||||
export function isRefOrRefValue(id: Identifier): boolean {
|
||||
return isUseRefType(id) || isRefValueType(id);
|
||||
}
|
||||
|
||||
export function isSetStateType(id: Identifier): boolean {
|
||||
return id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetState';
|
||||
}
|
||||
@@ -1657,12 +1549,6 @@ export function isUseActionStateType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isStartTransitionType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInStartTransition'
|
||||
);
|
||||
}
|
||||
|
||||
export function isSetActionStateType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
|
||||
@@ -1678,13 +1564,7 @@ export function isDispatcherType(id: Identifier): boolean {
|
||||
}
|
||||
|
||||
export function isStableType(id: Identifier): boolean {
|
||||
return (
|
||||
isSetStateType(id) ||
|
||||
isSetActionStateType(id) ||
|
||||
isDispatcherType(id) ||
|
||||
isUseRefType(id) ||
|
||||
isStartTransitionType(id)
|
||||
);
|
||||
return isSetStateType(id) || isSetActionStateType(id) || isDispatcherType(id);
|
||||
}
|
||||
|
||||
export function isUseEffectHookType(id: Identifier): boolean {
|
||||
@@ -1705,12 +1585,6 @@ export function isUseInsertionEffectHookType(id: Identifier): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isUseContextHookType(id: Identifier): boolean {
|
||||
return (
|
||||
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseContextHook'
|
||||
);
|
||||
}
|
||||
|
||||
export function getHookKind(env: Environment, id: Identifier): HookKind | null {
|
||||
return getHookKindForType(env, id.type);
|
||||
}
|
||||
|
||||
@@ -25,10 +25,8 @@ import {
|
||||
Terminal,
|
||||
VariableBinding,
|
||||
makeBlockId,
|
||||
makeDeclarationId,
|
||||
makeIdentifierName,
|
||||
makeInstructionId,
|
||||
makeTemporaryIdentifier,
|
||||
makeType,
|
||||
} from './HIR';
|
||||
import {printInstruction} from './PrintHIR';
|
||||
@@ -184,7 +182,14 @@ export default class HIRBuilder {
|
||||
|
||||
makeTemporary(loc: SourceLocation): Identifier {
|
||||
const id = this.nextIdentifierId;
|
||||
return makeTemporaryIdentifier(id, loc);
|
||||
return {
|
||||
id,
|
||||
name: null,
|
||||
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
#resolveBabelBinding(
|
||||
@@ -321,7 +326,6 @@ export default class HIRBuilder {
|
||||
const id = this.nextIdentifierId;
|
||||
const identifier: Identifier = {
|
||||
id,
|
||||
declarationId: makeDeclarationId(id),
|
||||
name: makeIdentifierName(name),
|
||||
mutableRange: {
|
||||
start: makeInstructionId(0),
|
||||
@@ -893,42 +897,16 @@ export function createTemporaryPlace(
|
||||
): Place {
|
||||
return {
|
||||
kind: 'Identifier',
|
||||
identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
|
||||
identifier: {
|
||||
id: env.nextIdentifierId,
|
||||
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc,
|
||||
},
|
||||
reactive: false,
|
||||
effect: Effect.Unknown,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an existing Place, returning a new temporary Place that shares the
|
||||
* same metadata properties as the original place (effect, reactive flag, type)
|
||||
* but has a new, temporary Identifier.
|
||||
*/
|
||||
export function clonePlaceToTemporary(env: Environment, place: Place): Place {
|
||||
const temp = createTemporaryPlace(env, place.loc);
|
||||
temp.effect = place.effect;
|
||||
temp.identifier.type = place.identifier.type;
|
||||
temp.reactive = place.reactive;
|
||||
return temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix scope and identifier ranges to account for renumbered instructions
|
||||
*/
|
||||
export function fixScopeAndIdentifierRanges(func: HIR): void {
|
||||
for (const [, block] of func.blocks) {
|
||||
const terminal = block.terminal;
|
||||
if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
|
||||
/*
|
||||
* Scope ranges should always align to start at the 'scope' terminal
|
||||
* and end at the first instruction of the fallthrough block
|
||||
*/
|
||||
const fallthroughBlock = func.blocks.get(terminal.fallthrough)!;
|
||||
const firstId =
|
||||
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
|
||||
terminal.scope.range.start = terminal.id;
|
||||
terminal.scope.range.end = firstId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import {terminalFallthrough, terminalHasFallthrough} from './visitors';
|
||||
* Note that this pass leaves value/loop blocks alone because they cannot
|
||||
* be merged without breaking the structure of the high-level terminals
|
||||
* that reference them.
|
||||
*
|
||||
* TODO @josephsavona make value blocks explicit (eg a `kind` on Block).
|
||||
*/
|
||||
export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
const merged = new MergedBlocks();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ReactiveScope,
|
||||
makeInstructionId,
|
||||
} from '.';
|
||||
import {getPlaceScope} from '../HIR/HIR';
|
||||
import {getPlaceScope} from '../ReactiveScopes/BuildReactiveBlocks';
|
||||
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
|
||||
@@ -126,8 +126,6 @@ export type HookKind =
|
||||
| 'useInsertionEffect'
|
||||
| 'useMemo'
|
||||
| 'useCallback'
|
||||
| 'useTransition'
|
||||
| 'useImperativeHandle'
|
||||
| 'Custom';
|
||||
|
||||
/*
|
||||
@@ -210,9 +208,6 @@ export const BuiltInUseInsertionEffectHookId = 'BuiltInUseInsertionEffectHook';
|
||||
export const BuiltInUseOperatorId = 'BuiltInUseOperator';
|
||||
export const BuiltInUseReducerId = 'BuiltInUseReducer';
|
||||
export const BuiltInDispatchId = 'BuiltInDispatch';
|
||||
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
|
||||
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
|
||||
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
|
||||
|
||||
// ShapeRegistry with default definitions for built-ins.
|
||||
export const BUILTIN_SHAPES: ShapeRegistry = new Map();
|
||||
@@ -318,23 +313,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'flatMap',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.ConditionallyMutate,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
|
||||
/*
|
||||
* callee is ConditionallyMutate because items of the array
|
||||
* flow into the lambda and may be mutated there, even though
|
||||
* the array object itself is not modified
|
||||
*/
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'filter',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
@@ -465,25 +443,6 @@ addObject(BUILTIN_SHAPES, BuiltInUseStateId, [
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
|
||||
['0', {kind: 'Primitive'}],
|
||||
[
|
||||
'1',
|
||||
addFunction(
|
||||
BUILTIN_SHAPES,
|
||||
[],
|
||||
{
|
||||
positionalParams: [],
|
||||
restParam: null,
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Read,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
},
|
||||
BuiltInStartTransitionId,
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
|
||||
['0', {kind: 'Poly'}],
|
||||
[
|
||||
@@ -552,17 +511,6 @@ addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
|
||||
noAlias: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'flatMap',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
positionalParams: [],
|
||||
restParam: Effect.Read,
|
||||
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
|
||||
calleeEffect: Effect.ConditionallyMutate,
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'filter',
|
||||
addFunction(BUILTIN_SHAPES, [], {
|
||||
|
||||
@@ -72,7 +72,6 @@ export function printFunction(fn: HIRFunction): string {
|
||||
if (definition.length !== 0) {
|
||||
output.push(definition);
|
||||
}
|
||||
output.push(printType(fn.returnType));
|
||||
output.push(printHIR(fn.body));
|
||||
output.push(...fn.directives);
|
||||
return output.join('\n');
|
||||
@@ -165,7 +164,7 @@ export function printPhi(phi: Phi): string {
|
||||
const items = [];
|
||||
items.push(printIdentifier(phi.id));
|
||||
items.push(printMutableRange(phi.id));
|
||||
items.push(printType(phi.id.type));
|
||||
items.push(printType(phi.type));
|
||||
items.push(': phi(');
|
||||
const phis = [];
|
||||
for (const [blockId, id] of phi.operands) {
|
||||
@@ -191,7 +190,7 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
case 'branch': {
|
||||
value = `[${terminal.id}] Branch (${printPlace(terminal.test)}) then:bb${
|
||||
terminal.consequent
|
||||
} else:bb${terminal.alternate} fallthrough:bb${terminal.fallthrough}`;
|
||||
} else:bb${terminal.alternate}`;
|
||||
break;
|
||||
}
|
||||
case 'logical': {
|
||||
@@ -556,8 +555,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const type = printType(instrValue.loweredFunc.func.returnType).trim();
|
||||
value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
|
||||
value = `${kind} ${name} @deps[${deps}] @context[${context}] @effects[${effects}]:\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -762,15 +760,6 @@ export function printLValue(lval: LValue): string {
|
||||
case InstructionKind.HoistedConst: {
|
||||
return `HoistedConst ${lvalue}$`;
|
||||
}
|
||||
case InstructionKind.HoistedLet: {
|
||||
return `HoistedLet ${lvalue}$`;
|
||||
}
|
||||
case InstructionKind.Function: {
|
||||
return `Function ${lvalue}$`;
|
||||
}
|
||||
case InstructionKind.HoistedFunction: {
|
||||
return `HoistedFunction ${lvalue}$`;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``);
|
||||
}
|
||||
@@ -875,7 +864,7 @@ export function printManualMemoDependency(
|
||||
? val.root.value.identifier.name.value
|
||||
: printIdentifier(val.root.value.identifier);
|
||||
}
|
||||
return `${rootStr}${val.path.map(v => `${v.optional ? '?.' : '.'}${v.property}`).join('')}`;
|
||||
return `${rootStr}${val.path.length > 0 ? '.' : ''}${val.path.join('.')}`;
|
||||
}
|
||||
export function printType(type: Type): string {
|
||||
if (type.kind === 'Type') return '';
|
||||
|
||||
@@ -1,627 +0,0 @@
|
||||
import {
|
||||
ScopeId,
|
||||
HIRFunction,
|
||||
Place,
|
||||
Instruction,
|
||||
ReactiveScopeDependency,
|
||||
Identifier,
|
||||
ReactiveScope,
|
||||
isObjectMethodType,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
makeInstructionId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
GeneratedSource,
|
||||
DeclarationId,
|
||||
areEqualPaths,
|
||||
IdentifierId,
|
||||
} from './HIR';
|
||||
import {collectHoistablePropertyLoads} from './CollectHoistablePropertyLoads';
|
||||
import {
|
||||
ScopeBlockTraversal,
|
||||
eachInstructionOperand,
|
||||
eachInstructionValueOperand,
|
||||
eachPatternOperand,
|
||||
eachTerminalOperand,
|
||||
} from './visitors';
|
||||
import {Stack, empty} from '../Utils/Stack';
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Iterable_some} from '../Utils/utils';
|
||||
import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
|
||||
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
|
||||
|
||||
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
|
||||
const usedOutsideDeclaringScope =
|
||||
findTemporariesUsedOutsideDeclaringScope(fn);
|
||||
const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope);
|
||||
const {
|
||||
temporariesReadInOptional,
|
||||
processedInstrsInOptional,
|
||||
hoistableObjects,
|
||||
} = collectOptionalChainSidemap(fn);
|
||||
|
||||
const hoistablePropertyLoads = collectHoistablePropertyLoads(
|
||||
fn,
|
||||
temporaries,
|
||||
hoistableObjects,
|
||||
);
|
||||
|
||||
const scopeDeps = collectDependencies(
|
||||
fn,
|
||||
usedOutsideDeclaringScope,
|
||||
new Map([...temporaries, ...temporariesReadInOptional]),
|
||||
processedInstrsInOptional,
|
||||
);
|
||||
|
||||
/**
|
||||
* Derive the minimal set of hoistable dependencies for each scope.
|
||||
*/
|
||||
for (const [scope, deps] of scopeDeps) {
|
||||
if (deps.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1: Find hoistable accesses, given the basic block in which the scope
|
||||
* begins.
|
||||
*/
|
||||
const hoistables = hoistablePropertyLoads.get(scope.id);
|
||||
CompilerError.invariant(hoistables != null, {
|
||||
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
|
||||
loc: GeneratedSource,
|
||||
});
|
||||
/**
|
||||
* Step 2: Calculate hoistable dependencies.
|
||||
*/
|
||||
const tree = new ReactiveScopeDependencyTreeHIR(
|
||||
[...hoistables.assumedNonNullObjects].map(o => o.fullPath),
|
||||
);
|
||||
for (const dep of deps) {
|
||||
tree.addDependency({...dep});
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3: Reduce dependencies to a minimal set.
|
||||
*/
|
||||
const candidates = tree.deriveMinimalDependencies();
|
||||
for (const candidateDep of candidates) {
|
||||
if (
|
||||
!Iterable_some(
|
||||
scope.dependencies,
|
||||
existingDep =>
|
||||
existingDep.identifier.declarationId ===
|
||||
candidateDep.identifier.declarationId &&
|
||||
areEqualPaths(existingDep.path, candidateDep.path),
|
||||
)
|
||||
)
|
||||
scope.dependencies.add(candidateDep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findTemporariesUsedOutsideDeclaringScope(
|
||||
fn: HIRFunction,
|
||||
): ReadonlySet<DeclarationId> {
|
||||
/*
|
||||
* tracks all relevant LoadLocal and PropertyLoad lvalues
|
||||
* and the scope where they are defined
|
||||
*/
|
||||
const declarations = new Map<DeclarationId, ScopeId>();
|
||||
const prunedScopes = new Set<ScopeId>();
|
||||
const scopeTraversal = new ScopeBlockTraversal();
|
||||
const usedOutsideDeclaringScope = new Set<DeclarationId>();
|
||||
|
||||
function handlePlace(place: Place): void {
|
||||
const declaringScope = declarations.get(place.identifier.declarationId);
|
||||
if (
|
||||
declaringScope != null &&
|
||||
!scopeTraversal.isScopeActive(declaringScope) &&
|
||||
!prunedScopes.has(declaringScope)
|
||||
) {
|
||||
// Declaring scope is not active === used outside declaring scope
|
||||
usedOutsideDeclaringScope.add(place.identifier.declarationId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleInstruction(instr: Instruction): void {
|
||||
const scope = scopeTraversal.currentScope;
|
||||
if (scope == null || prunedScopes.has(scope)) {
|
||||
return;
|
||||
}
|
||||
switch (instr.value.kind) {
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext':
|
||||
case 'PropertyLoad': {
|
||||
declarations.set(instr.lvalue.identifier.declarationId, scope);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
scopeTraversal.recordScopes(block);
|
||||
const scopeStartInfo = scopeTraversal.blockInfos.get(blockId);
|
||||
if (scopeStartInfo?.kind === 'begin' && scopeStartInfo.pruned) {
|
||||
prunedScopes.add(scopeStartInfo.scope.id);
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const place of eachInstructionOperand(instr)) {
|
||||
handlePlace(place);
|
||||
}
|
||||
handleInstruction(instr);
|
||||
}
|
||||
|
||||
for (const place of eachTerminalOperand(block.terminal)) {
|
||||
handlePlace(place);
|
||||
}
|
||||
}
|
||||
return usedOutsideDeclaringScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns mapping of LoadLocal and PropertyLoad to the source of the load.
|
||||
* ```js
|
||||
* // source
|
||||
* foo(a.b);
|
||||
*
|
||||
* // HIR: a potential sidemap is {0: a, 1: a.b, 2: foo}
|
||||
* $0 = LoadLocal 'a'
|
||||
* $1 = PropertyLoad $0, 'b'
|
||||
* $2 = LoadLocal 'foo'
|
||||
* $3 = CallExpression $2($1)
|
||||
* ```
|
||||
* Only map LoadLocal and PropertyLoad lvalues to their source if we know that
|
||||
* reordering the read (from the time-of-load to time-of-use) is valid.
|
||||
*
|
||||
* If a LoadLocal or PropertyLoad instruction is within the reactive scope range
|
||||
* (a proxy for mutable range) of the load source, later instructions may
|
||||
* reassign / mutate the source value. Since it's incorrect to reorder these
|
||||
* load instructions to after their scope ranges, we also do not store them in
|
||||
* identifier sidemaps.
|
||||
*
|
||||
* Take this example (from fixture
|
||||
* `evaluation-order-mutate-call-after-dependency-load`)
|
||||
* ```js
|
||||
* // source
|
||||
* function useFoo(arg) {
|
||||
* const arr = [1, 2, 3, ...arg];
|
||||
* return [
|
||||
* arr.length,
|
||||
* arr.push(0)
|
||||
* ];
|
||||
* }
|
||||
*
|
||||
* // IR pseudocode
|
||||
* scope @0 {
|
||||
* $0 = arr = ArrayExpression [1, 2, 3, ...arg]
|
||||
* $1 = arr.length
|
||||
* $2 = arr.push(0)
|
||||
* }
|
||||
* scope @1 {
|
||||
* $3 = ArrayExpression [$1, $2]
|
||||
* }
|
||||
* ```
|
||||
* Here, it's invalid for scope@1 to take `arr.length` as a dependency instead
|
||||
* of $1, as the evaluation of `arr.length` changes between instructions $1 and
|
||||
* $3. We do not track $1 -> arr.length in this case.
|
||||
*/
|
||||
function collectTemporariesSidemap(
|
||||
fn: HIRFunction,
|
||||
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
|
||||
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
|
||||
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
const usedOutside = usedOutsideDeclaringScope.has(
|
||||
lvalue.identifier.declarationId,
|
||||
);
|
||||
|
||||
if (value.kind === 'PropertyLoad' && !usedOutside) {
|
||||
const property = getProperty(
|
||||
value.object,
|
||||
value.property,
|
||||
false,
|
||||
temporaries,
|
||||
);
|
||||
temporaries.set(lvalue.identifier.id, property);
|
||||
} else if (
|
||||
value.kind === 'LoadLocal' &&
|
||||
lvalue.identifier.name == null &&
|
||||
value.place.identifier.name !== null &&
|
||||
!usedOutside
|
||||
) {
|
||||
temporaries.set(lvalue.identifier.id, {
|
||||
identifier: value.place.identifier,
|
||||
path: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return temporaries;
|
||||
}
|
||||
|
||||
function getProperty(
|
||||
object: Place,
|
||||
propertyName: string,
|
||||
optional: boolean,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
): ReactiveScopeDependency {
|
||||
/*
|
||||
* (1) Get the base object either from the temporary sidemap (e.g. a LoadLocal)
|
||||
* or a deep copy of an existing property dependency.
|
||||
* Example 1:
|
||||
* $0 = LoadLocal x
|
||||
* $1 = PropertyLoad $0.y
|
||||
* getProperty($0, ...) -> resolvedObject = x, resolvedDependency = null
|
||||
*
|
||||
* Example 2:
|
||||
* $0 = LoadLocal x
|
||||
* $1 = PropertyLoad $0.y
|
||||
* $2 = PropertyLoad $1.z
|
||||
* getProperty($1, ...) -> resolvedObject = null, resolvedDependency = x.y
|
||||
*
|
||||
* Example 3:
|
||||
* $0 = Call(...)
|
||||
* $1 = PropertyLoad $0.y
|
||||
* getProperty($0, ...) -> resolvedObject = null, resolvedDependency = null
|
||||
*/
|
||||
const resolvedDependency = temporaries.get(object.identifier.id);
|
||||
|
||||
/**
|
||||
* (2) Push the last PropertyLoad
|
||||
* TODO(mofeiZ): understand optional chaining
|
||||
*/
|
||||
let property: ReactiveScopeDependency;
|
||||
if (resolvedDependency == null) {
|
||||
property = {
|
||||
identifier: object.identifier,
|
||||
path: [{property: propertyName, optional}],
|
||||
};
|
||||
} else {
|
||||
property = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
path: [...resolvedDependency.path, {property: propertyName, optional}],
|
||||
};
|
||||
}
|
||||
return property;
|
||||
}
|
||||
|
||||
type Decl = {
|
||||
id: InstructionId;
|
||||
scope: Stack<ReactiveScope>;
|
||||
};
|
||||
|
||||
class Context {
|
||||
#declarations: Map<DeclarationId, Decl> = new Map();
|
||||
#reassignments: Map<Identifier, Decl> = new Map();
|
||||
|
||||
#scopes: Stack<ReactiveScope> = empty();
|
||||
// Reactive dependencies used in the current reactive scope.
|
||||
#dependencies: Stack<Array<ReactiveScopeDependency>> = empty();
|
||||
deps: Map<ReactiveScope, Array<ReactiveScopeDependency>> = new Map();
|
||||
|
||||
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
|
||||
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;
|
||||
|
||||
constructor(
|
||||
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
) {
|
||||
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
|
||||
this.#temporaries = temporaries;
|
||||
}
|
||||
|
||||
enterScope(scope: ReactiveScope): void {
|
||||
// Set context for new scope
|
||||
this.#dependencies = this.#dependencies.push([]);
|
||||
this.#scopes = this.#scopes.push(scope);
|
||||
}
|
||||
|
||||
exitScope(scope: ReactiveScope, pruned: boolean): void {
|
||||
// Save dependencies we collected from the exiting scope
|
||||
const scopedDependencies = this.#dependencies.value;
|
||||
CompilerError.invariant(scopedDependencies != null, {
|
||||
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
|
||||
loc: scope.loc,
|
||||
});
|
||||
|
||||
// Restore context of previous scope
|
||||
this.#scopes = this.#scopes.pop();
|
||||
this.#dependencies = this.#dependencies.pop();
|
||||
|
||||
/*
|
||||
* Collect dependencies we recorded for the exiting scope and propagate
|
||||
* them upward using the same rules as normal dependency collection.
|
||||
* Child scopes may have dependencies on values created within the outer
|
||||
* scope, which necessarily cannot be dependencies of the outer scope.
|
||||
*/
|
||||
for (const dep of scopedDependencies) {
|
||||
if (this.#checkValidDependency(dep)) {
|
||||
this.#dependencies.value?.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pruned) {
|
||||
this.deps.set(scope, scopedDependencies);
|
||||
}
|
||||
}
|
||||
|
||||
isUsedOutsideDeclaringScope(place: Place): boolean {
|
||||
return this.#temporariesUsedOutsideScope.has(
|
||||
place.identifier.declarationId,
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Records where a value was declared, and optionally, the scope where the value originated from.
|
||||
* This is later used to determine if a dependency should be added to a scope; if the current
|
||||
* scope we are visiting is the same scope where the value originates, it can't be a dependency
|
||||
* on itself.
|
||||
*/
|
||||
declare(identifier: Identifier, decl: Decl): void {
|
||||
if (!this.#declarations.has(identifier.declarationId)) {
|
||||
this.#declarations.set(identifier.declarationId, decl);
|
||||
}
|
||||
this.#reassignments.set(identifier, decl);
|
||||
}
|
||||
|
||||
// Checks if identifier is a valid dependency in the current scope
|
||||
#checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean {
|
||||
// ref.current access is not a valid dep
|
||||
if (
|
||||
isUseRefType(maybeDependency.identifier) &&
|
||||
maybeDependency.path.at(0)?.property === 'current'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ref value is not a valid dep
|
||||
if (isRefValueType(maybeDependency.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* object methods are not deps because they will be codegen'ed back in to
|
||||
* the object literal.
|
||||
*/
|
||||
if (isObjectMethodType(maybeDependency.identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const identifier = maybeDependency.identifier;
|
||||
/*
|
||||
* If this operand is used in a scope, has a dynamic value, and was defined
|
||||
* before this scope, then its a dependency of the scope.
|
||||
*/
|
||||
const currentDeclaration =
|
||||
this.#reassignments.get(identifier) ??
|
||||
this.#declarations.get(identifier.declarationId);
|
||||
const currentScope = this.currentScope.value;
|
||||
return (
|
||||
currentScope != null &&
|
||||
currentDeclaration !== undefined &&
|
||||
currentDeclaration.id < currentScope.range.start
|
||||
);
|
||||
}
|
||||
|
||||
#isScopeActive(scope: ReactiveScope): boolean {
|
||||
if (this.#scopes === null) {
|
||||
return false;
|
||||
}
|
||||
return this.#scopes.find(state => state === scope);
|
||||
}
|
||||
|
||||
get currentScope(): Stack<ReactiveScope> {
|
||||
return this.#scopes;
|
||||
}
|
||||
|
||||
visitOperand(place: Place): void {
|
||||
/*
|
||||
* if this operand is a temporary created for a property load, try to resolve it to
|
||||
* the expanded Place. Fall back to using the operand as-is.
|
||||
*/
|
||||
this.visitDependency(
|
||||
this.#temporaries.get(place.identifier.id) ?? {
|
||||
identifier: place.identifier,
|
||||
path: [],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
visitProperty(object: Place, property: string, optional: boolean): void {
|
||||
const nextDependency = getProperty(
|
||||
object,
|
||||
property,
|
||||
optional,
|
||||
this.#temporaries,
|
||||
);
|
||||
this.visitDependency(nextDependency);
|
||||
}
|
||||
|
||||
visitDependency(maybeDependency: ReactiveScopeDependency): void {
|
||||
/*
|
||||
* Any value used after its originally defining scope has concluded must be added as an
|
||||
* output of its defining scope. Regardless of whether its a const or not,
|
||||
* some later code needs access to the value. If the current
|
||||
* scope we are visiting is the same scope where the value originates, it can't be a dependency
|
||||
* on itself.
|
||||
*/
|
||||
|
||||
/*
|
||||
* if originalDeclaration is undefined here, then this is not a local var
|
||||
* (all decls e.g. `let x;` should be initialized in BuildHIR)
|
||||
*/
|
||||
const originalDeclaration = this.#declarations.get(
|
||||
maybeDependency.identifier.declarationId,
|
||||
);
|
||||
if (
|
||||
originalDeclaration !== undefined &&
|
||||
originalDeclaration.scope.value !== null
|
||||
) {
|
||||
originalDeclaration.scope.each(scope => {
|
||||
if (
|
||||
!this.#isScopeActive(scope) &&
|
||||
!Iterable_some(
|
||||
scope.declarations.values(),
|
||||
decl =>
|
||||
decl.identifier.declarationId ===
|
||||
maybeDependency.identifier.declarationId,
|
||||
)
|
||||
) {
|
||||
scope.declarations.set(maybeDependency.identifier.id, {
|
||||
identifier: maybeDependency.identifier,
|
||||
scope: originalDeclaration.scope.value!,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.#checkValidDependency(maybeDependency)) {
|
||||
this.#dependencies.value!.push(maybeDependency);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Record a variable that is declared in some other scope and that is being reassigned in the
|
||||
* current one as a {@link ReactiveScope.reassignments}
|
||||
*/
|
||||
visitReassignment(place: Place): void {
|
||||
const currentScope = this.currentScope.value;
|
||||
if (
|
||||
currentScope != null &&
|
||||
!Iterable_some(
|
||||
currentScope.reassignments,
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
) &&
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
) {
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleInstruction(instr: Instruction, context: Context): void {
|
||||
const {id, value, lvalue} = instr;
|
||||
if (value.kind === 'LoadLocal') {
|
||||
if (
|
||||
value.place.identifier.name === null ||
|
||||
lvalue.identifier.name !== null ||
|
||||
context.isUsedOutsideDeclaringScope(lvalue)
|
||||
) {
|
||||
context.visitOperand(value.place);
|
||||
}
|
||||
} else if (value.kind === 'PropertyLoad') {
|
||||
if (context.isUsedOutsideDeclaringScope(lvalue)) {
|
||||
context.visitProperty(value.object, value.property, false);
|
||||
}
|
||||
} else if (value.kind === 'StoreLocal') {
|
||||
context.visitOperand(value.value);
|
||||
if (value.lvalue.kind === InstructionKind.Reassign) {
|
||||
context.visitReassignment(value.lvalue.place);
|
||||
}
|
||||
context.declare(value.lvalue.place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
} 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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,
|
||||
});
|
||||
} else if (value.kind === 'Destructure') {
|
||||
context.visitOperand(value.value);
|
||||
for (const place of eachPatternOperand(value.lvalue.pattern)) {
|
||||
if (value.lvalue.kind === InstructionKind.Reassign) {
|
||||
context.visitReassignment(place);
|
||||
}
|
||||
context.declare(place.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
}
|
||||
}
|
||||
|
||||
context.declare(lvalue.identifier, {
|
||||
id,
|
||||
scope: context.currentScope,
|
||||
});
|
||||
}
|
||||
|
||||
function collectDependencies(
|
||||
fn: HIRFunction,
|
||||
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
|
||||
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
|
||||
processedInstrsInOptional: ReadonlySet<InstructionId>,
|
||||
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
|
||||
const context = new Context(usedOutsideDeclaringScope, temporaries);
|
||||
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
context.declare(param.identifier, {
|
||||
id: makeInstructionId(0),
|
||||
scope: empty(),
|
||||
});
|
||||
} else {
|
||||
context.declare(param.place.identifier, {
|
||||
id: makeInstructionId(0),
|
||||
scope: empty(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const scopeTraversal = new ScopeBlockTraversal();
|
||||
|
||||
for (const [blockId, block] of fn.body.blocks) {
|
||||
scopeTraversal.recordScopes(block);
|
||||
const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId);
|
||||
if (scopeBlockInfo?.kind === 'begin') {
|
||||
context.enterScope(scopeBlockInfo.scope);
|
||||
} else if (scopeBlockInfo?.kind === 'end') {
|
||||
context.exitScope(scopeBlockInfo.scope, scopeBlockInfo?.pruned);
|
||||
}
|
||||
|
||||
// Record referenced optional chains in phis
|
||||
for (const phi of block.phis) {
|
||||
for (const operand of phi.operands) {
|
||||
const maybeOptionalChain = temporaries.get(operand[1].id);
|
||||
if (maybeOptionalChain) {
|
||||
context.visitDependency(maybeOptionalChain);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (!processedInstrsInOptional.has(instr.id)) {
|
||||
handleInstruction(instr, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (!processedInstrsInOptional.has(block.terminal.id)) {
|
||||
for (const place of eachTerminalOperand(block.terminal)) {
|
||||
context.visitOperand(place);
|
||||
}
|
||||
}
|
||||
}
|
||||
return context.deps;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {isValidIdentifier} from '@babel/types';
|
||||
import {z} from 'zod';
|
||||
import {Effect, ValueKind} from '..';
|
||||
import {EffectSchema, ValueKindSchema} from './HIR';
|
||||
|
||||
export type ObjectPropertiesConfig = {[key: string]: TypeConfig};
|
||||
export const ObjectPropertiesSchema: z.ZodType<ObjectPropertiesConfig> = z
|
||||
.record(
|
||||
z.string(),
|
||||
z.lazy(() => TypeSchema),
|
||||
)
|
||||
.refine(record => {
|
||||
return Object.keys(record).every(
|
||||
key => key === '*' || key === 'default' || isValidIdentifier(key),
|
||||
);
|
||||
}, 'Expected all "object" property names to be valid identifier, `*` to match any property, of `default` to define a module default export');
|
||||
|
||||
export type ObjectTypeConfig = {
|
||||
kind: 'object';
|
||||
properties: ObjectPropertiesConfig | null;
|
||||
};
|
||||
export const ObjectTypeSchema: z.ZodType<ObjectTypeConfig> = z.object({
|
||||
kind: z.literal('object'),
|
||||
properties: ObjectPropertiesSchema.nullable(),
|
||||
});
|
||||
|
||||
export type FunctionTypeConfig = {
|
||||
kind: 'function';
|
||||
positionalParams: Array<Effect>;
|
||||
restParam: Effect | null;
|
||||
calleeEffect: Effect;
|
||||
returnType: TypeConfig;
|
||||
returnValueKind: ValueKind;
|
||||
noAlias?: boolean | null | undefined;
|
||||
mutableOnlyIfOperandsAreMutable?: boolean | null | undefined;
|
||||
};
|
||||
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
|
||||
kind: z.literal('function'),
|
||||
positionalParams: z.array(EffectSchema),
|
||||
restParam: EffectSchema.nullable(),
|
||||
calleeEffect: EffectSchema,
|
||||
returnType: z.lazy(() => TypeSchema),
|
||||
returnValueKind: ValueKindSchema,
|
||||
noAlias: z.boolean().nullable().optional(),
|
||||
mutableOnlyIfOperandsAreMutable: z.boolean().nullable().optional(),
|
||||
});
|
||||
|
||||
export type HookTypeConfig = {
|
||||
kind: 'hook';
|
||||
positionalParams?: Array<Effect> | null | undefined;
|
||||
restParam?: Effect | null | undefined;
|
||||
returnType: TypeConfig;
|
||||
returnValueKind?: ValueKind | null | undefined;
|
||||
noAlias?: boolean | null | undefined;
|
||||
};
|
||||
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
|
||||
kind: z.literal('hook'),
|
||||
positionalParams: z.array(EffectSchema).nullable().optional(),
|
||||
restParam: EffectSchema.nullable().optional(),
|
||||
returnType: z.lazy(() => TypeSchema),
|
||||
returnValueKind: ValueKindSchema.nullable().optional(),
|
||||
noAlias: z.boolean().nullable().optional(),
|
||||
});
|
||||
|
||||
export type BuiltInTypeConfig =
|
||||
| 'Any'
|
||||
| 'Ref'
|
||||
| 'Array'
|
||||
| 'Primitive'
|
||||
| 'MixedReadonly';
|
||||
export const BuiltInTypeSchema: z.ZodType<BuiltInTypeConfig> = z.union([
|
||||
z.literal('Any'),
|
||||
z.literal('Ref'),
|
||||
z.literal('Array'),
|
||||
z.literal('Primitive'),
|
||||
z.literal('MixedReadonly'),
|
||||
]);
|
||||
|
||||
export type TypeReferenceConfig = {
|
||||
kind: 'type';
|
||||
name: BuiltInTypeConfig;
|
||||
};
|
||||
export const TypeReferenceSchema: z.ZodType<TypeReferenceConfig> = z.object({
|
||||
kind: z.literal('type'),
|
||||
name: BuiltInTypeSchema,
|
||||
});
|
||||
|
||||
export type TypeConfig =
|
||||
| ObjectTypeConfig
|
||||
| FunctionTypeConfig
|
||||
| HookTypeConfig
|
||||
| TypeReferenceConfig;
|
||||
export const TypeSchema: z.ZodType<TypeConfig> = z.union([
|
||||
ObjectTypeSchema,
|
||||
FunctionTypeSchema,
|
||||
HookTypeSchema,
|
||||
TypeReferenceSchema,
|
||||
]);
|
||||
@@ -6,9 +6,7 @@
|
||||
*/
|
||||
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
@@ -16,9 +14,7 @@ import {
|
||||
Pattern,
|
||||
Place,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ReactiveValue,
|
||||
ScopeId,
|
||||
SpreadPattern,
|
||||
Terminal,
|
||||
} from './HIR';
|
||||
@@ -664,13 +660,11 @@ export function mapTerminalSuccessors(
|
||||
case 'branch': {
|
||||
const consequent = fn(terminal.consequent);
|
||||
const alternate = fn(terminal.alternate);
|
||||
const fallthrough = fn(terminal.fallthrough);
|
||||
return {
|
||||
kind: 'branch',
|
||||
test: terminal.test,
|
||||
consequent,
|
||||
alternate,
|
||||
fallthrough,
|
||||
id: makeInstructionId(0),
|
||||
loc: terminal.loc,
|
||||
};
|
||||
@@ -889,6 +883,7 @@ export function terminalHasFallthrough<
|
||||
>(terminal: T): terminal is U {
|
||||
switch (terminal.kind) {
|
||||
case 'maybe-throw':
|
||||
case 'branch':
|
||||
case 'goto':
|
||||
case 'return':
|
||||
case 'throw':
|
||||
@@ -897,7 +892,6 @@ export function terminalHasFallthrough<
|
||||
const _: undefined = terminal.fallthrough;
|
||||
return false;
|
||||
}
|
||||
case 'branch':
|
||||
case 'try':
|
||||
case 'do-while':
|
||||
case 'for-of':
|
||||
@@ -1153,72 +1147,3 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for traversing scope blocks in HIR-form.
|
||||
*/
|
||||
export class ScopeBlockTraversal {
|
||||
// Live stack of active scopes
|
||||
#activeScopes: Array<ScopeId> = [];
|
||||
blockInfos: Map<
|
||||
BlockId,
|
||||
| {
|
||||
kind: 'end';
|
||||
scope: ReactiveScope;
|
||||
pruned: boolean;
|
||||
}
|
||||
| {
|
||||
kind: 'begin';
|
||||
scope: ReactiveScope;
|
||||
pruned: boolean;
|
||||
fallthrough: BlockId;
|
||||
}
|
||||
> = new Map();
|
||||
|
||||
recordScopes(block: BasicBlock): void {
|
||||
const blockInfo = this.blockInfos.get(block.id);
|
||||
if (blockInfo?.kind === 'begin') {
|
||||
this.#activeScopes.push(blockInfo.scope.id);
|
||||
} else if (blockInfo?.kind === 'end') {
|
||||
const top = this.#activeScopes.at(-1);
|
||||
CompilerError.invariant(blockInfo.scope.id === top, {
|
||||
reason:
|
||||
'Expected traversed block fallthrough to match top-most active scope',
|
||||
loc: block.instructions[0]?.loc ?? block.terminal.id,
|
||||
});
|
||||
this.#activeScopes.pop();
|
||||
}
|
||||
|
||||
if (
|
||||
block.terminal.kind === 'scope' ||
|
||||
block.terminal.kind === 'pruned-scope'
|
||||
) {
|
||||
CompilerError.invariant(
|
||||
!this.blockInfos.has(block.terminal.block) &&
|
||||
!this.blockInfos.has(block.terminal.fallthrough),
|
||||
{
|
||||
reason: 'Expected unique scope blocks and fallthroughs',
|
||||
loc: block.terminal.loc,
|
||||
},
|
||||
);
|
||||
this.blockInfos.set(block.terminal.block, {
|
||||
kind: 'begin',
|
||||
scope: block.terminal.scope,
|
||||
pruned: block.terminal.kind === 'pruned-scope',
|
||||
fallthrough: block.terminal.fallthrough,
|
||||
});
|
||||
this.blockInfos.set(block.terminal.fallthrough, {
|
||||
kind: 'end',
|
||||
scope: block.terminal.scope,
|
||||
pruned: block.terminal.kind === 'pruned-scope',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isScopeActive(scopeId: ScopeId): boolean {
|
||||
return this.#activeScopes.indexOf(scopeId) !== -1;
|
||||
}
|
||||
get currentScope(): ScopeId | null {
|
||||
return this.#activeScopes.at(-1) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,25 +13,22 @@ import {
|
||||
IdentifierName,
|
||||
LoweredFunction,
|
||||
Place,
|
||||
isRefOrRefValue,
|
||||
ReactiveScopeDependency,
|
||||
isRefValueType,
|
||||
isUseRefType,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {deadCodeElimination} from '../Optimization';
|
||||
import {inferReactiveScopeVariables} from '../ReactiveScopes';
|
||||
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
|
||||
import {leaveSSA} from '../SSA';
|
||||
import {logHIRFunction} from '../Utils/logger';
|
||||
import {inferMutableContextVariables} from './InferMutableContextVariables';
|
||||
import {inferMutableRanges} from './InferMutableRanges';
|
||||
import inferReferenceEffects from './InferReferenceEffects';
|
||||
|
||||
type Dependency = {
|
||||
identifier: Identifier;
|
||||
path: Array<string>;
|
||||
};
|
||||
|
||||
// Helper class to track indirections such as LoadLocal and PropertyLoad.
|
||||
export class IdentifierState {
|
||||
properties: Map<Identifier, Dependency> = new Map();
|
||||
properties: Map<Identifier, ReactiveScopeDependency> = new Map();
|
||||
|
||||
resolve(identifier: Identifier): Identifier {
|
||||
const resolved = this.properties.get(identifier);
|
||||
@@ -43,7 +40,7 @@ export class IdentifierState {
|
||||
|
||||
declareProperty(lvalue: Place, object: Place, property: string): void {
|
||||
const objectDependency = this.properties.get(object.identifier);
|
||||
let nextDependency: Dependency;
|
||||
let nextDependency: ReactiveScopeDependency;
|
||||
if (objectDependency === undefined) {
|
||||
nextDependency = {identifier: object.identifier, path: [property]};
|
||||
} else {
|
||||
@@ -56,7 +53,9 @@ export class IdentifierState {
|
||||
}
|
||||
|
||||
declareTemporary(lvalue: Place, value: Place): void {
|
||||
const resolved: Dependency = this.properties.get(value.identifier) ?? {
|
||||
const resolved: ReactiveScopeDependency = this.properties.get(
|
||||
value.identifier,
|
||||
) ?? {
|
||||
identifier: value.identifier,
|
||||
path: [],
|
||||
};
|
||||
@@ -109,7 +108,7 @@ function lower(func: HIRFunction): void {
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
deadCodeElimination(func);
|
||||
inferMutableRanges(func);
|
||||
rewriteInstructionKindsBasedOnReassignment(func);
|
||||
leaveSSA(func);
|
||||
inferReactiveScopeVariables(func);
|
||||
inferMutableContextVariables(func);
|
||||
logHIRFunction('AnalyseFunction (inner)', func);
|
||||
@@ -140,7 +139,7 @@ function infer(
|
||||
name = dep.identifier.name;
|
||||
}
|
||||
|
||||
if (isRefOrRefValue(dep.identifier)) {
|
||||
if (isUseRefType(dep.identifier) || isRefValueType(dep.identifier)) {
|
||||
/*
|
||||
* TODO: this is a hack to ensure we treat functions which reference refs
|
||||
* as having a capture and therefore being considered mutable. this ensures
|
||||
|
||||
@@ -42,7 +42,6 @@ type IdentifierSidemap = {
|
||||
react: Set<IdentifierId>;
|
||||
maybeDepsLists: Map<IdentifierId, Array<Place>>;
|
||||
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
|
||||
optionals: Set<IdentifierId>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -53,7 +52,6 @@ type IdentifierSidemap = {
|
||||
export function collectMaybeMemoDependencies(
|
||||
value: InstructionValue,
|
||||
maybeDeps: Map<IdentifierId, ManualMemoDependency>,
|
||||
optional: boolean,
|
||||
): ManualMemoDependency | null {
|
||||
switch (value.kind) {
|
||||
case 'LoadGlobal': {
|
||||
@@ -70,8 +68,7 @@ export function collectMaybeMemoDependencies(
|
||||
if (object != null) {
|
||||
return {
|
||||
root: object.root,
|
||||
// TODO: determine if the access is optional
|
||||
path: [...object.path, {property: value.property, optional}],
|
||||
path: [...object.path, value.property],
|
||||
};
|
||||
}
|
||||
break;
|
||||
@@ -130,7 +127,7 @@ function collectTemporaries(
|
||||
break;
|
||||
}
|
||||
case 'LoadGlobal': {
|
||||
const global = env.getGlobalDeclaration(value.binding, value.loc);
|
||||
const global = env.getGlobalDeclaration(value.binding);
|
||||
const hookKind = global !== null ? getHookKindForType(env, global) : null;
|
||||
const lvalId = instr.lvalue.identifier.id;
|
||||
if (hookKind === 'useMemo' || hookKind === 'useCallback') {
|
||||
@@ -164,11 +161,7 @@ function collectTemporaries(
|
||||
break;
|
||||
}
|
||||
}
|
||||
const maybeDep = collectMaybeMemoDependencies(
|
||||
value,
|
||||
sidemap.maybeDeps,
|
||||
sidemap.optionals.has(lvalue.identifier.id),
|
||||
);
|
||||
const maybeDep = collectMaybeMemoDependencies(value, sidemap.maybeDeps);
|
||||
// We don't expect named lvalues during this pass (unlike ValidatePreservingManualMemo)
|
||||
if (maybeDep != null) {
|
||||
sidemap.maybeDeps.set(lvalue.identifier.id, maybeDep);
|
||||
@@ -342,16 +335,13 @@ function extractManualMemoizationArgs(
|
||||
export function dropManualMemoization(func: HIRFunction): void {
|
||||
const isValidationEnabled =
|
||||
func.env.config.validatePreserveExistingMemoizationGuarantees ||
|
||||
func.env.config.validateNoSetStateInRender ||
|
||||
func.env.config.enablePreserveExistingMemoizationGuarantees;
|
||||
const optionals = findOptionalPlaces(func);
|
||||
const sidemap: IdentifierSidemap = {
|
||||
functions: new Map(),
|
||||
manualMemos: new Map(),
|
||||
react: new Set(),
|
||||
maybeDeps: new Map(),
|
||||
maybeDepsLists: new Map(),
|
||||
optionals,
|
||||
};
|
||||
let nextManualMemoId = 0;
|
||||
|
||||
@@ -484,46 +474,3 @@ export function dropManualMemoization(func: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
|
||||
const optionals = new Set<IdentifierId>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
if (block.terminal.kind === 'optional' && block.terminal.optional) {
|
||||
const optionalTerminal = block.terminal;
|
||||
let testBlock = fn.body.blocks.get(block.terminal.test)!;
|
||||
loop: while (true) {
|
||||
const terminal = testBlock.terminal;
|
||||
switch (terminal.kind) {
|
||||
case 'branch': {
|
||||
if (terminal.fallthrough === optionalTerminal.fallthrough) {
|
||||
// found it
|
||||
const consequent = fn.body.blocks.get(terminal.consequent)!;
|
||||
const last = consequent.instructions.at(-1);
|
||||
if (last !== undefined && last.value.kind === 'StoreLocal') {
|
||||
optionals.add(last.value.value.identifier.id);
|
||||
}
|
||||
break loop;
|
||||
} else {
|
||||
testBlock = fn.body.blocks.get(terminal.fallthrough)!;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'optional':
|
||||
case 'logical':
|
||||
case 'sequence':
|
||||
case 'ternary': {
|
||||
testBlock = fn.body.blocks.get(terminal.fallthrough)!;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected terminal in optional`,
|
||||
loc: terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError, ErrorSeverity, ValueKind} from '..';
|
||||
import {
|
||||
AbstractValue,
|
||||
BasicBlock,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionEffect,
|
||||
Instruction,
|
||||
InstructionValue,
|
||||
Place,
|
||||
ValueReason,
|
||||
getHookKind,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR';
|
||||
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
interface State {
|
||||
kind(place: Place): AbstractValue;
|
||||
values(place: Place): Array<InstructionValue>;
|
||||
isDefined(place: Place): boolean;
|
||||
}
|
||||
|
||||
function inferOperandEffect(state: State, place: Place): null | FunctionEffect {
|
||||
const value = state.kind(place);
|
||||
CompilerError.invariant(value != null, {
|
||||
reason: 'Expected operand to have a kind',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
switch (place.effect) {
|
||||
case Effect.Store:
|
||||
case Effect.Mutate: {
|
||||
if (isRefOrRefValue(place.identifier)) {
|
||||
break;
|
||||
} else if (value.kind === ValueKind.Context) {
|
||||
return {
|
||||
kind: 'ContextMutation',
|
||||
loc: place.loc,
|
||||
effect: place.effect,
|
||||
places: value.context.size === 0 ? new Set([place]) : value.context,
|
||||
};
|
||||
} else if (
|
||||
value.kind !== ValueKind.Mutable &&
|
||||
// We ignore mutations of primitives since this is not a React-specific problem
|
||||
value.kind !== ValueKind.Primitive
|
||||
) {
|
||||
let reason = getWriteErrorReason(value);
|
||||
return {
|
||||
kind:
|
||||
value.reason.size === 1 && value.reason.has(ValueReason.Global)
|
||||
? 'GlobalMutation'
|
||||
: 'ReactMutation',
|
||||
error: {
|
||||
reason,
|
||||
description:
|
||||
place.identifier.name !== null &&
|
||||
place.identifier.name.kind === 'named'
|
||||
? `Found mutation of \`${place.identifier.name.value}\``
|
||||
: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inheritFunctionEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects = inferFunctionInstrEffects(state, place);
|
||||
|
||||
return effects
|
||||
.flatMap(effect => {
|
||||
if (effect.kind === 'GlobalMutation' || effect.kind === 'ReactMutation') {
|
||||
return [effect];
|
||||
} else {
|
||||
const effects: Array<FunctionEffect | null> = [];
|
||||
CompilerError.invariant(effect.kind === 'ContextMutation', {
|
||||
reason: 'Expected ContextMutation',
|
||||
loc: null,
|
||||
});
|
||||
/**
|
||||
* Contextual effects need to be replayed against the current inference
|
||||
* state, which may know more about the value to which the effect applied.
|
||||
* The main cases are:
|
||||
* 1. The mutated context value is _still_ a context value in the current scope,
|
||||
* so we have to continue propagating the original context mutation.
|
||||
* 2. The mutated context value is a mutable value in the current scope,
|
||||
* so the context mutation was fine and we can skip propagating the effect.
|
||||
* 3. The mutated context value is an immutable value in the current scope,
|
||||
* resulting in a non-ContextMutation FunctionEffect. We propagate that new,
|
||||
* more detailed effect to the current function context.
|
||||
*/
|
||||
for (const place of effect.places) {
|
||||
if (state.isDefined(place)) {
|
||||
const replayedEffect = inferOperandEffect(state, {
|
||||
...place,
|
||||
loc: effect.loc,
|
||||
effect: effect.effect,
|
||||
});
|
||||
if (replayedEffect != null) {
|
||||
if (replayedEffect.kind === 'ContextMutation') {
|
||||
// Case 1, still a context value so propagate the original effect
|
||||
effects.push(effect);
|
||||
} else {
|
||||
// Case 3, immutable value so propagate the more precise effect
|
||||
effects.push(replayedEffect);
|
||||
}
|
||||
} // else case 2, local mutable value so this effect was fine
|
||||
}
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
})
|
||||
.filter((effect): effect is FunctionEffect => effect != null);
|
||||
}
|
||||
|
||||
function inferFunctionInstrEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
): Array<FunctionEffect> {
|
||||
const effects: Array<FunctionEffect> = [];
|
||||
const instrs = state.values(place);
|
||||
CompilerError.invariant(instrs != null, {
|
||||
reason: 'Expected operand to have instructions',
|
||||
loc: null,
|
||||
});
|
||||
|
||||
for (const instr of instrs) {
|
||||
if (
|
||||
(instr.kind === 'FunctionExpression' || instr.kind === 'ObjectMethod') &&
|
||||
instr.loweredFunc.func.effects != null
|
||||
) {
|
||||
effects.push(...instr.loweredFunc.func.effects);
|
||||
}
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
function operandEffects(
|
||||
state: State,
|
||||
place: Place,
|
||||
filterRenderSafe: boolean,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
const effect = inferOperandEffect(state, place);
|
||||
effect && functionEffects.push(effect);
|
||||
functionEffects.push(...inheritFunctionEffects(state, place));
|
||||
if (filterRenderSafe) {
|
||||
return functionEffects.filter(effect => !isEffectSafeOutsideRender(effect));
|
||||
} else {
|
||||
return functionEffects;
|
||||
}
|
||||
}
|
||||
|
||||
export function inferInstructionFunctionEffects(
|
||||
env: Environment,
|
||||
state: State,
|
||||
instr: Instruction,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression': {
|
||||
if (instr.value.tag.kind === 'Identifier') {
|
||||
functionEffects.push(...operandEffects(state, instr.value.tag, false));
|
||||
}
|
||||
instr.value.children?.forEach(child =>
|
||||
functionEffects.push(...operandEffects(state, child, false)),
|
||||
);
|
||||
for (const attr of instr.value.props) {
|
||||
if (attr.kind === 'JsxSpreadAttribute') {
|
||||
functionEffects.push(...operandEffects(state, attr.argument, false));
|
||||
} else {
|
||||
functionEffects.push(...operandEffects(state, attr.place, true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
/**
|
||||
* If this function references other functions, propagate the referenced function's
|
||||
* effects to this function.
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* g();
|
||||
* ```
|
||||
*
|
||||
* In this example, because `g` references `f`, we propagate the GlobalMutation from
|
||||
* `f` to `g`. Thus, referencing `g` in `g()` will evaluate the GlobalMutation in the outer
|
||||
* function effect context and report an error. But if instead we do:
|
||||
*
|
||||
* ```
|
||||
* let f = () => global = true;
|
||||
* let g = () => f();
|
||||
* useEffect(() => g(), [g])
|
||||
* ```
|
||||
*
|
||||
* Now `g`'s effects will be discarded since they're in a useEffect.
|
||||
*/
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
instr.value.loweredFunc.func.effects ??= [];
|
||||
instr.value.loweredFunc.func.effects.push(
|
||||
...inferFunctionInstrEffects(state, operand),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall':
|
||||
case 'CallExpression': {
|
||||
let callee;
|
||||
if (instr.value.kind === 'MethodCall') {
|
||||
callee = instr.value.property;
|
||||
functionEffects.push(
|
||||
...operandEffects(state, instr.value.receiver, false),
|
||||
);
|
||||
} else {
|
||||
callee = instr.value.callee;
|
||||
}
|
||||
functionEffects.push(...operandEffects(state, callee, false));
|
||||
let isHook = getHookKind(env, callee.identifier) != null;
|
||||
for (const arg of instr.value.args) {
|
||||
const place = arg.kind === 'Identifier' ? arg : arg.place;
|
||||
/*
|
||||
* Join the effects of the argument with the effects of the enclosing function,
|
||||
* unless the we're detecting a global mutation inside a useEffect hook
|
||||
*/
|
||||
functionEffects.push(...operandEffects(state, place, isHook));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'StartMemoize':
|
||||
case 'FinishMemoize':
|
||||
case 'LoadLocal':
|
||||
case 'StoreLocal': {
|
||||
break;
|
||||
}
|
||||
case 'StoreGlobal': {
|
||||
functionEffects.push({
|
||||
kind: 'GlobalMutation',
|
||||
error: {
|
||||
reason:
|
||||
'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
functionEffects.push(...operandEffects(state, operand, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function inferTerminalFunctionEffects(
|
||||
state: State,
|
||||
block: BasicBlock,
|
||||
): Array<FunctionEffect> {
|
||||
const functionEffects: Array<FunctionEffect> = [];
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
functionEffects.push(...operandEffects(state, operand, true));
|
||||
}
|
||||
return functionEffects;
|
||||
}
|
||||
|
||||
export function raiseFunctionEffectErrors(
|
||||
functionEffects: Array<FunctionEffect>,
|
||||
): void {
|
||||
functionEffects.forEach(eff => {
|
||||
switch (eff.kind) {
|
||||
case 'ReactMutation':
|
||||
case 'GlobalMutation': {
|
||||
CompilerError.throw(eff.error);
|
||||
}
|
||||
case 'ContextMutation': {
|
||||
CompilerError.throw({
|
||||
severity: ErrorSeverity.Invariant,
|
||||
reason: `Unexpected ContextMutation in top-level function effects`,
|
||||
loc: eff.loc,
|
||||
});
|
||||
}
|
||||
default:
|
||||
assertExhaustive(
|
||||
eff,
|
||||
`Unexpected function effect kind \`${(eff as any).kind}\``,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
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)) {
|
||||
return 'Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX';
|
||||
} else if (abstractValue.reason.has(ValueReason.Context)) {
|
||||
return `Mutating a value returned from 'useContext()', which should not be mutated`;
|
||||
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
|
||||
return 'Mutating a value returned from a function whose return value should not be mutated';
|
||||
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
|
||||
return 'Mutating component props or hook arguments is not allowed. Consider using a local variable instead';
|
||||
} else if (abstractValue.reason.has(ValueReason.State)) {
|
||||
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 {
|
||||
return 'This mutates a variable that React considers immutable';
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Identifier,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
@@ -67,9 +66,7 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
*/
|
||||
|
||||
function infer(place: Place, instrId: InstructionId): void {
|
||||
if (!isRefOrRefValue(place.identifier)) {
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
|
||||
}
|
||||
|
||||
function inferPlace(
|
||||
@@ -174,10 +171,7 @@ export function inferMutableLifetimes(
|
||||
const declaration = contextVariableDeclarationInstructions.get(
|
||||
instr.value.lvalue.place.identifier,
|
||||
);
|
||||
if (
|
||||
declaration != null &&
|
||||
!isRefOrRefValue(instr.value.lvalue.place.identifier)
|
||||
) {
|
||||
if (declaration != null) {
|
||||
const range = instr.value.lvalue.place.identifier.mutableRange;
|
||||
if (range.start === 0) {
|
||||
range.start = declaration;
|
||||
|
||||
@@ -50,38 +50,8 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
// Re-infer mutable ranges for all values
|
||||
inferMutableLifetimes(ir, true);
|
||||
|
||||
/**
|
||||
* The second inferMutableLifetimes() call updates mutable ranges
|
||||
* of values to account for Store effects. Now we need to update
|
||||
* all aliases of such values to extend their ranges as well. Note
|
||||
* that the store only mutates the the directly aliased value and
|
||||
* not any of its inner captured references. For example:
|
||||
*
|
||||
* ```
|
||||
* let y;
|
||||
* if (cond) {
|
||||
* y = [];
|
||||
* } else {
|
||||
* y = [{}];
|
||||
* }
|
||||
* y.push(z);
|
||||
* ```
|
||||
*
|
||||
* The Store effect from the `y.push` modifies the values that `y`
|
||||
* directly aliases - the two arrays from the if/else branches -
|
||||
* but does not modify values that `y` "contains" such as the
|
||||
* object literal or `z`.
|
||||
*/
|
||||
prevAliases = aliases.canonicalize();
|
||||
while (true) {
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
inferAliasForPhis(ir, aliases);
|
||||
const nextAliases = aliases.canonicalize();
|
||||
if (areEqualMaps(prevAliases, nextAliases)) {
|
||||
break;
|
||||
}
|
||||
prevAliases = nextAliases;
|
||||
}
|
||||
// Re-infer mutable ranges for aliases
|
||||
inferMutableRangesForAlias(ir, aliases);
|
||||
}
|
||||
|
||||
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
|
||||
|
||||
@@ -5,12 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
isRefOrRefValue,
|
||||
} from '../HIR/HIR';
|
||||
import {HIRFunction, Identifier, InstructionId} from '../HIR/HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
|
||||
export function inferMutableRangesForAlias(
|
||||
@@ -24,8 +19,7 @@ export function inferMutableRangesForAlias(
|
||||
* mutated.
|
||||
*/
|
||||
const mutatingIdentifiers = [...aliasSet].filter(
|
||||
id =>
|
||||
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
|
||||
id => id.mutableRange.end - id.mutableRange.start > 1,
|
||||
);
|
||||
|
||||
if (mutatingIdentifiers.length > 0) {
|
||||
@@ -42,10 +36,7 @@ export function inferMutableRangesForAlias(
|
||||
* last mutation.
|
||||
*/
|
||||
for (const alias of aliasSet) {
|
||||
if (
|
||||
alias.mutableRange.end < lastMutatingInstructionId &&
|
||||
!isRefOrRefValue(alias)
|
||||
) {
|
||||
if (alias.mutableRange.end < lastMutatingInstructionId) {
|
||||
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
Effect,
|
||||
Environment,
|
||||
FunctionExpression,
|
||||
GeneratedSource,
|
||||
@@ -18,14 +19,11 @@ import {
|
||||
LabelTerminal,
|
||||
Place,
|
||||
makeInstructionId,
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {markInstructionIds, markPredecessors} from '../HIR/HIRBuilder';
|
||||
import {eachInstructionValueOperand} from '../HIR/visitors';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
|
||||
@@ -227,7 +225,23 @@ function rewriteBlock(
|
||||
block.instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: terminal.loc,
|
||||
lvalue: createTemporaryPlace(env, terminal.loc),
|
||||
lvalue: {
|
||||
effect: Effect.Unknown,
|
||||
identifier: {
|
||||
id: env.nextIdentifierId,
|
||||
mutableRange: {
|
||||
start: makeInstructionId(0),
|
||||
end: makeInstructionId(0),
|
||||
},
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc: terminal.loc,
|
||||
},
|
||||
kind: 'Identifier',
|
||||
reactive: false,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
value: {
|
||||
kind: 'StoreLocal',
|
||||
lvalue: {kind: InstructionKind.Reassign, place: {...returnValue}},
|
||||
@@ -253,7 +267,23 @@ function declareTemporary(
|
||||
block.instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, result.loc),
|
||||
lvalue: {
|
||||
effect: Effect.Unknown,
|
||||
identifier: {
|
||||
id: env.nextIdentifierId,
|
||||
mutableRange: {
|
||||
start: makeInstructionId(0),
|
||||
end: makeInstructionId(0),
|
||||
},
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
loc: result.loc,
|
||||
},
|
||||
kind: 'Identifier',
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
value: {
|
||||
kind: 'DeclareLocal',
|
||||
lvalue: {
|
||||
|
||||
@@ -1,426 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
BuiltinTag,
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Instruction,
|
||||
JsxAttribute,
|
||||
makeInstructionId,
|
||||
ObjectProperty,
|
||||
Place,
|
||||
SpreadPattern,
|
||||
} from '../HIR';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
fixScopeAndIdentifierRanges,
|
||||
markInstructionIds,
|
||||
markPredecessors,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR/HIRBuilder';
|
||||
import {CompilerError, EnvironmentConfig} from '..';
|
||||
|
||||
function createSymbolProperty(
|
||||
fn: HIRFunction,
|
||||
instr: Instruction,
|
||||
nextInstructions: Array<Instruction>,
|
||||
propertyName: string,
|
||||
symbolName: string,
|
||||
): ObjectProperty {
|
||||
const symbolPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const symbolInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...symbolPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {kind: 'Global', name: 'Symbol'},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolInstruction);
|
||||
|
||||
const symbolForPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const symbolForInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...symbolForPlace, effect: Effect.Read},
|
||||
value: {
|
||||
kind: 'PropertyLoad',
|
||||
object: {...symbolInstruction.lvalue},
|
||||
property: 'for',
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolForInstruction);
|
||||
|
||||
const symbolValuePlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const symbolValueInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...symbolValuePlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: symbolName,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolValueInstruction);
|
||||
|
||||
const $$typeofPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const $$typeofInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...$$typeofPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'MethodCall',
|
||||
receiver: symbolInstruction.lvalue,
|
||||
property: symbolForInstruction.lvalue,
|
||||
args: [symbolValueInstruction.lvalue],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
const $$typeofProperty: ObjectProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: propertyName, kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...$$typeofPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push($$typeofInstruction);
|
||||
return $$typeofProperty;
|
||||
}
|
||||
|
||||
function createTagProperty(
|
||||
fn: HIRFunction,
|
||||
instr: Instruction,
|
||||
nextInstructions: Array<Instruction>,
|
||||
componentTag: BuiltinTag | Place,
|
||||
): ObjectProperty {
|
||||
let tagProperty: ObjectProperty;
|
||||
switch (componentTag.kind) {
|
||||
case 'BuiltinTag': {
|
||||
const tagPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const tagInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...tagPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: componentTag.name,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
tagProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'type', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...tagPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(tagInstruction);
|
||||
break;
|
||||
}
|
||||
case 'Identifier': {
|
||||
tagProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'type', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...componentTag, effect: Effect.Capture},
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tagProperty;
|
||||
}
|
||||
|
||||
function createPropsProperties(
|
||||
fn: HIRFunction,
|
||||
instr: Instruction,
|
||||
nextInstructions: Array<Instruction>,
|
||||
propAttributes: Array<JsxAttribute>,
|
||||
children: Array<Place> | null,
|
||||
): {
|
||||
refProperty: ObjectProperty;
|
||||
keyProperty: ObjectProperty;
|
||||
propsProperty: ObjectProperty;
|
||||
} {
|
||||
let refProperty: ObjectProperty | undefined;
|
||||
let keyProperty: ObjectProperty | undefined;
|
||||
const props: Array<ObjectProperty | SpreadPattern> = [];
|
||||
const jsxAttributesWithoutKeyAndRef = propAttributes.filter(
|
||||
p => p.kind === 'JsxAttribute' && p.name !== 'key' && p.name !== 'ref',
|
||||
);
|
||||
const jsxSpreadAttributes = propAttributes.filter(
|
||||
p => p.kind === 'JsxSpreadAttribute',
|
||||
);
|
||||
const spreadPropsOnly =
|
||||
jsxAttributesWithoutKeyAndRef.length === 0 &&
|
||||
jsxSpreadAttributes.length === 1;
|
||||
|
||||
propAttributes.forEach(prop => {
|
||||
switch (prop.kind) {
|
||||
case 'JsxAttribute': {
|
||||
if (prop.name === 'ref') {
|
||||
refProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'ref', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
} else if (prop.name === 'key') {
|
||||
keyProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'key', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
} else {
|
||||
const attributeProperty: ObjectProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: prop.name, kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...prop.place},
|
||||
};
|
||||
props.push(attributeProperty);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'JsxSpreadAttribute': {
|
||||
props.push({
|
||||
kind: 'Spread',
|
||||
place: {...prop.argument},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const propsPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
if (children) {
|
||||
let childrenPropProperty: ObjectProperty;
|
||||
if (children.length === 1) {
|
||||
childrenPropProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'children', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...children[0], effect: Effect.Capture},
|
||||
};
|
||||
} else {
|
||||
const childrenPropPropertyPlace = createTemporaryPlace(
|
||||
fn.env,
|
||||
instr.value.loc,
|
||||
);
|
||||
|
||||
const childrenPropInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...childrenPropPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'ArrayExpression',
|
||||
elements: [...children],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(childrenPropInstruction);
|
||||
childrenPropProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'children', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...childrenPropPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
}
|
||||
props.push(childrenPropProperty);
|
||||
}
|
||||
|
||||
if (refProperty == null) {
|
||||
const refPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const refInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...refPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
refProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'ref', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...refPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(refInstruction);
|
||||
}
|
||||
|
||||
if (keyProperty == null) {
|
||||
const keyPropertyPlace = createTemporaryPlace(fn.env, instr.value.loc);
|
||||
const keyInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...keyPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'Primitive',
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
keyProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'key', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...keyPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(keyInstruction);
|
||||
}
|
||||
|
||||
let propsProperty: ObjectProperty;
|
||||
if (spreadPropsOnly) {
|
||||
const spreadProp = jsxSpreadAttributes[0];
|
||||
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
|
||||
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
|
||||
loc: instr.loc,
|
||||
});
|
||||
propsProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'props', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...spreadProp.argument, effect: Effect.Mutate},
|
||||
};
|
||||
} else {
|
||||
const propsInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...propsPropertyPlace, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'ObjectExpression',
|
||||
properties: props,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
propsProperty = {
|
||||
kind: 'ObjectProperty',
|
||||
key: {name: 'props', kind: 'string'},
|
||||
type: 'property',
|
||||
place: {...propsPropertyPlace, effect: Effect.Capture},
|
||||
};
|
||||
nextInstructions.push(propsInstruction);
|
||||
}
|
||||
|
||||
return {refProperty, keyProperty, propsProperty};
|
||||
}
|
||||
|
||||
// TODO: Make PROD only with conditional statements
|
||||
export function inlineJsxTransform(
|
||||
fn: HIRFunction,
|
||||
inlineJsxTransformConfig: NonNullable<
|
||||
EnvironmentConfig['inlineJsxTransform']
|
||||
>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
let nextInstructions: Array<Instruction> | null = null;
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i]!;
|
||||
switch (instr.value.kind) {
|
||||
case 'JsxExpression': {
|
||||
nextInstructions ??= block.instructions.slice(0, i);
|
||||
|
||||
const {refProperty, keyProperty, propsProperty} =
|
||||
createPropsProperties(
|
||||
fn,
|
||||
instr,
|
||||
nextInstructions,
|
||||
instr.value.props,
|
||||
instr.value.children,
|
||||
);
|
||||
const reactElementInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...instr.lvalue, effect: Effect.Store},
|
||||
value: {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [
|
||||
createSymbolProperty(
|
||||
fn,
|
||||
instr,
|
||||
nextInstructions,
|
||||
'$$typeof',
|
||||
inlineJsxTransformConfig.elementSymbol,
|
||||
),
|
||||
createTagProperty(fn, instr, nextInstructions, instr.value.tag),
|
||||
refProperty,
|
||||
keyProperty,
|
||||
propsProperty,
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(reactElementInstruction);
|
||||
|
||||
break;
|
||||
}
|
||||
case 'JsxFragment': {
|
||||
nextInstructions ??= block.instructions.slice(0, i);
|
||||
const {refProperty, keyProperty, propsProperty} =
|
||||
createPropsProperties(
|
||||
fn,
|
||||
instr,
|
||||
nextInstructions,
|
||||
[],
|
||||
instr.value.children,
|
||||
);
|
||||
const reactElementInstruction: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...instr.lvalue, effect: Effect.Store},
|
||||
value: {
|
||||
kind: 'ObjectExpression',
|
||||
properties: [
|
||||
createSymbolProperty(
|
||||
fn,
|
||||
instr,
|
||||
nextInstructions,
|
||||
'$$typeof',
|
||||
inlineJsxTransformConfig.elementSymbol,
|
||||
),
|
||||
createSymbolProperty(
|
||||
fn,
|
||||
instr,
|
||||
nextInstructions,
|
||||
'type',
|
||||
'react.fragment',
|
||||
),
|
||||
refProperty,
|
||||
keyProperty,
|
||||
propsProperty,
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(reactElementInstruction);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (nextInstructions !== null) {
|
||||
nextInstructions.push(instr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nextInstructions !== null) {
|
||||
block.instructions = nextInstructions;
|
||||
}
|
||||
}
|
||||
|
||||
// Fixup the HIR to restore RPO, ensure correct predecessors, and renumber instructions.
|
||||
reversePostorderBlocks(fn.body);
|
||||
markPredecessors(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
// The renumbering instructions invalidates scope and identifier ranges
|
||||
fixScopeAndIdentifierRanges(fn.body);
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {
|
||||
ArrayExpression,
|
||||
BasicBlock,
|
||||
CallExpression,
|
||||
Destructure,
|
||||
Environment,
|
||||
ExternalFunction,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
Instruction,
|
||||
LoadGlobal,
|
||||
LoadLocal,
|
||||
Place,
|
||||
PropertyLoad,
|
||||
isUseContextHookType,
|
||||
makeBlockId,
|
||||
makeInstructionId,
|
||||
makeType,
|
||||
markInstructionIds,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {enterSSA} from '../SSA';
|
||||
import {inferTypes} from '../TypeInference';
|
||||
|
||||
export function lowerContextAccess(
|
||||
fn: HIRFunction,
|
||||
loweredContextCallee: ExternalFunction,
|
||||
): void {
|
||||
const contextAccess: Map<IdentifierId, CallExpression> = new Map();
|
||||
const contextKeys: Map<IdentifierId, Array<string>> = new Map();
|
||||
|
||||
// collect context access and keys
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isUseContextHookType(value.callee.identifier)
|
||||
) {
|
||||
contextAccess.set(lvalue.identifier.id, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.kind !== 'Destructure') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const destructureId = value.value.identifier.id;
|
||||
if (!contextAccess.has(destructureId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keys = getContextKeys(value);
|
||||
if (keys === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextKeys.has(destructureId)) {
|
||||
/*
|
||||
* TODO(gsn): Add support for accessing context over multiple
|
||||
* statements.
|
||||
*/
|
||||
return;
|
||||
} else {
|
||||
contextKeys.set(destructureId, keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contextAccess.size > 0 && contextKeys.size > 0) {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
let nextInstructions: Array<Instruction> | null = null;
|
||||
|
||||
for (let i = 0; i < block.instructions.length; i++) {
|
||||
const instr = block.instructions[i];
|
||||
const {lvalue, value} = instr;
|
||||
if (
|
||||
value.kind === 'CallExpression' &&
|
||||
isUseContextHookType(value.callee.identifier) &&
|
||||
contextKeys.has(lvalue.identifier.id)
|
||||
) {
|
||||
const loweredContextCalleeInstr = emitLoadLoweredContextCallee(
|
||||
fn.env,
|
||||
loweredContextCallee,
|
||||
);
|
||||
|
||||
if (nextInstructions === null) {
|
||||
nextInstructions = block.instructions.slice(0, i);
|
||||
}
|
||||
nextInstructions.push(loweredContextCalleeInstr);
|
||||
|
||||
const keys = contextKeys.get(lvalue.identifier.id)!;
|
||||
const selectorFnInstr = emitSelectorFn(fn.env, keys);
|
||||
nextInstructions.push(selectorFnInstr);
|
||||
|
||||
const lowerContextCallId = loweredContextCalleeInstr.lvalue;
|
||||
value.callee = lowerContextCallId;
|
||||
|
||||
const selectorFn = selectorFnInstr.lvalue;
|
||||
value.args.push(selectorFn);
|
||||
}
|
||||
|
||||
if (nextInstructions) {
|
||||
nextInstructions.push(instr);
|
||||
}
|
||||
}
|
||||
if (nextInstructions) {
|
||||
block.instructions = nextInstructions;
|
||||
}
|
||||
}
|
||||
markInstructionIds(fn.body);
|
||||
inferTypes(fn);
|
||||
fn.env.hasLoweredContextAccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
function emitLoadLoweredContextCallee(
|
||||
env: Environment,
|
||||
loweredContextCallee: ExternalFunction,
|
||||
): Instruction {
|
||||
const loadGlobal: LoadGlobal = {
|
||||
kind: 'LoadGlobal',
|
||||
binding: {
|
||||
kind: 'ImportNamespace',
|
||||
module: loweredContextCallee.source,
|
||||
name: loweredContextCallee.importSpecifierName,
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
return {
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
value: loadGlobal,
|
||||
};
|
||||
}
|
||||
|
||||
function getContextKeys(value: Destructure): Array<string> | null {
|
||||
const keys = [];
|
||||
const pattern = value.lvalue.pattern;
|
||||
|
||||
switch (pattern.kind) {
|
||||
case 'ArrayPattern': {
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'ObjectPattern': {
|
||||
for (const place of pattern.properties) {
|
||||
if (
|
||||
place.kind !== 'ObjectProperty' ||
|
||||
place.type !== 'property' ||
|
||||
place.key.kind !== 'identifier' ||
|
||||
place.place.identifier.name === null ||
|
||||
place.place.identifier.name.kind !== 'named'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
keys.push(place.key.name);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitPropertyLoad(
|
||||
env: Environment,
|
||||
obj: Place,
|
||||
property: string,
|
||||
): {instructions: Array<Instruction>; element: Place} {
|
||||
const loadObj: LoadLocal = {
|
||||
kind: 'LoadLocal',
|
||||
place: obj,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const object: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
const loadLocalInstr: Instruction = {
|
||||
lvalue: object,
|
||||
value: loadObj,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
const loadProp: PropertyLoad = {
|
||||
kind: 'PropertyLoad',
|
||||
object,
|
||||
property,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const element: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
const loadPropInstr: Instruction = {
|
||||
lvalue: element,
|
||||
value: loadProp,
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
instructions: [loadLocalInstr, loadPropInstr],
|
||||
element: element,
|
||||
};
|
||||
}
|
||||
|
||||
function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
const obj: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
promoteTemporary(obj.identifier);
|
||||
const instr: Array<Instruction> = [];
|
||||
const elements = [];
|
||||
for (const key of keys) {
|
||||
const {instructions, element: prop} = emitPropertyLoad(env, obj, key);
|
||||
instr.push(...instructions);
|
||||
elements.push(prop);
|
||||
}
|
||||
|
||||
const arrayInstr = emitArrayInstr(elements, env);
|
||||
instr.push(arrayInstr);
|
||||
|
||||
const block: BasicBlock = {
|
||||
kind: 'block',
|
||||
id: makeBlockId(0),
|
||||
instructions: instr,
|
||||
terminal: {
|
||||
id: makeInstructionId(0),
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
};
|
||||
|
||||
const fn: HIRFunction = {
|
||||
loc: GeneratedSource,
|
||||
id: null,
|
||||
fnType: 'Other',
|
||||
env,
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
entry: block.id,
|
||||
blocks: new Map([[block.id, block]]),
|
||||
},
|
||||
generator: false,
|
||||
async: false,
|
||||
directives: [],
|
||||
};
|
||||
|
||||
reversePostorderBlocks(fn.body);
|
||||
markInstructionIds(fn.body);
|
||||
enterSSA(fn);
|
||||
inferTypes(fn);
|
||||
|
||||
const fnInstr: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
value: {
|
||||
kind: 'FunctionExpression',
|
||||
name: null,
|
||||
loweredFunc: {
|
||||
func: fn,
|
||||
dependencies: [],
|
||||
},
|
||||
type: 'ArrowFunctionExpression',
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return fnInstr;
|
||||
}
|
||||
|
||||
function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
|
||||
const array: ArrayExpression = {
|
||||
kind: 'ArrayExpression',
|
||||
elements,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const arrayLvalue: Place = createTemporaryPlace(env, GeneratedSource);
|
||||
const arrayInstr: Instruction = {
|
||||
id: makeInstructionId(0),
|
||||
value: array,
|
||||
lvalue: arrayLvalue,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return arrayInstr;
|
||||
}
|
||||
@@ -5,30 +5,27 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, IdentifierId} from '../HIR';
|
||||
import {HIRFunction} from '../HIR';
|
||||
|
||||
export function outlineFunctions(
|
||||
fn: HIRFunction,
|
||||
fbtOperands: Set<IdentifierId>,
|
||||
): void {
|
||||
export function outlineFunctions(fn: HIRFunction): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value, lvalue} = instr;
|
||||
const {value} = instr;
|
||||
|
||||
if (
|
||||
value.kind === 'FunctionExpression' ||
|
||||
value.kind === 'ObjectMethod'
|
||||
) {
|
||||
// Recurse in case there are inner functions which can be outlined
|
||||
outlineFunctions(value.loweredFunc.func, fbtOperands);
|
||||
outlineFunctions(value.loweredFunc.func);
|
||||
}
|
||||
|
||||
if (
|
||||
value.kind === 'FunctionExpression' &&
|
||||
value.loweredFunc.dependencies.length === 0 &&
|
||||
value.loweredFunc.func.context.length === 0 &&
|
||||
// TODO: handle outlining named functions
|
||||
value.loweredFunc.func.id === null &&
|
||||
!fbtOperands.has(lvalue.identifier.id)
|
||||
value.loweredFunc.func.id === null
|
||||
) {
|
||||
const loweredFunc = value.loweredFunc.func;
|
||||
|
||||
|
||||
@@ -8,4 +8,3 @@
|
||||
export {constantPropagation} from './ConstantPropagation';
|
||||
export {deadCodeElimination} from './DeadCodeElimination';
|
||||
export {pruneMaybeThrows} from './PruneMaybeThrows';
|
||||
export {inlineJsxTransform} from './InlineJsxTransform';
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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 {
|
||||
InstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ScopeId,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
|
||||
/*
|
||||
* Note: this is the 2nd of 4 passes that determine how to break a function into discrete
|
||||
* reactive scopes (independently memoizeable units of code):
|
||||
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
|
||||
* them a unique reactive scope.
|
||||
* 2. AlignReactiveScopesToBlockScopes (this pass, on ReactiveFunction) aligns reactive scopes
|
||||
* to block scopes.
|
||||
* 3. MergeOverlappingReactiveScopes (on ReactiveFunction) ensures that reactive scopes do not
|
||||
* overlap, merging any such scopes.
|
||||
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
|
||||
* a ReactiveScopeBlock.
|
||||
*
|
||||
* Prior inference passes assign a reactive scope to each operand, but the ranges of these
|
||||
* scopes are based on specific instructions at arbitrary points in the control-flow graph.
|
||||
* However, to codegen blocks around the instructions in each scope, the scopes must be
|
||||
* aligned to block-scope boundaries - we can't memoize half of a loop!
|
||||
*
|
||||
* This pass updates reactive scope boundaries to align to control flow boundaries, for
|
||||
* example:
|
||||
*
|
||||
* ```javascript
|
||||
* function foo(cond, a) {
|
||||
* ⌵ original scope
|
||||
* ⌵ expanded scope
|
||||
* const x = []; ⌝ ⌝
|
||||
* if (cond) { ⎮ ⎮
|
||||
* ... ⎮ ⎮
|
||||
* x.push(a); ⌟ ⎮
|
||||
* ... ⎮
|
||||
* } ⌟
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Here the original scope for `x` ended partway through the if consequent, but we can't
|
||||
* memoize part of that block. This pass would align the scope to the end of the consequent.
|
||||
*
|
||||
* The more general rule is that a reactive scope may only end at the same block scope as it
|
||||
* began: this pass therefore finds, for each scope, the block where that scope started and
|
||||
* finds the first instruction after the scope's mutable range in that same block scope (which
|
||||
* will be the updated end for that scope).
|
||||
*/
|
||||
|
||||
export function alignReactiveScopesToBlockScopes(fn: ReactiveFunction): void {
|
||||
const context = new Context();
|
||||
visitReactiveFunction(fn, new Visitor(), context);
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<Context> {
|
||||
override visitID(id: InstructionId, state: Context): void {
|
||||
state.visitId(id);
|
||||
}
|
||||
override visitPlace(id: InstructionId, place: Place, state: Context): void {
|
||||
const scope = getPlaceScope(id, place);
|
||||
if (scope !== null) {
|
||||
state.visitScope(scope);
|
||||
}
|
||||
}
|
||||
override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
|
||||
const scope = getPlaceScope(id, lvalue);
|
||||
if (scope !== null) {
|
||||
state.visitScope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
override visitInstruction(instr: ReactiveInstruction, state: Context): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'OptionalExpression':
|
||||
case 'SequenceExpression':
|
||||
case 'ConditionalExpression':
|
||||
case 'LogicalExpression': {
|
||||
const prevScopeCount = state.currentScopes().length;
|
||||
this.traverseInstruction(instr, state);
|
||||
|
||||
/**
|
||||
* These compound value types can have nested sequences of instructions
|
||||
* with scopes that start "partway" through a block-level instruction.
|
||||
* This would cause the start of the scope to not align with any block-level
|
||||
* instruction and get skipped by the later BuildReactiveBlocks pass.
|
||||
*
|
||||
* Here we detect scopes created within compound instructions and align the
|
||||
* start of these scopes to the outer instruction id to ensure the scopes
|
||||
* aren't skipped.
|
||||
*/
|
||||
const scopes = state.currentScopes();
|
||||
for (let i = prevScopeCount; i < scopes.length; i++) {
|
||||
const scope = scopes[i];
|
||||
scope.scope.range.start = makeInstructionId(
|
||||
Math.min(instr.id, scope.scope.range.start),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.traverseInstruction(instr, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override visitBlock(block: ReactiveBlock, state: Context): void {
|
||||
state.enter(() => {
|
||||
this.traverseBlock(block, state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type PendingReactiveScope = {active: boolean; scope: ReactiveScope};
|
||||
|
||||
class Context {
|
||||
/*
|
||||
* For each block scope (outer array) stores a list of ReactiveScopes that start
|
||||
* in that block scope.
|
||||
*/
|
||||
#blockScopes: Array<Array<PendingReactiveScope>> = [];
|
||||
|
||||
/*
|
||||
* ReactiveScopes whose declaring block scope has ended but may still need to
|
||||
* be "closed" (ie have their range.end be updated). A given scope can be in
|
||||
* blockScopes OR this array but not both.
|
||||
*/
|
||||
#unclosedScopes: Array<PendingReactiveScope> = [];
|
||||
|
||||
/*
|
||||
* Set of all scope ids that have been seen so far, regardless of which of
|
||||
* the above data structures they're in, to avoid tracking the same scope twice.
|
||||
*/
|
||||
#seenScopes: Set<ScopeId> = new Set();
|
||||
|
||||
currentScopes(): Array<PendingReactiveScope> {
|
||||
return this.#blockScopes.at(-1) ?? [];
|
||||
}
|
||||
|
||||
enter(fn: () => void): void {
|
||||
this.#blockScopes.push([]);
|
||||
fn();
|
||||
const lastScope = this.#blockScopes.pop()!;
|
||||
for (const scope of lastScope) {
|
||||
if (scope.active) {
|
||||
this.#unclosedScopes.push(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
const currentScopes = this.#blockScopes.at(-1)!;
|
||||
const scopes = [...currentScopes, ...this.#unclosedScopes];
|
||||
for (const pending of scopes) {
|
||||
if (!pending.active) {
|
||||
continue;
|
||||
}
|
||||
if (id >= pending.scope.range.end) {
|
||||
pending.active = false;
|
||||
pending.scope.range.end = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visitScope(scope: ReactiveScope): void {
|
||||
if (!this.#seenScopes.has(scope.id)) {
|
||||
const currentScopes = this.#blockScopes.at(-1)!;
|
||||
this.#seenScopes.add(scope.id);
|
||||
currentScopes.push({
|
||||
active: true,
|
||||
scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
getPlaceScope,
|
||||
makeInstructionId,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
terminalFallthrough,
|
||||
} from '../HIR/visitors';
|
||||
import {retainWhere_Set} from '../Utils/utils';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
|
||||
type InstructionRange = MutableRange;
|
||||
/*
|
||||
@@ -140,7 +140,7 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
|
||||
}
|
||||
|
||||
const fallthrough = terminalFallthrough(terminal);
|
||||
if (fallthrough !== null && terminal.kind !== 'branch') {
|
||||
if (fallthrough !== null) {
|
||||
/*
|
||||
* Any currently active scopes that overlaps the block-fallthrough range
|
||||
* need their range extended to at least the first instruction of the
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
ReactiveScopeBlock,
|
||||
ScopeId,
|
||||
} from '../HIR';
|
||||
import {getPlaceScope} from '../HIR/HIR';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {ReactiveFunctionVisitor} from './visitors';
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
BlockId,
|
||||
InstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
ScopeId,
|
||||
} from '../HIR';
|
||||
import {eachInstructionLValue} from '../HIR/visitors';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {eachReactiveValueOperand, mapTerminalBlocks} from './visitors';
|
||||
|
||||
/*
|
||||
* Note: this is the 4th of 4 passes that determine how to break a function into discrete
|
||||
* reactive scopes (independently memoizeable units of code):
|
||||
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
|
||||
* them a unique reactive scope.
|
||||
* 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
|
||||
* to block scopes.
|
||||
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
|
||||
* scopes do not overlap, merging any such scopes.
|
||||
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
|
||||
* a ReactiveScopeBlock.
|
||||
*
|
||||
* Given a function where the reactive scopes have been correctly aligned and merged,
|
||||
* this pass groups the instructions for each reactive scope into ReactiveBlocks.
|
||||
*/
|
||||
export function buildReactiveBlocks(fn: ReactiveFunction): void {
|
||||
const context = new Context();
|
||||
fn.body = context.enter(() => {
|
||||
visitBlock(context, fn.body);
|
||||
});
|
||||
}
|
||||
|
||||
class Context {
|
||||
#builders: Array<Builder> = [];
|
||||
#scopes: Set<ScopeId> = new Set();
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
const builder = this.#builders.at(-1)!;
|
||||
builder.visitId(id);
|
||||
}
|
||||
|
||||
visitScope(scope: ReactiveScope): void {
|
||||
if (this.#scopes.has(scope.id)) {
|
||||
return;
|
||||
}
|
||||
this.#scopes.add(scope.id);
|
||||
this.#builders.at(-1)!.startScope(scope);
|
||||
}
|
||||
|
||||
append(
|
||||
stmt: ReactiveStatement,
|
||||
label: {id: BlockId; implicit: boolean} | null,
|
||||
): void {
|
||||
this.#builders.at(-1)!.append(stmt, label);
|
||||
}
|
||||
|
||||
enter(fn: () => void): ReactiveBlock {
|
||||
const builder = new Builder();
|
||||
this.#builders.push(builder);
|
||||
fn();
|
||||
const popped = this.#builders.pop();
|
||||
CompilerError.invariant(popped === builder, {
|
||||
reason: 'Expected push/pop to be called 1:1',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
return builder.complete();
|
||||
}
|
||||
}
|
||||
|
||||
class Builder {
|
||||
#instructions: ReactiveBlock;
|
||||
#stack: Array<
|
||||
| {kind: 'scope'; block: ReactiveScopeBlock}
|
||||
| {kind: 'block'; block: ReactiveBlock}
|
||||
>;
|
||||
|
||||
constructor() {
|
||||
const block: ReactiveBlock = [];
|
||||
this.#instructions = block;
|
||||
this.#stack = [{kind: 'block', block}];
|
||||
}
|
||||
|
||||
append(
|
||||
item: ReactiveStatement,
|
||||
label: {id: BlockId; implicit: boolean} | null,
|
||||
): void {
|
||||
if (label !== null) {
|
||||
CompilerError.invariant(item.kind === 'terminal', {
|
||||
reason: 'Only terminals may have a label',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
item.label = label;
|
||||
}
|
||||
this.#instructions.push(item);
|
||||
}
|
||||
|
||||
startScope(scope: ReactiveScope): void {
|
||||
const block: ReactiveScopeBlock = {
|
||||
kind: 'scope',
|
||||
scope,
|
||||
instructions: [],
|
||||
};
|
||||
this.append(block, null);
|
||||
this.#instructions = block.instructions;
|
||||
this.#stack.push({kind: 'scope', block});
|
||||
}
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
for (let i = 0; i < this.#stack.length; i++) {
|
||||
const entry = this.#stack[i]!;
|
||||
if (entry.kind === 'scope' && id >= entry.block.scope.range.end) {
|
||||
this.#stack.length = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const last = this.#stack[this.#stack.length - 1]!;
|
||||
if (last.kind === 'block') {
|
||||
this.#instructions = last.block;
|
||||
} else {
|
||||
this.#instructions = last.block.instructions;
|
||||
}
|
||||
}
|
||||
|
||||
complete(): ReactiveBlock {
|
||||
/*
|
||||
* TODO: @josephsavona debug violations of this invariant
|
||||
* invariant(
|
||||
* this.#stack.length === 1,
|
||||
* "Expected all scopes to be closed when exiting a block"
|
||||
* );
|
||||
*/
|
||||
const first = this.#stack[0]!;
|
||||
CompilerError.invariant(first.kind === 'block', {
|
||||
reason: 'Expected first stack item to be a basic block',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
return first.block;
|
||||
}
|
||||
}
|
||||
|
||||
function visitBlock(context: Context, block: ReactiveBlock): void {
|
||||
for (const stmt of block) {
|
||||
switch (stmt.kind) {
|
||||
case 'instruction': {
|
||||
context.visitId(stmt.instruction.id);
|
||||
const scope = getInstructionScope(stmt.instruction);
|
||||
if (scope !== null) {
|
||||
context.visitScope(scope);
|
||||
}
|
||||
context.append(stmt, null);
|
||||
break;
|
||||
}
|
||||
case 'terminal': {
|
||||
const id = stmt.terminal.id;
|
||||
if (id !== null) {
|
||||
context.visitId(id);
|
||||
}
|
||||
mapTerminalBlocks(stmt.terminal, block => {
|
||||
return context.enter(() => {
|
||||
visitBlock(context, block);
|
||||
});
|
||||
});
|
||||
context.append(stmt, stmt.label);
|
||||
break;
|
||||
}
|
||||
case 'pruned-scope':
|
||||
case 'scope': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Expected the function to not have scopes already assigned',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
stmt,
|
||||
`Unexpected statement kind \`${(stmt as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getInstructionScope(
|
||||
instr: ReactiveInstruction,
|
||||
): ReactiveScope | null {
|
||||
CompilerError.invariant(instr.lvalue !== null, {
|
||||
reason:
|
||||
'Expected lvalues to not be null when assigning scopes. ' +
|
||||
'Pruning lvalues too early can result in missing scope information.',
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
const operandScope = getPlaceScope(instr.id, operand);
|
||||
if (operandScope !== null) {
|
||||
return operandScope;
|
||||
}
|
||||
}
|
||||
for (const operand of eachReactiveValueOperand(instr.value)) {
|
||||
const operandScope = getPlaceScope(instr.id, operand);
|
||||
if (operandScope !== null) {
|
||||
return operandScope;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getPlaceScope(
|
||||
id: InstructionId,
|
||||
place: Place,
|
||||
): ReactiveScope | null {
|
||||
const scope = place.identifier.scope;
|
||||
if (scope !== null && isScopeActive(scope, id)) {
|
||||
return scope;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isScopeActive(scope: ReactiveScope, id: InstructionId): boolean {
|
||||
return id >= scope.range.start && id < scope.range.end;
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import {Environment, EnvironmentConfig, ExternalFunction} from '../HIR';
|
||||
import {
|
||||
ArrayPattern,
|
||||
BlockId,
|
||||
DeclarationId,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
@@ -97,11 +96,6 @@ export type CodegenFunction = {
|
||||
fn: CodegenFunction;
|
||||
type: ReactFunctionType | null;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* This is true if the compiler has the lowered useContext calls.
|
||||
*/
|
||||
hasLoweredContextAccess: boolean;
|
||||
};
|
||||
|
||||
export function codegenFunction(
|
||||
@@ -315,9 +309,9 @@ function codegenReactiveFunction(
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
cx.temp.set(param.identifier.declarationId, null);
|
||||
cx.temp.set(param.identifier.id, null);
|
||||
} else {
|
||||
cx.temp.set(param.place.identifier.declarationId, null);
|
||||
cx.temp.set(param.place.identifier.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +347,6 @@ function codegenReactiveFunction(
|
||||
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
|
||||
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
|
||||
outlined: [],
|
||||
hasLoweredContextAccess: fn.env.hasLoweredContextAccess,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,11 +392,7 @@ class Context {
|
||||
env: Environment;
|
||||
fnName: string;
|
||||
#nextCacheIndex: number = 0;
|
||||
/**
|
||||
* Tracks which named variables have been declared to dedupe declarations,
|
||||
* so this uses DeclarationId instead of IdentifierId
|
||||
*/
|
||||
#declarations: Set<DeclarationId> = new Set();
|
||||
#declarations: Set<IdentifierId> = new Set();
|
||||
temp: Temporaries;
|
||||
errors: CompilerError = new CompilerError();
|
||||
objectMethods: Map<IdentifierId, ObjectMethod> = new Map();
|
||||
@@ -429,11 +418,11 @@ class Context {
|
||||
}
|
||||
|
||||
declare(identifier: Identifier): void {
|
||||
this.#declarations.add(identifier.declarationId);
|
||||
this.#declarations.add(identifier.id);
|
||||
}
|
||||
|
||||
hasDeclared(identifier: Identifier): boolean {
|
||||
return this.#declarations.has(identifier.declarationId);
|
||||
return this.#declarations.has(identifier.id);
|
||||
}
|
||||
|
||||
synthesizeName(name: string): ValidIdentifierName {
|
||||
@@ -981,12 +970,15 @@ function codegenTerminal(
|
||||
suggestions: null,
|
||||
});
|
||||
case InstructionKind.Catch:
|
||||
case InstructionKind.HoistedConst:
|
||||
case InstructionKind.HoistedLet:
|
||||
case InstructionKind.HoistedFunction:
|
||||
case InstructionKind.Function:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
|
||||
reason: 'Unexpected catch variable as for..in collection',
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
case InstructionKind.HoistedConst:
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected HoistedConst variable in for..in collection',
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
suggestions: null,
|
||||
@@ -1065,13 +1057,23 @@ function codegenTerminal(
|
||||
varDeclKind = 'let' as const;
|
||||
break;
|
||||
case InstructionKind.Reassign:
|
||||
case InstructionKind.Catch:
|
||||
case InstructionKind.HoistedConst:
|
||||
case InstructionKind.HoistedLet:
|
||||
case InstructionKind.HoistedFunction:
|
||||
case InstructionKind.Function:
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
|
||||
reason:
|
||||
'Destructure should never be Reassign as it would be an Object/ArrayPattern',
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
case InstructionKind.Catch:
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected catch variable as for..of collection',
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
case InstructionKind.HoistedConst:
|
||||
CompilerError.invariant(false, {
|
||||
reason: 'Unexpected HoistedConst variable in for..of collection',
|
||||
description: null,
|
||||
loc: iterableItem.loc,
|
||||
suggestions: null,
|
||||
@@ -1145,7 +1147,7 @@ function codegenTerminal(
|
||||
let catchParam = null;
|
||||
if (terminal.handlerBinding !== null) {
|
||||
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
|
||||
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
|
||||
cx.temp.set(terminal.handlerBinding.identifier.id, null);
|
||||
}
|
||||
return t.tryStatement(
|
||||
codegenBlock(cx, terminal.block),
|
||||
@@ -1196,20 +1198,20 @@ function codegenInstructionNullable(
|
||||
value = null;
|
||||
} else {
|
||||
lvalue = instr.value.lvalue.pattern;
|
||||
let hasReassign = false;
|
||||
let hasReasign = false;
|
||||
let hasDeclaration = false;
|
||||
for (const place of eachPatternOperand(lvalue)) {
|
||||
if (
|
||||
kind !== InstructionKind.Reassign &&
|
||||
place.identifier.name === null
|
||||
) {
|
||||
cx.temp.set(place.identifier.declarationId, null);
|
||||
cx.temp.set(place.identifier.id, null);
|
||||
}
|
||||
const isDeclared = cx.hasDeclared(place.identifier);
|
||||
hasReassign ||= isDeclared;
|
||||
hasReasign ||= isDeclared;
|
||||
hasDeclaration ||= !isDeclared;
|
||||
}
|
||||
if (hasReassign && hasDeclaration) {
|
||||
if (hasReasign && hasDeclaration) {
|
||||
CompilerError.invariant(false, {
|
||||
reason:
|
||||
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
|
||||
@@ -1217,7 +1219,7 @@ function codegenInstructionNullable(
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
} else if (hasReassign) {
|
||||
} else if (hasReasign) {
|
||||
kind = InstructionKind.Reassign;
|
||||
}
|
||||
value = codegenPlaceToExpression(cx, instr.value.value);
|
||||
@@ -1234,35 +1236,6 @@ function codegenInstructionNullable(
|
||||
t.variableDeclarator(codegenLValue(cx, lvalue), value),
|
||||
]);
|
||||
}
|
||||
case InstructionKind.Function: {
|
||||
CompilerError.invariant(instr.lvalue === null, {
|
||||
reason: `Function declaration cannot be referenced as an expression`,
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
const genLvalue = codegenLValue(cx, lvalue);
|
||||
CompilerError.invariant(genLvalue.type === 'Identifier', {
|
||||
reason: 'Expected an identifier as a function declaration lvalue',
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
CompilerError.invariant(value?.type === 'FunctionExpression', {
|
||||
reason: 'Expected a function as a function declaration value',
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
return createFunctionDeclaration(
|
||||
instr.loc,
|
||||
genLvalue,
|
||||
value.params,
|
||||
value.body,
|
||||
value.generator,
|
||||
value.async,
|
||||
);
|
||||
}
|
||||
case InstructionKind.Let: {
|
||||
CompilerError.invariant(instr.lvalue === null, {
|
||||
reason: `Const declaration cannot be referenced as an expression`,
|
||||
@@ -1288,7 +1261,7 @@ function codegenInstructionNullable(
|
||||
);
|
||||
if (instr.lvalue !== null) {
|
||||
if (instr.value.kind !== 'StoreContext') {
|
||||
cx.temp.set(instr.lvalue.identifier.declarationId, expr);
|
||||
cx.temp.set(instr.lvalue.identifier.id, expr);
|
||||
return null;
|
||||
} else {
|
||||
// Handle chained reassignments for context variables
|
||||
@@ -1305,11 +1278,10 @@ function codegenInstructionNullable(
|
||||
case InstructionKind.Catch: {
|
||||
return t.emptyStatement();
|
||||
}
|
||||
case InstructionKind.HoistedLet:
|
||||
case InstructionKind.HoistedConst:
|
||||
case InstructionKind.HoistedFunction: {
|
||||
case InstructionKind.HoistedConst: {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
|
||||
reason:
|
||||
'Expected HoistedConsts to have been pruned in PruneHoistedContexts',
|
||||
description: null,
|
||||
loc: instr.loc,
|
||||
suggestions: null,
|
||||
@@ -1405,7 +1377,7 @@ function printDependencyComment(dependency: ReactiveScopeDependency): string {
|
||||
let name = identifier.name;
|
||||
if (dependency.path !== null) {
|
||||
for (const path of dependency.path) {
|
||||
name += `.${path.property}`;
|
||||
name += `.${path}`;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
@@ -1440,19 +1412,9 @@ function codegenDependency(
|
||||
dependency: ReactiveScopeDependency,
|
||||
): t.Expression {
|
||||
let object: t.Expression = convertIdentifier(dependency.identifier);
|
||||
if (dependency.path.length !== 0) {
|
||||
const hasOptional = dependency.path.some(path => path.optional);
|
||||
if (dependency.path !== null) {
|
||||
for (const path of dependency.path) {
|
||||
if (hasOptional) {
|
||||
object = t.optionalMemberExpression(
|
||||
object,
|
||||
t.identifier(path.property),
|
||||
false,
|
||||
path.optional,
|
||||
);
|
||||
} else {
|
||||
object = t.memberExpression(object, t.identifier(path.property));
|
||||
}
|
||||
object = t.memberExpression(object, t.identifier(path));
|
||||
}
|
||||
}
|
||||
return object;
|
||||
@@ -1480,7 +1442,6 @@ const createBinaryExpression = withLoc(t.binaryExpression);
|
||||
const createExpressionStatement = withLoc(t.expressionStatement);
|
||||
const _createLabelledStatement = withLoc(t.labeledStatement);
|
||||
const createVariableDeclaration = withLoc(t.variableDeclaration);
|
||||
const createFunctionDeclaration = withLoc(t.functionDeclaration);
|
||||
const _createWhileStatement = withLoc(t.whileStatement);
|
||||
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
|
||||
const createLogicalExpression = withLoc(t.logicalExpression);
|
||||
@@ -1569,7 +1530,7 @@ function createCallExpression(
|
||||
}
|
||||
}
|
||||
|
||||
type Temporaries = Map<DeclarationId, t.Expression | t.JSXText | null>;
|
||||
type Temporaries = Map<IdentifierId, t.Expression | t.JSXText | null>;
|
||||
|
||||
function codegenLabel(id: BlockId): string {
|
||||
return `bb${id}`;
|
||||
@@ -1588,7 +1549,7 @@ function codegenInstruction(
|
||||
}
|
||||
if (instr.lvalue.identifier.name === null) {
|
||||
// temporary
|
||||
cx.temp.set(instr.lvalue.identifier.declarationId, value);
|
||||
cx.temp.set(instr.lvalue.identifier.id, value);
|
||||
return t.emptyStatement();
|
||||
} else {
|
||||
const expressionValue = convertValueToExpression(value);
|
||||
@@ -2036,7 +1997,7 @@ function codegenInstructionValue(
|
||||
),
|
||||
reactiveFunction,
|
||||
).unwrap();
|
||||
if (instrValue.type === 'ArrowFunctionExpression') {
|
||||
if (instrValue.expr.type === 'ArrowFunctionExpression') {
|
||||
let body: t.BlockStatement | t.Expression = fn.body;
|
||||
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
|
||||
const stmt = body.body[0]!;
|
||||
@@ -2537,7 +2498,7 @@ function codegenPlaceToExpression(cx: Context, place: Place): t.Expression {
|
||||
}
|
||||
|
||||
function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
|
||||
let tmp = cx.temp.get(place.identifier.declarationId);
|
||||
let tmp = cx.temp.get(place.identifier.id);
|
||||
if (tmp != null) {
|
||||
return tmp;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {DependencyPath, Identifier, ReactiveScopeDependency} from '../HIR';
|
||||
import {Identifier, ReactiveScopeDependency} from '../HIR';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
|
||||
@@ -14,8 +14,20 @@ import {assertExhaustive} from '../Utils/utils';
|
||||
* We need to understand optional member expressions only when determining
|
||||
* dependencies of a ReactiveScope (i.e. in {@link PropagateScopeDependencies}),
|
||||
* hence why this type lives here (not in HIR.ts)
|
||||
*
|
||||
* {@link ReactiveScopePropertyDependency.optionalPath} is populated only if the Property
|
||||
* represents an optional member expression, and it represents the property path
|
||||
* loaded conditionally.
|
||||
* e.g. the member expr a.b.c?.d.e?.f is represented as
|
||||
* {
|
||||
* identifier: 'a';
|
||||
* path: ['b', 'c'],
|
||||
* optionalPath: ['d', 'e', 'f'].
|
||||
* }
|
||||
*/
|
||||
export type ReactiveScopePropertyDependency = ReactiveScopeDependency;
|
||||
export type ReactiveScopePropertyDependency = ReactiveScopeDependency & {
|
||||
optionalPath: Array<string>;
|
||||
};
|
||||
|
||||
/*
|
||||
* Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional
|
||||
@@ -57,43 +69,68 @@ export class ReactiveScopeDependencyTree {
|
||||
}
|
||||
|
||||
add(dep: ReactiveScopePropertyDependency, inConditional: boolean): void {
|
||||
const {path} = dep;
|
||||
const {path, optionalPath} = dep;
|
||||
let currNode = this.#getOrCreateRoot(dep.identifier);
|
||||
|
||||
for (const item of path) {
|
||||
const accessType = inConditional
|
||||
? PropertyAccessType.ConditionalAccess
|
||||
: PropertyAccessType.UnconditionalAccess;
|
||||
|
||||
for (const property of path) {
|
||||
// all properties read 'on the way' to a dependency are marked as 'access'
|
||||
let currChild = getOrMakeProperty(currNode, item.property);
|
||||
const accessType = inConditional
|
||||
? PropertyAccessType.ConditionalAccess
|
||||
: item.optional
|
||||
? PropertyAccessType.OptionalAccess
|
||||
: PropertyAccessType.UnconditionalAccess;
|
||||
let currChild = getOrMakeProperty(currNode, property);
|
||||
currChild.accessType = merge(currChild.accessType, accessType);
|
||||
currNode = currChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final property node should be marked as an conditional/unconditional
|
||||
* `dependency` as based on control flow.
|
||||
*/
|
||||
const depType = inConditional
|
||||
? PropertyAccessType.ConditionalDependency
|
||||
: isOptional(currNode.accessType)
|
||||
? PropertyAccessType.OptionalDependency
|
||||
if (optionalPath.length === 0) {
|
||||
/*
|
||||
* If this property does not have a conditional path (i.e. a.b.c), the
|
||||
* final property node should be marked as an conditional/unconditional
|
||||
* `dependency` as based on control flow.
|
||||
*/
|
||||
const depType = inConditional
|
||||
? PropertyAccessType.ConditionalDependency
|
||||
: PropertyAccessType.UnconditionalDependency;
|
||||
|
||||
currNode.accessType = merge(currNode.accessType, depType);
|
||||
currNode.accessType = merge(currNode.accessType, depType);
|
||||
} else {
|
||||
/*
|
||||
* Technically, we only depend on whether unconditional path `dep.path`
|
||||
* is nullish (not its actual value). As long as we preserve the nullthrows
|
||||
* behavior of `dep.path`, we can keep it as an access (and not promote
|
||||
* to a dependency).
|
||||
* See test `reduce-reactive-cond-memberexpr-join` for example.
|
||||
*/
|
||||
|
||||
/*
|
||||
* If this property has an optional path (i.e. a?.b.c), all optional
|
||||
* nodes should be marked accordingly.
|
||||
*/
|
||||
for (const property of optionalPath) {
|
||||
let currChild = getOrMakeProperty(currNode, property);
|
||||
currChild.accessType = merge(
|
||||
currChild.accessType,
|
||||
PropertyAccessType.ConditionalAccess,
|
||||
);
|
||||
currNode = currChild;
|
||||
}
|
||||
|
||||
// The final node should be marked as a conditional dependency.
|
||||
currNode.accessType = merge(
|
||||
currNode.accessType,
|
||||
PropertyAccessType.ConditionalDependency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
|
||||
const results = new Set<ReactiveScopeDependency>();
|
||||
for (const [rootId, rootNode] of this.#roots.entries()) {
|
||||
const deps = deriveMinimalDependenciesInSubtree(rootNode, null);
|
||||
const deps = deriveMinimalDependenciesInSubtree(rootNode);
|
||||
CompilerError.invariant(
|
||||
deps.every(
|
||||
dep =>
|
||||
dep.accessType === PropertyAccessType.UnconditionalDependency ||
|
||||
dep.accessType == PropertyAccessType.OptionalDependency,
|
||||
dep => dep.accessType === PropertyAccessType.UnconditionalDependency,
|
||||
),
|
||||
{
|
||||
reason:
|
||||
@@ -178,27 +215,6 @@ export class ReactiveScopeDependencyTree {
|
||||
}
|
||||
return res.flat().join('\n');
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
const buf: Array<string> = [`tree() [`];
|
||||
for (const [rootId, rootNode] of this.#roots) {
|
||||
buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`);
|
||||
this.#debugImpl(buf, rootNode, 1);
|
||||
}
|
||||
buf.push(']');
|
||||
return buf.length > 2 ? buf.join('\n') : buf.join('');
|
||||
}
|
||||
|
||||
#debugImpl(
|
||||
buf: Array<string>,
|
||||
node: DependencyNode,
|
||||
depth: number = 0,
|
||||
): void {
|
||||
for (const [property, childNode] of node.properties) {
|
||||
buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`);
|
||||
this.#debugImpl(buf, childNode, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -222,10 +238,8 @@ export class ReactiveScopeDependencyTree {
|
||||
*/
|
||||
enum PropertyAccessType {
|
||||
ConditionalAccess = 'ConditionalAccess',
|
||||
OptionalAccess = 'OptionalAccess',
|
||||
UnconditionalAccess = 'UnconditionalAccess',
|
||||
ConditionalDependency = 'ConditionalDependency',
|
||||
OptionalDependency = 'OptionalDependency',
|
||||
UnconditionalDependency = 'UnconditionalDependency',
|
||||
}
|
||||
|
||||
@@ -239,16 +253,9 @@ function isUnconditional(access: PropertyAccessType): boolean {
|
||||
function isDependency(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.ConditionalDependency ||
|
||||
access === PropertyAccessType.OptionalDependency ||
|
||||
access === PropertyAccessType.UnconditionalDependency
|
||||
);
|
||||
}
|
||||
function isOptional(access: PropertyAccessType): boolean {
|
||||
return (
|
||||
access === PropertyAccessType.OptionalAccess ||
|
||||
access === PropertyAccessType.OptionalDependency
|
||||
);
|
||||
}
|
||||
|
||||
function merge(
|
||||
access1: PropertyAccessType,
|
||||
@@ -257,7 +264,6 @@ function merge(
|
||||
const resultIsUnconditional =
|
||||
isUnconditional(access1) || isUnconditional(access2);
|
||||
const resultIsDependency = isDependency(access1) || isDependency(access2);
|
||||
const resultIsOptional = isOptional(access1) || isOptional(access2);
|
||||
|
||||
/*
|
||||
* Straightforward merge.
|
||||
@@ -273,12 +279,6 @@ function merge(
|
||||
} else {
|
||||
return PropertyAccessType.UnconditionalAccess;
|
||||
}
|
||||
} else if (resultIsOptional) {
|
||||
if (resultIsDependency) {
|
||||
return PropertyAccessType.OptionalDependency;
|
||||
} else {
|
||||
return PropertyAccessType.OptionalAccess;
|
||||
}
|
||||
} else {
|
||||
if (resultIsDependency) {
|
||||
return PropertyAccessType.ConditionalDependency;
|
||||
@@ -294,38 +294,23 @@ type DependencyNode = {
|
||||
};
|
||||
|
||||
type ReduceResultNode = {
|
||||
relativePath: DependencyPath;
|
||||
relativePath: Array<string>;
|
||||
accessType: PropertyAccessType;
|
||||
};
|
||||
|
||||
function promoteResult(
|
||||
accessType: PropertyAccessType,
|
||||
path: {property: string; optional: boolean} | null,
|
||||
): Array<ReduceResultNode> {
|
||||
const result: ReduceResultNode = {
|
||||
const promoteUncondResult = [
|
||||
{
|
||||
relativePath: [],
|
||||
accessType,
|
||||
};
|
||||
if (path !== null) {
|
||||
result.relativePath.push(path);
|
||||
}
|
||||
return [result];
|
||||
}
|
||||
accessType: PropertyAccessType.UnconditionalDependency,
|
||||
},
|
||||
];
|
||||
|
||||
function prependPath(
|
||||
results: Array<ReduceResultNode>,
|
||||
path: {property: string; optional: boolean} | null,
|
||||
): Array<ReduceResultNode> {
|
||||
if (path === null) {
|
||||
return results;
|
||||
}
|
||||
return results.map(result => {
|
||||
return {
|
||||
accessType: result.accessType,
|
||||
relativePath: [path, ...result.relativePath],
|
||||
};
|
||||
});
|
||||
}
|
||||
const promoteCondResult = [
|
||||
{
|
||||
relativePath: [],
|
||||
accessType: PropertyAccessType.ConditionalDependency,
|
||||
},
|
||||
];
|
||||
|
||||
/*
|
||||
* Recursively calculates minimal dependencies in a subtree.
|
||||
@@ -334,76 +319,39 @@ function prependPath(
|
||||
*/
|
||||
function deriveMinimalDependenciesInSubtree(
|
||||
dep: DependencyNode,
|
||||
property: string | null,
|
||||
): Array<ReduceResultNode> {
|
||||
const results: Array<ReduceResultNode> = [];
|
||||
for (const [childName, childNode] of dep.properties) {
|
||||
const childResult = deriveMinimalDependenciesInSubtree(
|
||||
childNode,
|
||||
childName,
|
||||
const childResult = deriveMinimalDependenciesInSubtree(childNode).map(
|
||||
({relativePath, accessType}) => {
|
||||
return {
|
||||
relativePath: [childName, ...relativePath],
|
||||
accessType,
|
||||
};
|
||||
},
|
||||
);
|
||||
results.push(...childResult);
|
||||
}
|
||||
|
||||
switch (dep.accessType) {
|
||||
case PropertyAccessType.UnconditionalDependency: {
|
||||
return promoteResult(
|
||||
PropertyAccessType.UnconditionalDependency,
|
||||
property !== null ? {property, optional: false} : null,
|
||||
);
|
||||
return promoteUncondResult;
|
||||
}
|
||||
case PropertyAccessType.UnconditionalAccess: {
|
||||
if (
|
||||
results.every(
|
||||
({accessType}) =>
|
||||
accessType === PropertyAccessType.UnconditionalDependency ||
|
||||
accessType === PropertyAccessType.OptionalDependency,
|
||||
accessType === PropertyAccessType.UnconditionalDependency,
|
||||
)
|
||||
) {
|
||||
// all children are unconditional dependencies, return them to preserve granularity
|
||||
return prependPath(
|
||||
results,
|
||||
property !== null ? {property, optional: false} : null,
|
||||
);
|
||||
return results;
|
||||
} else {
|
||||
/*
|
||||
* at least one child is accessed conditionally, so this node needs to be promoted to
|
||||
* unconditional dependency
|
||||
*/
|
||||
return promoteResult(
|
||||
PropertyAccessType.UnconditionalDependency,
|
||||
property !== null ? {property, optional: false} : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
case PropertyAccessType.OptionalDependency: {
|
||||
return promoteResult(
|
||||
PropertyAccessType.OptionalDependency,
|
||||
property !== null ? {property, optional: true} : null,
|
||||
);
|
||||
}
|
||||
case PropertyAccessType.OptionalAccess: {
|
||||
if (
|
||||
results.every(
|
||||
({accessType}) =>
|
||||
accessType === PropertyAccessType.UnconditionalDependency ||
|
||||
accessType === PropertyAccessType.OptionalDependency,
|
||||
)
|
||||
) {
|
||||
// all children are unconditional dependencies, return them to preserve granularity
|
||||
return prependPath(
|
||||
results,
|
||||
property !== null ? {property, optional: true} : null,
|
||||
);
|
||||
} else {
|
||||
/*
|
||||
* at least one child is accessed conditionally, so this node needs to be promoted to
|
||||
* unconditional dependency
|
||||
*/
|
||||
return promoteResult(
|
||||
PropertyAccessType.OptionalDependency,
|
||||
property !== null ? {property, optional: true} : null,
|
||||
);
|
||||
return promoteUncondResult;
|
||||
}
|
||||
}
|
||||
case PropertyAccessType.ConditionalAccess:
|
||||
@@ -419,19 +367,13 @@ function deriveMinimalDependenciesInSubtree(
|
||||
* unconditional access.
|
||||
* Truncate results of child nodes here, since we shouldn't access them anyways
|
||||
*/
|
||||
return promoteResult(
|
||||
PropertyAccessType.ConditionalDependency,
|
||||
property !== null ? {property, optional: true} : null,
|
||||
);
|
||||
return promoteCondResult;
|
||||
} else {
|
||||
/*
|
||||
* at least one child is accessed unconditionally, so this node can be promoted to
|
||||
* unconditional dependency
|
||||
*/
|
||||
return promoteResult(
|
||||
PropertyAccessType.UnconditionalDependency,
|
||||
property !== null ? {property, optional: true} : null,
|
||||
);
|
||||
return promoteUncondResult;
|
||||
}
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
DeclarationId,
|
||||
Destructure,
|
||||
Environment,
|
||||
IdentifierId,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
ReactiveStatement,
|
||||
promoteTemporary,
|
||||
} from '../HIR';
|
||||
import {clonePlaceToTemporary} from '../HIR/HIRBuilder';
|
||||
import {eachPatternOperand, mapPatternOperands} from '../HIR/visitors';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
@@ -84,11 +82,7 @@ export function extractScopeDeclarationsFromDestructuring(
|
||||
|
||||
class State {
|
||||
env: Environment;
|
||||
/**
|
||||
* We need to track which program variables are already declared to convert
|
||||
* declarations into reassignments, so we use DeclarationId
|
||||
*/
|
||||
declared: Set<DeclarationId> = new Set();
|
||||
declared: Set<IdentifierId> = new Set();
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
@@ -98,7 +92,7 @@ class State {
|
||||
class Visitor extends ReactiveFunctionTransform<State> {
|
||||
override visitScope(scope: ReactiveScopeBlock, state: State): void {
|
||||
for (const [, declaration] of scope.scope.declarations) {
|
||||
state.declared.add(declaration.identifier.declarationId);
|
||||
state.declared.add(declaration.identifier.id);
|
||||
}
|
||||
this.traverseScope(scope, state);
|
||||
}
|
||||
@@ -137,7 +131,7 @@ function transformDestructuring(
|
||||
let reassigned: Set<IdentifierId> = new Set();
|
||||
let hasDeclaration = false;
|
||||
for (const place of eachPatternOperand(destructure.lvalue.pattern)) {
|
||||
const isDeclared = state.declared.has(place.identifier.declarationId);
|
||||
const isDeclared = state.declared.has(place.identifier.id);
|
||||
if (isDeclared) {
|
||||
reassigned.add(place.identifier.id);
|
||||
}
|
||||
@@ -156,7 +150,15 @@ function transformDestructuring(
|
||||
if (!reassigned.has(place.identifier.id)) {
|
||||
return place;
|
||||
}
|
||||
const temporary = clonePlaceToTemporary(state.env, place);
|
||||
const tempId = state.env.nextIdentifierId;
|
||||
const temporary = {
|
||||
...place,
|
||||
identifier: {
|
||||
...place.identifier,
|
||||
id: tempId,
|
||||
name: null, // overwritten below
|
||||
},
|
||||
};
|
||||
promoteTemporary(temporary.identifier);
|
||||
renamed.set(place, temporary);
|
||||
return temporary;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 {
|
||||
ReactiveFunction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
ReactiveTerminal,
|
||||
ReactiveTerminalStatement,
|
||||
} from '../HIR/HIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
|
||||
/*
|
||||
* Given a reactive function, flattens any scopes contained within a loop construct.
|
||||
* We won't initially support memoization within loops though this is possible in the future.
|
||||
*/
|
||||
export function flattenReactiveLoops(fn: ReactiveFunction): void {
|
||||
visitReactiveFunction(fn, new Transform(), false);
|
||||
}
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<boolean> {
|
||||
override transformScope(
|
||||
scope: ReactiveScopeBlock,
|
||||
isWithinLoop: boolean,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scope, isWithinLoop);
|
||||
if (isWithinLoop) {
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scope.scope,
|
||||
instructions: scope.instructions,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
override visitTerminal(
|
||||
stmt: ReactiveTerminalStatement<ReactiveTerminal>,
|
||||
isWithinLoop: boolean,
|
||||
): void {
|
||||
switch (stmt.terminal.kind) {
|
||||
// Loop terminals flatten nested scopes
|
||||
case 'do-while':
|
||||
case 'while':
|
||||
case 'for':
|
||||
case 'for-of':
|
||||
case 'for-in': {
|
||||
this.traverseTerminal(stmt, true);
|
||||
break;
|
||||
}
|
||||
// Non-loop terminals passthrough is contextual, inherits the parent isWithinScope
|
||||
case 'try':
|
||||
case 'label':
|
||||
case 'break':
|
||||
case 'continue':
|
||||
case 'if':
|
||||
case 'return':
|
||||
case 'switch':
|
||||
case 'throw': {
|
||||
this.traverseTerminal(stmt, isWithinLoop);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
stmt.terminal,
|
||||
`Unexpected terminal kind \`${(stmt.terminal as any).kind}\``,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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 {
|
||||
Environment,
|
||||
InstructionId,
|
||||
ReactiveFunction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveStatement,
|
||||
ReactiveValue,
|
||||
getHookKind,
|
||||
isUseOperator,
|
||||
} from '../HIR';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
Transformed,
|
||||
visitReactiveFunction,
|
||||
} from './visitors';
|
||||
|
||||
/**
|
||||
* For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different
|
||||
* from regular functions in two key ways:
|
||||
* - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces)
|
||||
* - They cannot be called conditionally
|
||||
*
|
||||
* The `use` operator is similar:
|
||||
* - It can access context, and therefore introduce reactivity
|
||||
* - It can be called conditionally, but _it must be called if the component needs the return value_. This is because
|
||||
* React uses the fact that use was called to remember that the component needs the value, and that changes to the
|
||||
* input should invalidate the component itself.
|
||||
*
|
||||
* This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly
|
||||
* different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them
|
||||
* called conditionally in the output.
|
||||
*
|
||||
* The pass finds and removes any scopes that transitively contain a hook or use call. By running all
|
||||
* the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately
|
||||
* describe the set of values which "construct together", and remove _all_ that memoization in order
|
||||
* to ensure the hook call does not inadvertently become conditional.
|
||||
*/
|
||||
export function flattenScopesWithHooksOrUse(fn: ReactiveFunction): void {
|
||||
visitReactiveFunction(fn, new Transform(), {
|
||||
env: fn.env,
|
||||
hasHook: false,
|
||||
});
|
||||
}
|
||||
|
||||
type State = {
|
||||
env: Environment;
|
||||
hasHook: boolean;
|
||||
};
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<State> {
|
||||
override transformScope(
|
||||
scope: ReactiveScopeBlock,
|
||||
outerState: State,
|
||||
): Transformed<ReactiveStatement> {
|
||||
const innerState: State = {
|
||||
env: outerState.env,
|
||||
hasHook: false,
|
||||
};
|
||||
this.visitScope(scope, innerState);
|
||||
outerState.hasHook ||= innerState.hasHook;
|
||||
if (innerState.hasHook) {
|
||||
if (scope.instructions.length === 1) {
|
||||
/*
|
||||
* This was a scope just for a hook call, which doesn't need memoization.
|
||||
* flatten it away
|
||||
*/
|
||||
return {
|
||||
kind: 'replace-many',
|
||||
value: scope.instructions,
|
||||
};
|
||||
}
|
||||
/*
|
||||
* else this scope had multiple instructions and produced some other value:
|
||||
* mark it as pruned
|
||||
*/
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
kind: 'pruned-scope',
|
||||
scope: scope.scope,
|
||||
instructions: scope.instructions,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {kind: 'keep'};
|
||||
}
|
||||
}
|
||||
|
||||
override visitValue(
|
||||
id: InstructionId,
|
||||
value: ReactiveValue,
|
||||
state: State,
|
||||
): void {
|
||||
this.traverseValue(id, value, state);
|
||||
switch (value.kind) {
|
||||
case 'CallExpression': {
|
||||
if (
|
||||
getHookKind(state.env, value.callee.identifier) != null ||
|
||||
isUseOperator(value.callee.identifier)
|
||||
) {
|
||||
state.hasHook = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MethodCall': {
|
||||
if (
|
||||
getHookKind(state.env, value.property.identifier) != null ||
|
||||
isUseOperator(value.property.identifier)
|
||||
) {
|
||||
state.hasHook = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,10 @@
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {Environment} from '../HIR';
|
||||
import {
|
||||
DeclarationId,
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
Instruction,
|
||||
InstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveScope,
|
||||
makeInstructionId,
|
||||
@@ -188,14 +185,8 @@ function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation {
|
||||
}
|
||||
|
||||
// Is the operand mutable at this given instruction
|
||||
export function isMutable(instr: {id: InstructionId}, place: Place): boolean {
|
||||
return inRange(instr, place.identifier.mutableRange);
|
||||
}
|
||||
|
||||
export function inRange(
|
||||
{id}: {id: InstructionId},
|
||||
range: MutableRange,
|
||||
): boolean {
|
||||
export function isMutable({id}: Instruction, place: Place): boolean {
|
||||
const range = place.identifier.mutableRange;
|
||||
return id >= range.start && id < range.end;
|
||||
}
|
||||
|
||||
@@ -235,7 +226,6 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
|
||||
case 'StoreGlobal': {
|
||||
return false;
|
||||
}
|
||||
case 'TaggedTemplateExpression':
|
||||
case 'CallExpression':
|
||||
case 'MethodCall': {
|
||||
return instruction.lvalue.identifier.type.kind !== 'Primitive';
|
||||
@@ -250,7 +240,8 @@ function mayAllocate(env: Environment, instruction: Instruction): boolean {
|
||||
case 'ObjectExpression':
|
||||
case 'UnsupportedNode':
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
case 'FunctionExpression':
|
||||
case 'TaggedTemplateExpression': {
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
@@ -266,14 +257,6 @@ export function findDisjointMutableValues(
|
||||
fn: HIRFunction,
|
||||
): DisjointSet<Identifier> {
|
||||
const scopeIdentifiers = new DisjointSet<Identifier>();
|
||||
|
||||
const declarations = new Map<DeclarationId, Identifier>();
|
||||
function declareIdentifier(lvalue: Place): void {
|
||||
if (!declarations.has(lvalue.identifier.declarationId)) {
|
||||
declarations.set(lvalue.identifier.declarationId, lvalue.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_, block] of fn.body.blocks) {
|
||||
/*
|
||||
* If a phi is mutated after creation, then we need to alias all of its operands such that they
|
||||
@@ -281,19 +264,14 @@ export function findDisjointMutableValues(
|
||||
*/
|
||||
for (const phi of block.phis) {
|
||||
if (
|
||||
// The phi was reset because it was not mutated after creation
|
||||
phi.id.mutableRange.start + 1 !== phi.id.mutableRange.end &&
|
||||
phi.id.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id)
|
||||
) {
|
||||
const operands = [phi.id];
|
||||
const declaration = declarations.get(phi.id.declarationId);
|
||||
if (declaration !== undefined) {
|
||||
operands.push(declaration);
|
||||
for (const [, phiId] of phi.operands) {
|
||||
scopeIdentifiers.union([phi.id, phiId]);
|
||||
}
|
||||
for (const [_, phiId] of phi.operands) {
|
||||
operands.push(phiId);
|
||||
}
|
||||
scopeIdentifiers.union(operands);
|
||||
} else if (fn.env.config.enableForest) {
|
||||
for (const [, phiId] of phi.operands) {
|
||||
scopeIdentifiers.union([phi.id, phiId]);
|
||||
@@ -308,15 +286,9 @@ export function findDisjointMutableValues(
|
||||
operands.push(instr.lvalue!.identifier);
|
||||
}
|
||||
if (
|
||||
instr.value.kind === 'DeclareLocal' ||
|
||||
instr.value.kind === 'DeclareContext'
|
||||
) {
|
||||
declareIdentifier(instr.value.lvalue.place);
|
||||
} else if (
|
||||
instr.value.kind === 'StoreLocal' ||
|
||||
instr.value.kind === 'StoreContext'
|
||||
) {
|
||||
declareIdentifier(instr.value.lvalue.place);
|
||||
if (
|
||||
instr.value.lvalue.place.identifier.mutableRange.end >
|
||||
instr.value.lvalue.place.identifier.mutableRange.start + 1
|
||||
@@ -331,7 +303,6 @@ export function findDisjointMutableValues(
|
||||
}
|
||||
} else if (instr.value.kind === 'Destructure') {
|
||||
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
|
||||
declareIdentifier(place);
|
||||
if (
|
||||
place.identifier.mutableRange.end >
|
||||
place.identifier.mutableRange.start + 1
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
HIRFunction,
|
||||
IdentifierId,
|
||||
makeInstructionId,
|
||||
MutableRange,
|
||||
Place,
|
||||
ReactiveValue,
|
||||
} from '../HIR';
|
||||
import {Macro, MacroMethod} from '../HIR/Environment';
|
||||
import {eachReactiveValueOperand} from './visitors';
|
||||
|
||||
/**
|
||||
@@ -44,17 +42,15 @@ import {eachReactiveValueOperand} from './visitors';
|
||||
export function memoizeFbtAndMacroOperandsInSameScope(
|
||||
fn: HIRFunction,
|
||||
): Set<IdentifierId> {
|
||||
const fbtMacroTags = new Set<Macro>([
|
||||
...Array.from(FBT_TAGS).map((tag): Macro => [tag, []]),
|
||||
const fbtMacroTags = new Set([
|
||||
...FBT_TAGS,
|
||||
...(fn.env.config.customMacros ?? []),
|
||||
]);
|
||||
const fbtValues: Set<IdentifierId> = new Set();
|
||||
const macroMethods = new Map<IdentifierId, Array<Array<MacroMethod>>>();
|
||||
while (true) {
|
||||
let vsize = fbtValues.size;
|
||||
let msize = macroMethods.size;
|
||||
visit(fn, fbtMacroTags, fbtValues, macroMethods);
|
||||
if (vsize === fbtValues.size && msize === macroMethods.size) {
|
||||
let size = fbtValues.size;
|
||||
visit(fn, fbtMacroTags, fbtValues);
|
||||
if (size === fbtValues.size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -74,9 +70,8 @@ export const SINGLE_CHILD_FBT_TAGS: Set<string> = new Set([
|
||||
|
||||
function visit(
|
||||
fn: HIRFunction,
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtMacroTags: Set<string>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
macroMethods: Map<IdentifierId, Array<Array<MacroMethod>>>,
|
||||
): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instruction of block.instructions) {
|
||||
@@ -87,7 +82,7 @@ function visit(
|
||||
if (
|
||||
value.kind === 'Primitive' &&
|
||||
typeof value.value === 'string' &&
|
||||
matchesExactTag(value.value, fbtMacroTags)
|
||||
fbtMacroTags.has(value.value)
|
||||
) {
|
||||
/*
|
||||
* We don't distinguish between tag names and strings, so record
|
||||
@@ -96,38 +91,10 @@ function visit(
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchesExactTag(value.binding.name, fbtMacroTags)
|
||||
fbtMacroTags.has(value.binding.name)
|
||||
) {
|
||||
// Record references to `fbt` as a global
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
} else if (
|
||||
value.kind === 'LoadGlobal' &&
|
||||
matchTagRoot(value.binding.name, fbtMacroTags) !== null
|
||||
) {
|
||||
const methods = matchTagRoot(value.binding.name, fbtMacroTags)!;
|
||||
macroMethods.set(lvalue.identifier.id, methods);
|
||||
} else if (
|
||||
value.kind === 'PropertyLoad' &&
|
||||
macroMethods.has(value.object.identifier.id)
|
||||
) {
|
||||
const methods = macroMethods.get(value.object.identifier.id)!;
|
||||
const newMethods = [];
|
||||
for (const method of methods) {
|
||||
if (
|
||||
method.length > 0 &&
|
||||
(method[0].type === 'wildcard' ||
|
||||
(method[0].type === 'name' && method[0].name === value.property))
|
||||
) {
|
||||
if (method.length > 1) {
|
||||
newMethods.push(method.slice(1));
|
||||
} else {
|
||||
fbtValues.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newMethods.length > 0) {
|
||||
macroMethods.set(lvalue.identifier.id, newMethods);
|
||||
}
|
||||
} else if (isFbtCallExpression(fbtValues, value)) {
|
||||
const fbtScope = lvalue.identifier.scope;
|
||||
if (fbtScope === null) {
|
||||
@@ -143,7 +110,12 @@ function visit(
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtScope.range.start = makeInstructionId(
|
||||
Math.min(
|
||||
fbtScope.range.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
fbtValues.add(operand.identifier.id);
|
||||
}
|
||||
} else if (
|
||||
@@ -164,7 +136,12 @@ function visit(
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtScope.range.start = makeInstructionId(
|
||||
Math.min(
|
||||
fbtScope.range.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
|
||||
/*
|
||||
* NOTE: we add the operands as fbt values so that they are also
|
||||
@@ -192,55 +169,29 @@ function visit(
|
||||
operand.identifier.scope = fbtScope;
|
||||
|
||||
// Expand the jsx element's range to account for its operands
|
||||
expandFbtScopeRange(fbtScope.range, operand.identifier.mutableRange);
|
||||
fbtScope.range.start = makeInstructionId(
|
||||
Math.min(
|
||||
fbtScope.range.start,
|
||||
operand.identifier.mutableRange.start,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function matchesExactTag(s: string, tags: Set<Macro>): boolean {
|
||||
return Array.from(tags).some(macro =>
|
||||
typeof macro === 'string'
|
||||
? s === macro
|
||||
: macro[1].length === 0 && macro[0] === s,
|
||||
);
|
||||
}
|
||||
|
||||
function matchTagRoot(
|
||||
s: string,
|
||||
tags: Set<Macro>,
|
||||
): Array<Array<MacroMethod>> | null {
|
||||
const methods: Array<Array<MacroMethod>> = [];
|
||||
for (const macro of tags) {
|
||||
if (typeof macro === 'string') {
|
||||
continue;
|
||||
}
|
||||
const [tag, rest] = macro;
|
||||
if (tag === s && rest.length > 0) {
|
||||
methods.push(rest);
|
||||
}
|
||||
}
|
||||
if (methods.length > 0) {
|
||||
return methods;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isFbtCallExpression(
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
return (
|
||||
(value.kind === 'CallExpression' &&
|
||||
fbtValues.has(value.callee.identifier.id)) ||
|
||||
(value.kind === 'MethodCall' && fbtValues.has(value.property.identifier.id))
|
||||
value.kind === 'CallExpression' && fbtValues.has(value.callee.identifier.id)
|
||||
);
|
||||
}
|
||||
|
||||
function isFbtJsxExpression(
|
||||
fbtMacroTags: Set<Macro>,
|
||||
fbtMacroTags: Set<string>,
|
||||
fbtValues: Set<IdentifierId>,
|
||||
value: ReactiveValue,
|
||||
): boolean {
|
||||
@@ -248,8 +199,7 @@ function isFbtJsxExpression(
|
||||
value.kind === 'JsxExpression' &&
|
||||
((value.tag.kind === 'Identifier' &&
|
||||
fbtValues.has(value.tag.identifier.id)) ||
|
||||
(value.tag.kind === 'BuiltinTag' &&
|
||||
matchesExactTag(value.tag.name, fbtMacroTags)))
|
||||
(value.tag.kind === 'BuiltinTag' && fbtMacroTags.has(value.tag.name)))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,14 +214,3 @@ function isFbtJsxChild(
|
||||
fbtValues.has(lvalue.identifier.id)
|
||||
);
|
||||
}
|
||||
|
||||
function expandFbtScopeRange(
|
||||
fbtRange: MutableRange,
|
||||
extendWith: MutableRange,
|
||||
): void {
|
||||
if (extendWith.start !== 0) {
|
||||
fbtRange.start = makeInstructionId(
|
||||
Math.min(fbtRange.start, extendWith.start),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* 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 {
|
||||
InstructionId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
ReactiveBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveScope,
|
||||
ScopeId,
|
||||
} from '../HIR';
|
||||
import DisjointSet from '../Utils/DisjointSet';
|
||||
import {retainWhere} from '../Utils/utils';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
|
||||
/*
|
||||
* Note: this is the 3rd of 4 passes that determine how to break a function into discrete
|
||||
* reactive scopes (independently memoizeable units of code):
|
||||
* 1. InferReactiveScopeVariables (on HIR) determines operands that mutate together and assigns
|
||||
* them a unique reactive scope.
|
||||
* 2. AlignReactiveScopesToBlockScopes (on ReactiveFunction) aligns reactive scopes
|
||||
* to block scopes.
|
||||
* 3. MergeOverlappingReactiveScopes (this pass, on ReactiveFunction) ensures that reactive
|
||||
* scopes do not overlap, merging any such scopes.
|
||||
* 4. BuildReactiveBlocks (on ReactiveFunction) groups the statements for each scope into
|
||||
* a ReactiveScopeBlock.
|
||||
*
|
||||
* Previous passes may leave "overlapping" scopes, ie where one or more instructions are within
|
||||
* the mutable range of multiple reactive scopes. We prefer to avoid executing instructions twice
|
||||
* for performance reasons (side effects are less of a concern bc components are required to be
|
||||
* idempotent), so we cannot simply repeat the instruction once for each scope. Instead, the only
|
||||
* option is to combine the two scopes into one. This is an area where an eventual Forget IDE
|
||||
* could provide real-time feedback to the developer that two computations are accidentally merged.
|
||||
*
|
||||
* ## Detailed Walkthrough
|
||||
*
|
||||
* Two scopes overlap if there is one or more instruction that is inside the range
|
||||
* of both scopes. In general, overlapping scopes are merged togther. The only
|
||||
* exception to this is when one scope *shadows* another scope. For example:
|
||||
*
|
||||
* ```javascript
|
||||
* function foo(cond, a) {
|
||||
* ⌵ scope for x
|
||||
* let x = []; ⌝
|
||||
* if (cond) { ⎮
|
||||
* ⌵ scope for y ⎮
|
||||
* let y = []; ⌝ ⎮
|
||||
* if (b) { ⎮ ⎮
|
||||
* y.push(b); ⌟ ⎮
|
||||
* } ⎮
|
||||
* x.push(<div>{y}</div>); ⎮
|
||||
* } ⌟
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In this example the two scopes overlap, but mutation of the two scopes is not
|
||||
* interleaved. Specifically within the y scope there are no instructions that
|
||||
* modify any other scope: the inner scope "shadows" the outer one. This category
|
||||
* of overlap does *NOT* merge the scopes together.
|
||||
*
|
||||
* The implementation is inspired by the Rust notion of "stacked borrows". We traverse
|
||||
* the control-flow graph in tree form, at each point keeping track of which scopes are
|
||||
* active. So initially we see
|
||||
*
|
||||
* `let x = []`
|
||||
* active scopes: [x]
|
||||
*
|
||||
* and mark the x scope as active.
|
||||
*
|
||||
* Then we later encounter
|
||||
*
|
||||
* `let y = [];`
|
||||
* active scopes: [x, y]
|
||||
*
|
||||
* Here we first check to see if 'y' is already in the list of active scopes. It isn't,
|
||||
* so we push it to the stop of the stack.
|
||||
*
|
||||
* Then
|
||||
*
|
||||
* `y.push(b)`
|
||||
* active scopes: [x, y]
|
||||
*
|
||||
* Mutates y, so we check if y is the top of the stack. It is, so no merging must occur.
|
||||
*
|
||||
* If instead we saw eg
|
||||
*
|
||||
* `x.push(b)`
|
||||
* active scopes: [x, y]
|
||||
*
|
||||
* Then we would see that 'x' is active, but that it is shadowed. The two scopes would have
|
||||
* to be merged.
|
||||
*/
|
||||
export function mergeOverlappingReactiveScopes(fn: ReactiveFunction): void {
|
||||
const context = new Context();
|
||||
visitReactiveFunction(fn, new Visitor(), context);
|
||||
context.complete();
|
||||
}
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<Context> {
|
||||
override visitID(id: InstructionId, state: Context): void {
|
||||
state.visitId(id);
|
||||
}
|
||||
override visitPlace(id: InstructionId, place: Place, state: Context): void {
|
||||
state.visitPlace(id, place);
|
||||
}
|
||||
override visitLValue(id: InstructionId, lvalue: Place, state: Context): void {
|
||||
state.visitPlace(id, lvalue);
|
||||
}
|
||||
override visitBlock(block: ReactiveBlock, state: Context): void {
|
||||
state.enter(() => {
|
||||
this.traverseBlock(block, state);
|
||||
});
|
||||
}
|
||||
override visitInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: Context,
|
||||
): void {
|
||||
if (
|
||||
instruction.value.kind === 'ConditionalExpression' ||
|
||||
instruction.value.kind === 'LogicalExpression' ||
|
||||
instruction.value.kind === 'OptionalExpression'
|
||||
) {
|
||||
state.enter(() => {
|
||||
super.visitInstruction(instruction, state);
|
||||
});
|
||||
} else {
|
||||
super.visitInstruction(instruction, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BlockScope {
|
||||
seen: Set<ScopeId> = new Set();
|
||||
scopes: Array<ShadowableReactiveScope> = [];
|
||||
}
|
||||
|
||||
type ShadowableReactiveScope = {
|
||||
scope: ReactiveScope;
|
||||
shadowedBy: ReactiveScope | null;
|
||||
};
|
||||
|
||||
class Context {
|
||||
scopes: Array<BlockScope> = [];
|
||||
seenScopes: Set<ScopeId> = new Set();
|
||||
joinedScopes: DisjointSet<ReactiveScope> = new DisjointSet();
|
||||
operandScopes: Map<Place, ReactiveScope> = new Map();
|
||||
|
||||
visitId(id: InstructionId): void {
|
||||
const currentBlock = this.scopes[this.scopes.length - 1]!;
|
||||
retainWhere(currentBlock.scopes, pending => {
|
||||
if (pending.scope.range.end > id) {
|
||||
return true;
|
||||
} else {
|
||||
currentBlock.seen.delete(pending.scope.id);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
visitPlace(id: InstructionId, place: Place): void {
|
||||
const scope = getPlaceScope(id, place);
|
||||
if (scope === null) {
|
||||
return;
|
||||
}
|
||||
this.operandScopes.set(place, scope);
|
||||
const currentBlock = this.scopes[this.scopes.length - 1]!;
|
||||
// Fast-path for the first time we see a new scope
|
||||
if (!this.seenScopes.has(scope.id)) {
|
||||
this.seenScopes.add(scope.id);
|
||||
currentBlock.seen.add(scope.id);
|
||||
currentBlock.scopes.push({shadowedBy: null, scope});
|
||||
return;
|
||||
}
|
||||
// Scope has already been seen, find it in the current block or a parent
|
||||
let index = this.scopes.length - 1;
|
||||
let nextBlock = currentBlock;
|
||||
while (!nextBlock.seen.has(scope.id)) {
|
||||
/*
|
||||
* scopes that cross control-flow boundaries are merged with overlapping
|
||||
* scopes
|
||||
*/
|
||||
this.joinedScopes.union([scope, ...nextBlock.scopes.map(s => s.scope)]);
|
||||
index--;
|
||||
if (index < 0) {
|
||||
/*
|
||||
* TODO: handle reassignments in multiple branches. these create new identifiers that
|
||||
* add an entry to this.seenScopes but which are then removed when their blocks exit.
|
||||
* this is also wrong for codegen, different versions of an identifier could be cached
|
||||
* differently and so a reassigned version of a variable needs a separate declaration.
|
||||
* console.log(`scope ${scope.id} not found`);
|
||||
*/
|
||||
|
||||
/*
|
||||
* for (let i = this.scopes.length - 1; i > index; i--) {
|
||||
* const s = this.scopes[i];
|
||||
* console.log(
|
||||
* JSON.stringify(
|
||||
* {
|
||||
* seen: Array.from(s.seen),
|
||||
* scopes: s.scopes,
|
||||
* },
|
||||
* null,
|
||||
* 2
|
||||
* )
|
||||
* );
|
||||
* }
|
||||
*/
|
||||
currentBlock.seen.add(scope.id);
|
||||
currentBlock.scopes.push({shadowedBy: null, scope});
|
||||
return;
|
||||
}
|
||||
nextBlock = this.scopes[index]!;
|
||||
}
|
||||
|
||||
// Handle interleaving within a given block scope
|
||||
let found = false;
|
||||
for (let i = 0; i < nextBlock.scopes.length; i++) {
|
||||
const current = nextBlock.scopes[i]!;
|
||||
if (current.scope.id === scope.id) {
|
||||
found = true;
|
||||
if (current.shadowedBy !== null) {
|
||||
this.joinedScopes.union([current.shadowedBy, current.scope]);
|
||||
}
|
||||
} else if (found && current.shadowedBy === null) {
|
||||
// `scope` is shadowing `current` and may interleave
|
||||
current.shadowedBy = scope;
|
||||
if (current.scope.range.end > scope.range.end) {
|
||||
/*
|
||||
* Current is shadowed by `scope`, and we know that `current` will mutate
|
||||
* again (per its range), so the scopes are already known to interleave.
|
||||
*
|
||||
* Eagerly extend the ranges of the scopes so that we don't prematurely end
|
||||
* a scope relative to its eventual post-merge mutable range
|
||||
*/
|
||||
const end = makeInstructionId(
|
||||
Math.max(current.scope.range.end, scope.range.end),
|
||||
);
|
||||
current.scope.range.end = end;
|
||||
scope.range.end = end;
|
||||
this.joinedScopes.union([current.scope, scope]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!currentBlock.seen.has(scope.id)) {
|
||||
currentBlock.seen.add(scope.id);
|
||||
currentBlock.scopes.push({shadowedBy: null, scope});
|
||||
}
|
||||
}
|
||||
|
||||
enter(fn: () => void): void {
|
||||
this.scopes.push(new BlockScope());
|
||||
fn();
|
||||
this.scopes.pop();
|
||||
}
|
||||
|
||||
complete(): void {
|
||||
this.joinedScopes.forEach((scope, groupScope) => {
|
||||
if (scope !== groupScope) {
|
||||
groupScope.range.start = makeInstructionId(
|
||||
Math.min(groupScope.range.start, scope.range.start),
|
||||
);
|
||||
groupScope.range.end = makeInstructionId(
|
||||
Math.max(groupScope.range.end, scope.range.end),
|
||||
);
|
||||
}
|
||||
});
|
||||
for (const [operand, originalScope] of this.operandScopes) {
|
||||
const mergedScope = this.joinedScopes.find(originalScope);
|
||||
if (mergedScope !== null) {
|
||||
operand.identifier.scope = mergedScope;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import {CompilerError} from '..';
|
||||
import {
|
||||
DeclarationId,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
Place,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ReactiveScopeDependency,
|
||||
ReactiveStatement,
|
||||
Type,
|
||||
areEqualPaths,
|
||||
makeInstructionId,
|
||||
} from '../HIR';
|
||||
import {
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
BuiltInObjectId,
|
||||
} from '../HIR/ObjectShape';
|
||||
import {eachInstructionLValue} from '../HIR/visitors';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {printReactiveScopeSummary} from './PrintReactiveFunction';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
@@ -98,29 +97,22 @@ function log(msg: string): void {
|
||||
}
|
||||
|
||||
class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
|
||||
/*
|
||||
* TODO LeaveSSA: use IdentifierId for more precise tracking
|
||||
* Using DeclarationId is necessary for compatible output but produces suboptimal results
|
||||
* in cases where a scope defines a variable, but that version is never read and always
|
||||
* overwritten later.
|
||||
* see reassignment-separate-scopes.js for example
|
||||
*/
|
||||
lastUsage: Map<DeclarationId, InstructionId> = new Map();
|
||||
lastUsage: Map<IdentifierId, InstructionId> = new Map();
|
||||
|
||||
override visitPlace(id: InstructionId, place: Place, _state: void): void {
|
||||
const previousUsage = this.lastUsage.get(place.identifier.declarationId);
|
||||
const previousUsage = this.lastUsage.get(place.identifier.id);
|
||||
const lastUsage =
|
||||
previousUsage !== undefined
|
||||
? makeInstructionId(Math.max(previousUsage, id))
|
||||
: id;
|
||||
this.lastUsage.set(place.identifier.declarationId, lastUsage);
|
||||
this.lastUsage.set(place.identifier.id, lastUsage);
|
||||
}
|
||||
}
|
||||
|
||||
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
|
||||
lastUsage: Map<DeclarationId, InstructionId>;
|
||||
lastUsage: Map<IdentifierId, InstructionId>;
|
||||
|
||||
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
|
||||
constructor(lastUsage: Map<IdentifierId, InstructionId>) {
|
||||
super();
|
||||
this.lastUsage = lastUsage;
|
||||
}
|
||||
@@ -152,7 +144,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
block: ReactiveScopeBlock;
|
||||
from: number;
|
||||
to: number;
|
||||
lvalues: Set<DeclarationId>;
|
||||
lvalues: Set<IdentifierId>;
|
||||
};
|
||||
let current: MergedScope | null = null;
|
||||
const merged: Array<MergedScope> = [];
|
||||
@@ -212,9 +204,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
* subsequent code wo expanding the set of declarations, which we want to avoid
|
||||
*/
|
||||
if (current !== null && instr.instruction.lvalue !== null) {
|
||||
current.lvalues.add(
|
||||
instr.instruction.lvalue.identifier.declarationId,
|
||||
);
|
||||
current.lvalues.add(instr.instruction.lvalue.identifier.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -234,7 +224,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
for (const lvalue of eachInstructionLValue(
|
||||
instr.instruction,
|
||||
)) {
|
||||
current.lvalues.add(lvalue.identifier.declarationId);
|
||||
current.lvalues.add(lvalue.identifier.id);
|
||||
}
|
||||
} else {
|
||||
log(
|
||||
@@ -393,12 +383,12 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
|
||||
*/
|
||||
function updateScopeDeclarations(
|
||||
scope: ReactiveScope,
|
||||
lastUsage: Map<DeclarationId, InstructionId>,
|
||||
lastUsage: Map<IdentifierId, InstructionId>,
|
||||
): void {
|
||||
for (const [id, decl] of scope.declarations) {
|
||||
const lastUsedAt = lastUsage.get(decl.identifier.declarationId)!;
|
||||
for (const [key] of scope.declarations) {
|
||||
const lastUsedAt = lastUsage.get(key)!;
|
||||
if (lastUsedAt < scope.range.end) {
|
||||
scope.declarations.delete(id);
|
||||
scope.declarations.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,8 +400,8 @@ function updateScopeDeclarations(
|
||||
*/
|
||||
function areLValuesLastUsedByScope(
|
||||
scope: ReactiveScope,
|
||||
lvalues: Set<DeclarationId>,
|
||||
lastUsage: Map<DeclarationId, InstructionId>,
|
||||
lvalues: Set<IdentifierId>,
|
||||
lastUsage: Map<IdentifierId, InstructionId>,
|
||||
): boolean {
|
||||
for (const lvalue of lvalues) {
|
||||
const lastUsedAt = lastUsage.get(lvalue)!;
|
||||
@@ -464,12 +454,8 @@ function canMergeScopes(
|
||||
(next.scope.dependencies.size !== 0 &&
|
||||
[...next.scope.dependencies].every(
|
||||
dep =>
|
||||
isAlwaysInvalidatingType(dep.identifier.type) &&
|
||||
Iterable_some(
|
||||
current.scope.declarations.values(),
|
||||
decl =>
|
||||
decl.identifier.declarationId === dep.identifier.declarationId,
|
||||
),
|
||||
current.scope.declarations.has(dep.identifier.id) &&
|
||||
isAlwaysInvalidatingType(dep.identifier.type),
|
||||
))
|
||||
) {
|
||||
log(` outputs of prev are input to current`);
|
||||
@@ -482,20 +468,14 @@ function canMergeScopes(
|
||||
}
|
||||
|
||||
function isAlwaysInvalidatingType(type: Type): boolean {
|
||||
switch (type.kind) {
|
||||
case 'Object': {
|
||||
switch (type.shapeId) {
|
||||
case BuiltInArrayId:
|
||||
case BuiltInObjectId:
|
||||
case BuiltInFunctionId:
|
||||
case BuiltInJsxId: {
|
||||
return true;
|
||||
}
|
||||
if (type.kind === 'Object') {
|
||||
switch (type.shapeId) {
|
||||
case BuiltInArrayId:
|
||||
case BuiltInObjectId:
|
||||
case BuiltInFunctionId:
|
||||
case BuiltInJsxId: {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Function': {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -512,7 +492,7 @@ function areEqualDependencies(
|
||||
let found = false;
|
||||
for (const bValue of b) {
|
||||
if (
|
||||
aValue.identifier.declarationId === bValue.identifier.declarationId &&
|
||||
aValue.identifier === bValue.identifier &&
|
||||
areEqualPaths(aValue.path, bValue.path)
|
||||
) {
|
||||
found = true;
|
||||
@@ -526,6 +506,10 @@ function areEqualDependencies(
|
||||
return true;
|
||||
}
|
||||
|
||||
function areEqualPaths(a: Array<string>, b: Array<string>): boolean {
|
||||
return a.length === b.length && a.every((item, ix) => item === b[ix]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this scope eligible for merging with subsequent scopes? In general this
|
||||
* is only true if the scope's output values are guaranteed to change when its
|
||||
|
||||
@@ -113,7 +113,7 @@ export function printDependency(dependency: ReactiveScopeDependency): string {
|
||||
const identifier =
|
||||
printIdentifier(dependency.identifier) +
|
||||
printType(dependency.identifier.type);
|
||||
return `${identifier}${dependency.path.map(token => `${token.optional ? '?.' : '.'}${token.property}`).join('')}`;
|
||||
return `${identifier}${dependency.path.map(prop => `.${prop}`).join('')}`;
|
||||
}
|
||||
|
||||
export function printReactiveInstructions(
|
||||
|
||||
@@ -8,30 +8,23 @@
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {GeneratedSource} from '../HIR';
|
||||
import {
|
||||
DeclarationId,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
Place,
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveScope,
|
||||
ReactiveInstruction,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveValue,
|
||||
ScopeId,
|
||||
SpreadPattern,
|
||||
promoteTemporary,
|
||||
promoteTemporaryJsxTag,
|
||||
IdentifierId,
|
||||
} from '../HIR/HIR';
|
||||
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* Phase 2: Promote identifiers which are used in a place that requires a named variable.
|
||||
*/
|
||||
class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
class Visitor extends ReactiveFunctionVisitor<State> {
|
||||
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
|
||||
this.traverseScope(scopeBlock, state);
|
||||
for (const dep of scopeBlock.scope.dependencies) {
|
||||
const {identifier} = dep;
|
||||
if (identifier.name == null) {
|
||||
@@ -50,23 +43,21 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
promoteIdentifier(declaration.identifier, state);
|
||||
}
|
||||
}
|
||||
this.traverseScope(scopeBlock, state);
|
||||
}
|
||||
|
||||
override visitPrunedScope(
|
||||
scopeBlock: PrunedReactiveScopeBlock,
|
||||
state: State,
|
||||
): void {
|
||||
this.traversePrunedScope(scopeBlock, state);
|
||||
for (const [, declaration] of scopeBlock.scope.declarations) {
|
||||
if (
|
||||
declaration.identifier.name == null &&
|
||||
state.pruned.get(declaration.identifier.declarationId)
|
||||
?.usedOutsideScope === true
|
||||
state.pruned.get(declaration.identifier.id)?.usedOutsideScope === true
|
||||
) {
|
||||
promoteIdentifier(declaration.identifier, state);
|
||||
}
|
||||
}
|
||||
this.traversePrunedScope(scopeBlock, state);
|
||||
}
|
||||
|
||||
override visitParam(place: Place, state: State): void {
|
||||
@@ -102,96 +93,24 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3: Now that identifiers which need promotion are promoted, find and promote
|
||||
* all other Identifier instances of each promoted DeclarationId.
|
||||
*/
|
||||
class PromoteAllInstancedOfPromotedTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
override visitPlace(_id: InstructionId, place: Place, state: State): void {
|
||||
if (
|
||||
place.identifier.name === null &&
|
||||
state.promoted.has(place.identifier.declarationId)
|
||||
) {
|
||||
promoteIdentifier(place.identifier, state);
|
||||
}
|
||||
}
|
||||
override visitLValue(
|
||||
_id: InstructionId,
|
||||
_lvalue: Place,
|
||||
_state: State,
|
||||
): void {
|
||||
this.visitPlace(_id, _lvalue, _state);
|
||||
}
|
||||
traverseScopeIdentifiers(scope: ReactiveScope, state: State): void {
|
||||
for (const [, decl] of scope.declarations) {
|
||||
if (
|
||||
decl.identifier.name === null &&
|
||||
state.promoted.has(decl.identifier.declarationId)
|
||||
) {
|
||||
promoteIdentifier(decl.identifier, state);
|
||||
}
|
||||
}
|
||||
for (const dep of scope.dependencies) {
|
||||
if (
|
||||
dep.identifier.name === null &&
|
||||
state.promoted.has(dep.identifier.declarationId)
|
||||
) {
|
||||
promoteIdentifier(dep.identifier, state);
|
||||
}
|
||||
}
|
||||
for (const reassignment of scope.reassignments) {
|
||||
if (
|
||||
reassignment.name === null &&
|
||||
state.promoted.has(reassignment.declarationId)
|
||||
) {
|
||||
promoteIdentifier(reassignment, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
override visitScope(scope: ReactiveScopeBlock, state: State): void {
|
||||
this.traverseScope(scope, state);
|
||||
this.traverseScopeIdentifiers(scope.scope, state);
|
||||
}
|
||||
override visitPrunedScope(
|
||||
scopeBlock: PrunedReactiveScopeBlock,
|
||||
state: State,
|
||||
): void {
|
||||
this.traversePrunedScope(scopeBlock, state);
|
||||
this.traverseScopeIdentifiers(scopeBlock.scope, state);
|
||||
}
|
||||
override visitReactiveFunctionValue(
|
||||
_id: InstructionId,
|
||||
_dependencies: Array<Place>,
|
||||
fn: ReactiveFunction,
|
||||
state: State,
|
||||
): void {
|
||||
visitReactiveFunction(fn, this, state);
|
||||
}
|
||||
}
|
||||
|
||||
type JsxExpressionTags = Set<DeclarationId>;
|
||||
type JsxExpressionTags = Set<IdentifierId>;
|
||||
type State = {
|
||||
tags: JsxExpressionTags;
|
||||
promoted: Set<DeclarationId>;
|
||||
pruned: Map<
|
||||
DeclarationId,
|
||||
IdentifierId,
|
||||
{activeScopes: Array<ScopeId>; usedOutsideScope: boolean}
|
||||
>; // true if referenced within another scope, false if only accessed outside of scopes
|
||||
};
|
||||
|
||||
/**
|
||||
* Phase 1: checks for pruned variables which need to be promoted, as well as
|
||||
* usage of identifiers as jsx tags, which need to be promoted differently
|
||||
*/
|
||||
class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
activeScopes: Array<ScopeId> = [];
|
||||
|
||||
override visitPlace(_id: InstructionId, place: Place, state: State): void {
|
||||
if (
|
||||
this.activeScopes.length !== 0 &&
|
||||
state.pruned.has(place.identifier.declarationId)
|
||||
state.pruned.has(place.identifier.id)
|
||||
) {
|
||||
const prunedPlace = state.pruned.get(place.identifier.declarationId)!;
|
||||
const prunedPlace = state.pruned.get(place.identifier.id)!;
|
||||
if (prunedPlace.activeScopes.indexOf(this.activeScopes.at(-1)!) === -1) {
|
||||
prunedPlace.usedOutsideScope = true;
|
||||
}
|
||||
@@ -205,7 +124,7 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
): void {
|
||||
this.traverseValue(id, value, state);
|
||||
if (value.kind === 'JsxExpression' && value.tag.kind === 'Identifier') {
|
||||
state.tags.add(value.tag.identifier.declarationId);
|
||||
state.tags.add(value.tag.identifier.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,8 +132,8 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
scopeBlock: PrunedReactiveScopeBlock,
|
||||
state: State,
|
||||
): void {
|
||||
for (const [_id, decl] of scopeBlock.scope.declarations) {
|
||||
state.pruned.set(decl.identifier.declarationId, {
|
||||
for (const [id] of scopeBlock.scope.declarations) {
|
||||
state.pruned.set(id, {
|
||||
activeScopes: [...this.activeScopes],
|
||||
usedOutsideScope: false,
|
||||
});
|
||||
@@ -229,203 +148,9 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
|
||||
}
|
||||
}
|
||||
|
||||
type InterState = Map<IdentifierId, [Identifier, boolean]>;
|
||||
class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
|
||||
#promotable: State;
|
||||
#consts: Set<IdentifierId> = new Set();
|
||||
#globals: Set<IdentifierId> = new Set();
|
||||
|
||||
/*
|
||||
* Unpromoted temporaries will be emitted at their use sites rather than as separate
|
||||
* declarations. However, this causes errors if an interposing temporary has been
|
||||
* promoted, or if an interposing instruction has had its lvalues deleted, because such
|
||||
* temporaries will be emitted as separate statements, which can effectively cause
|
||||
* code to be reordered, and when that code has side effects that changes program behavior.
|
||||
* This visitor promotes temporarties that have such interposing instructions to preserve
|
||||
* source ordering.
|
||||
*/
|
||||
constructor(promotable: State, params: Array<Place | SpreadPattern>) {
|
||||
super();
|
||||
params.forEach(param => {
|
||||
switch (param.kind) {
|
||||
case 'Identifier':
|
||||
this.#consts.add(param.identifier.id);
|
||||
break;
|
||||
case 'Spread':
|
||||
this.#consts.add(param.place.identifier.id);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.#promotable = promotable;
|
||||
}
|
||||
|
||||
override visitPlace(
|
||||
_id: InstructionId,
|
||||
place: Place,
|
||||
state: InterState,
|
||||
): void {
|
||||
const promo = state.get(place.identifier.id);
|
||||
if (promo) {
|
||||
const [identifier, needsPromotion] = promo;
|
||||
if (
|
||||
needsPromotion &&
|
||||
identifier.name === null &&
|
||||
!this.#consts.has(identifier.id)
|
||||
) {
|
||||
/*
|
||||
* If the identifier hasn't been promoted but is marked as needing
|
||||
* promotion by the logic in `visitInstruction`, and we've seen a
|
||||
* use of it after said marking, promote it
|
||||
*/
|
||||
promoteIdentifier(identifier, this.#promotable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override visitInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: InterState,
|
||||
): void {
|
||||
for (const lval of eachInstructionValueLValue(instruction.value)) {
|
||||
CompilerError.invariant(lval.identifier.name != null, {
|
||||
reason:
|
||||
'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
|
||||
loc: instruction.loc,
|
||||
});
|
||||
}
|
||||
|
||||
switch (instruction.value.kind) {
|
||||
case 'CallExpression':
|
||||
case 'MethodCall':
|
||||
case 'Await':
|
||||
case 'PropertyStore':
|
||||
case 'PropertyDelete':
|
||||
case 'ComputedStore':
|
||||
case 'ComputedDelete':
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate':
|
||||
case 'StoreLocal':
|
||||
case 'StoreContext':
|
||||
case 'StoreGlobal':
|
||||
case 'Destructure': {
|
||||
let constStore = false;
|
||||
|
||||
if (
|
||||
(instruction.value.kind === 'StoreContext' ||
|
||||
instruction.value.kind === 'StoreLocal') &&
|
||||
(instruction.value.lvalue.kind === 'Const' ||
|
||||
instruction.value.lvalue.kind === 'HoistedConst')
|
||||
) {
|
||||
/*
|
||||
* If an identifier is const, we don't need to worry about it
|
||||
* being mutated between being loaded and being used
|
||||
*/
|
||||
this.#consts.add(instruction.value.lvalue.place.identifier.id);
|
||||
constStore = true;
|
||||
}
|
||||
if (
|
||||
instruction.value.kind === 'Destructure' &&
|
||||
(instruction.value.lvalue.kind === 'Const' ||
|
||||
instruction.value.lvalue.kind === 'HoistedConst')
|
||||
) {
|
||||
[...eachPatternOperand(instruction.value.lvalue.pattern)].forEach(
|
||||
ident => this.#consts.add(ident.identifier.id),
|
||||
);
|
||||
constStore = true;
|
||||
}
|
||||
if (instruction.value.kind === 'MethodCall') {
|
||||
// Treat property of method call as constlike so we don't promote it.
|
||||
this.#consts.add(instruction.value.property.identifier.id);
|
||||
}
|
||||
|
||||
super.visitInstruction(instruction, state);
|
||||
if (
|
||||
!constStore &&
|
||||
(instruction.lvalue == null ||
|
||||
instruction.lvalue.identifier.name != null)
|
||||
) {
|
||||
/*
|
||||
* If we've stripped the lvalue or promoted the lvalue, then we will emit this instruction
|
||||
* as a statement in codegen.
|
||||
*
|
||||
* If this instruction will be emitted directly as a statement rather than as a temporary
|
||||
* during codegen, then it can interpose between the defs and the uses of other temporaries.
|
||||
* Since this instruction could potentially mutate those defs, it's not safe to relocate
|
||||
* the definition of those temporaries to after this instruction. Mark all those temporaries
|
||||
* as needing promotion, but don't promote them until we actually see them being used.
|
||||
*/
|
||||
for (const [key, [ident, _]] of state.entries()) {
|
||||
state.set(key, [ident, true]);
|
||||
}
|
||||
}
|
||||
if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
|
||||
// Add this instruction's lvalue to the state, initially not marked as needing promotion
|
||||
state.set(instruction.lvalue.identifier.id, [
|
||||
instruction.lvalue.identifier,
|
||||
false,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'DeclareContext':
|
||||
case 'DeclareLocal': {
|
||||
if (
|
||||
instruction.value.lvalue.kind === 'Const' ||
|
||||
instruction.value.lvalue.kind === 'HoistedConst'
|
||||
) {
|
||||
this.#consts.add(instruction.value.lvalue.place.identifier.id);
|
||||
}
|
||||
super.visitInstruction(instruction, state);
|
||||
break;
|
||||
}
|
||||
case 'LoadContext':
|
||||
case 'LoadLocal': {
|
||||
if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
|
||||
if (this.#consts.has(instruction.value.place.identifier.id)) {
|
||||
this.#consts.add(instruction.lvalue.identifier.id);
|
||||
}
|
||||
state.set(instruction.lvalue.identifier.id, [
|
||||
instruction.lvalue.identifier,
|
||||
false,
|
||||
]);
|
||||
}
|
||||
super.visitInstruction(instruction, state);
|
||||
break;
|
||||
}
|
||||
case 'PropertyLoad':
|
||||
case 'ComputedLoad': {
|
||||
if (instruction.lvalue) {
|
||||
if (this.#globals.has(instruction.value.object.identifier.id)) {
|
||||
this.#globals.add(instruction.lvalue.identifier.id);
|
||||
this.#consts.add(instruction.lvalue.identifier.id);
|
||||
}
|
||||
if (instruction.lvalue.identifier.name === null) {
|
||||
state.set(instruction.lvalue.identifier.id, [
|
||||
instruction.lvalue.identifier,
|
||||
false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
super.visitInstruction(instruction, state);
|
||||
break;
|
||||
}
|
||||
case 'LoadGlobal': {
|
||||
instruction.lvalue &&
|
||||
this.#globals.add(instruction.lvalue.identifier.id);
|
||||
super.visitInstruction(instruction, state);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
super.visitInstruction(instruction, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function promoteUsedTemporaries(fn: ReactiveFunction): void {
|
||||
const state: State = {
|
||||
tags: new Set(),
|
||||
promoted: new Set(),
|
||||
pruned: new Map(),
|
||||
};
|
||||
visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
|
||||
@@ -435,18 +160,7 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
|
||||
promoteIdentifier(place.identifier, state);
|
||||
}
|
||||
}
|
||||
visitReactiveFunction(fn, new PromoteTemporaries(), state);
|
||||
|
||||
visitReactiveFunction(
|
||||
fn,
|
||||
new PromoteInterposedTemporaries(state, fn.params),
|
||||
new Map(),
|
||||
);
|
||||
visitReactiveFunction(
|
||||
fn,
|
||||
new PromoteAllInstancedOfPromotedTemporaries(),
|
||||
state,
|
||||
);
|
||||
visitReactiveFunction(fn, new Visitor(), state);
|
||||
}
|
||||
|
||||
function promoteIdentifier(identifier: Identifier, state: State): void {
|
||||
@@ -457,10 +171,9 @@ function promoteIdentifier(identifier: Identifier, state: State): void {
|
||||
loc: GeneratedSource,
|
||||
suggestions: null,
|
||||
});
|
||||
if (state.tags.has(identifier.declarationId)) {
|
||||
if (state.tags.has(identifier.id)) {
|
||||
promoteTemporaryJsxTag(identifier);
|
||||
} else {
|
||||
promoteTemporary(identifier);
|
||||
}
|
||||
state.promoted.add(identifier.declarationId);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {Environment} from '../HIR';
|
||||
import {
|
||||
areEqualPaths,
|
||||
BlockId,
|
||||
DeclarationId,
|
||||
GeneratedSource,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
InstructionKind,
|
||||
isObjectMethodType,
|
||||
@@ -23,7 +21,6 @@ import {
|
||||
PrunedReactiveScopeBlock,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
ReactiveOptionalCallValue,
|
||||
ReactiveScope,
|
||||
ReactiveScopeBlock,
|
||||
ReactiveScopeDependency,
|
||||
@@ -33,7 +30,7 @@ import {
|
||||
} from '../HIR/HIR';
|
||||
import {eachInstructionValueOperand, eachPatternOperand} from '../HIR/visitors';
|
||||
import {empty, Stack} from '../Utils/Stack';
|
||||
import {assertExhaustive, Iterable_some} from '../Utils/utils';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
ReactiveScopeDependencyTree,
|
||||
ReactiveScopePropertyDependency,
|
||||
@@ -67,7 +64,11 @@ export function propagateScopeDependencies(fn: ReactiveFunction): void {
|
||||
});
|
||||
}
|
||||
}
|
||||
visitReactiveFunction(fn, new PropagationVisitor(fn.env), context);
|
||||
visitReactiveFunction(
|
||||
fn,
|
||||
new PropagationVisitor(fn.env.config.enableTreatFunctionDepsAsConditional),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
type TemporariesUsedOutsideDefiningScope = {
|
||||
@@ -75,9 +76,9 @@ type TemporariesUsedOutsideDefiningScope = {
|
||||
* tracks all relevant temporary declarations (currently LoadLocal and PropertyLoad)
|
||||
* and the scope where they are defined
|
||||
*/
|
||||
declarations: Map<DeclarationId, ScopeId>;
|
||||
declarations: Map<IdentifierId, ScopeId>;
|
||||
// temporaries used outside of their defining scope
|
||||
usedOutsideDeclaringScope: Set<DeclarationId>;
|
||||
usedOutsideDeclaringScope: Set<IdentifierId>;
|
||||
};
|
||||
class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOutsideDefiningScope> {
|
||||
scopes: Array<ScopeId> = [];
|
||||
@@ -106,10 +107,7 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOut
|
||||
case 'LoadLocal':
|
||||
case 'LoadContext':
|
||||
case 'PropertyLoad': {
|
||||
state.declarations.set(
|
||||
instruction.lvalue.identifier.declarationId,
|
||||
scope,
|
||||
);
|
||||
state.declarations.set(instruction.lvalue.identifier.id, scope);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -123,20 +121,18 @@ class FindPromotedTemporaries extends ReactiveFunctionVisitor<TemporariesUsedOut
|
||||
place: Place,
|
||||
state: TemporariesUsedOutsideDefiningScope,
|
||||
): void {
|
||||
const declaringScope = state.declarations.get(
|
||||
place.identifier.declarationId,
|
||||
);
|
||||
const declaringScope = state.declarations.get(place.identifier.id);
|
||||
if (declaringScope === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this.scopes.indexOf(declaringScope) === -1) {
|
||||
// Declaring scope is not active === used outside declaring scope
|
||||
state.usedOutsideDeclaringScope.add(place.identifier.declarationId);
|
||||
state.usedOutsideDeclaringScope.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DeclMap = Map<DeclarationId, Decl>;
|
||||
type DeclMap = Map<IdentifierId, Decl>;
|
||||
type Decl = {
|
||||
id: InstructionId;
|
||||
scope: Stack<ScopeTraversalState>;
|
||||
@@ -284,7 +280,7 @@ class PoisonState {
|
||||
}
|
||||
|
||||
class Context {
|
||||
#temporariesUsedOutsideScope: Set<DeclarationId>;
|
||||
#temporariesUsedOutsideScope: Set<IdentifierId>;
|
||||
#declarations: DeclMap = new Map();
|
||||
#reassignments: Map<Identifier, Decl> = new Map();
|
||||
// Reactive dependencies used in the current reactive scope.
|
||||
@@ -311,7 +307,7 @@ class Context {
|
||||
#scopes: Stack<ScopeTraversalState> = empty();
|
||||
poisonState: PoisonState = new PoisonState(new Set(), new Set(), false);
|
||||
|
||||
constructor(temporariesUsedOutsideScope: Set<DeclarationId>) {
|
||||
constructor(temporariesUsedOutsideScope: Set<IdentifierId>) {
|
||||
this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope;
|
||||
}
|
||||
|
||||
@@ -381,9 +377,7 @@ class Context {
|
||||
}
|
||||
|
||||
isUsedOutsideDeclaringScope(place: Place): boolean {
|
||||
return this.#temporariesUsedOutsideScope.has(
|
||||
place.identifier.declarationId,
|
||||
);
|
||||
return this.#temporariesUsedOutsideScope.has(place.identifier.id);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -446,8 +440,8 @@ class Context {
|
||||
* on itself.
|
||||
*/
|
||||
declare(identifier: Identifier, decl: Decl): void {
|
||||
if (!this.#declarations.has(identifier.declarationId)) {
|
||||
this.#declarations.set(identifier.declarationId, decl);
|
||||
if (!this.#declarations.has(identifier.id)) {
|
||||
this.#declarations.set(identifier.id, decl);
|
||||
}
|
||||
this.#reassignments.set(identifier, decl);
|
||||
}
|
||||
@@ -463,7 +457,7 @@ class Context {
|
||||
#getProperty(
|
||||
object: Place,
|
||||
property: string,
|
||||
optional: boolean,
|
||||
isConditional: boolean,
|
||||
): ReactiveScopePropertyDependency {
|
||||
const resolvedObject = this.resolveTemporary(object);
|
||||
const resolvedDependency = this.#properties.get(resolvedObject.identifier);
|
||||
@@ -476,26 +470,36 @@ class Context {
|
||||
objectDependency = {
|
||||
identifier: resolvedObject.identifier,
|
||||
path: [],
|
||||
optionalPath: [],
|
||||
};
|
||||
} else {
|
||||
objectDependency = {
|
||||
identifier: resolvedDependency.identifier,
|
||||
path: [...resolvedDependency.path],
|
||||
optionalPath: [...resolvedDependency.optionalPath],
|
||||
};
|
||||
}
|
||||
|
||||
objectDependency.path.push({property, optional});
|
||||
// (2) Determine whether property is an optional access
|
||||
if (objectDependency.optionalPath.length > 0) {
|
||||
/*
|
||||
* If the base property dependency represents a optional member expression,
|
||||
* property is on the optionalPath (regardless of whether this PropertyLoad
|
||||
* itself was conditional)
|
||||
* e.g. for `a.b?.c.d`, `d` should be added to optionalPath
|
||||
*/
|
||||
objectDependency.optionalPath.push(property);
|
||||
} else if (isConditional) {
|
||||
objectDependency.optionalPath.push(property);
|
||||
} else {
|
||||
objectDependency.path.push(property);
|
||||
}
|
||||
|
||||
return objectDependency;
|
||||
}
|
||||
|
||||
declareProperty(
|
||||
lvalue: Place,
|
||||
object: Place,
|
||||
property: string,
|
||||
optional: boolean,
|
||||
): void {
|
||||
const nextDependency = this.#getProperty(object, property, optional);
|
||||
declareProperty(lvalue: Place, object: Place, property: string): void {
|
||||
const nextDependency = this.#getProperty(object, property, false);
|
||||
this.#properties.set(lvalue.identifier, nextDependency);
|
||||
}
|
||||
|
||||
@@ -504,7 +508,7 @@ class Context {
|
||||
// ref.current access is not a valid dep
|
||||
if (
|
||||
isUseRefType(maybeDependency.identifier) &&
|
||||
maybeDependency.path.at(0)?.property === 'current'
|
||||
maybeDependency.path.at(0) === 'current'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -529,7 +533,7 @@ class Context {
|
||||
*/
|
||||
const currentDeclaration =
|
||||
this.#reassignments.get(identifier) ??
|
||||
this.#declarations.get(identifier.declarationId);
|
||||
this.#declarations.get(identifier.id);
|
||||
const currentScope = this.currentScope.value?.value;
|
||||
return (
|
||||
currentScope != null &&
|
||||
@@ -565,6 +569,7 @@ class Context {
|
||||
let dependency: ReactiveScopePropertyDependency = {
|
||||
identifier: resolved.identifier,
|
||||
path: [],
|
||||
optionalPath: [],
|
||||
};
|
||||
if (resolved.identifier.name === null) {
|
||||
const propertyDependency = this.#properties.get(resolved.identifier);
|
||||
@@ -575,8 +580,8 @@ class Context {
|
||||
this.visitDependency(dependency);
|
||||
}
|
||||
|
||||
visitProperty(object: Place, property: string, optional: boolean): void {
|
||||
const nextDependency = this.#getProperty(object, property, optional);
|
||||
visitProperty(object: Place, property: string): void {
|
||||
const nextDependency = this.#getProperty(object, property, false);
|
||||
this.visitDependency(nextDependency);
|
||||
}
|
||||
|
||||
@@ -594,23 +599,14 @@ class Context {
|
||||
* (all other decls e.g. `let x;` should be initialized in BuildHIR)
|
||||
*/
|
||||
const originalDeclaration = this.#declarations.get(
|
||||
maybeDependency.identifier.declarationId,
|
||||
maybeDependency.identifier.id,
|
||||
);
|
||||
if (
|
||||
originalDeclaration !== undefined &&
|
||||
originalDeclaration.scope.value !== null
|
||||
) {
|
||||
originalDeclaration.scope.each(scope => {
|
||||
if (
|
||||
!this.#isScopeActive(scope.value) &&
|
||||
// TODO LeaveSSA: key scope.declarations by DeclarationId
|
||||
!Iterable_some(
|
||||
scope.value.declarations.values(),
|
||||
decl =>
|
||||
decl.identifier.declarationId ===
|
||||
maybeDependency.identifier.declarationId,
|
||||
)
|
||||
) {
|
||||
if (!this.#isScopeActive(scope.value)) {
|
||||
scope.value.declarations.set(maybeDependency.identifier.id, {
|
||||
identifier: maybeDependency.identifier,
|
||||
scope: originalDeclaration.scope.value!.value,
|
||||
@@ -641,14 +637,11 @@ class Context {
|
||||
const currentScope = this.currentScope.value?.value;
|
||||
if (
|
||||
currentScope != null &&
|
||||
!Iterable_some(
|
||||
currentScope.reassignments,
|
||||
identifier =>
|
||||
identifier.declarationId === place.identifier.declarationId,
|
||||
!Array.from(currentScope.reassignments).some(
|
||||
identifier => identifier.id === place.identifier.id,
|
||||
) &&
|
||||
this.#checkValidDependency({identifier: place.identifier, path: []})
|
||||
) {
|
||||
// TODO LeaveSSA: scope.reassignments should be keyed by declarationid
|
||||
currentScope.reassignments.add(place.identifier);
|
||||
}
|
||||
}
|
||||
@@ -675,48 +668,19 @@ class Context {
|
||||
}
|
||||
|
||||
class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
env: Environment;
|
||||
enableTreatFunctionDepsAsConditional = false;
|
||||
|
||||
constructor(env: Environment) {
|
||||
constructor(enableTreatFunctionDepsAsConditional: boolean) {
|
||||
super();
|
||||
this.env = env;
|
||||
this.enableTreatFunctionDepsAsConditional =
|
||||
enableTreatFunctionDepsAsConditional;
|
||||
}
|
||||
|
||||
override visitScope(scope: ReactiveScopeBlock, context: Context): void {
|
||||
const scopeDependencies = context.enter(scope.scope, () => {
|
||||
this.visitBlock(scope.instructions, context);
|
||||
});
|
||||
for (const candidateDep of scopeDependencies) {
|
||||
if (
|
||||
!Iterable_some(
|
||||
scope.scope.dependencies,
|
||||
existingDep =>
|
||||
existingDep.identifier.declarationId ===
|
||||
candidateDep.identifier.declarationId &&
|
||||
areEqualPaths(existingDep.path, candidateDep.path),
|
||||
)
|
||||
) {
|
||||
scope.scope.dependencies.add(candidateDep);
|
||||
}
|
||||
}
|
||||
/*
|
||||
* TODO LeaveSSA: fix existing bug with duplicate deps and reassignments
|
||||
* see fixture ssa-cascading-eliminated-phis, note that we cache `x`
|
||||
* twice because its both a dep and a reassignment.
|
||||
*
|
||||
* for (const reassignment of scope.scope.reassignments) {
|
||||
* if (
|
||||
* Iterable_some(
|
||||
* scope.scope.dependencies.values(),
|
||||
* dep =>
|
||||
* dep.identifier.declarationId === reassignment.declarationId &&
|
||||
* dep.path.length === 0,
|
||||
* )
|
||||
* ) {
|
||||
* scope.scope.reassignments.delete(reassignment);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
scope.scope.dependencies = scopeDependencies;
|
||||
}
|
||||
|
||||
override visitPrunedScope(
|
||||
@@ -747,288 +711,51 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
});
|
||||
}
|
||||
|
||||
extractOptionalProperty(
|
||||
context: Context,
|
||||
optionalValue: ReactiveOptionalCallValue,
|
||||
lvalue: Place,
|
||||
): {
|
||||
lvalue: Place;
|
||||
object: Place;
|
||||
property: string;
|
||||
optional: boolean;
|
||||
} | null {
|
||||
const sequence = optionalValue.value;
|
||||
CompilerError.invariant(sequence.kind === 'SequenceExpression', {
|
||||
reason: 'Expected OptionalExpression value to be a SequenceExpression',
|
||||
description: `Found a \`${sequence.kind}\``,
|
||||
loc: sequence.loc,
|
||||
});
|
||||
/**
|
||||
* Base case: inner `<variable> "?." <property>`
|
||||
*```
|
||||
* <lvalue> = OptionalExpression optional=true (`optionalValue` is here)
|
||||
* Sequence (`sequence` is here)
|
||||
* t0 = LoadLocal <variable>
|
||||
* Sequence
|
||||
* t1 = PropertyLoad t0 . <property>
|
||||
* LoadLocal t1
|
||||
* ```
|
||||
*/
|
||||
if (
|
||||
sequence.instructions.length === 1 &&
|
||||
sequence.instructions[0].lvalue !== null &&
|
||||
sequence.instructions[0].value.kind === 'LoadLocal' &&
|
||||
sequence.instructions[0].value.place.identifier.name !== null &&
|
||||
!context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) &&
|
||||
sequence.value.kind === 'SequenceExpression' &&
|
||||
sequence.value.instructions.length === 1 &&
|
||||
sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
|
||||
sequence.value.instructions[0].value.object.identifier.id ===
|
||||
sequence.instructions[0].lvalue.identifier.id &&
|
||||
sequence.value.instructions[0].lvalue !== null &&
|
||||
sequence.value.value.kind === 'LoadLocal' &&
|
||||
sequence.value.value.place.identifier.id ===
|
||||
sequence.value.instructions[0].lvalue.identifier.id
|
||||
) {
|
||||
context.declareTemporary(
|
||||
sequence.instructions[0].lvalue,
|
||||
sequence.instructions[0].value.place,
|
||||
);
|
||||
const propertyLoad = sequence.value.instructions[0].value;
|
||||
return {
|
||||
lvalue,
|
||||
object: propertyLoad.object,
|
||||
property: propertyLoad.property,
|
||||
optional: optionalValue.optional,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Base case 2: inner `<variable> "." <property1> "?." <property2>
|
||||
* ```
|
||||
* <lvalue> = OptionalExpression optional=true (`optionalValue` is here)
|
||||
* Sequence (`sequence` is here)
|
||||
* t0 = Sequence
|
||||
* t1 = LoadLocal <variable>
|
||||
* ... // see note
|
||||
* PropertyLoad t1 . <property1>
|
||||
* [46] Sequence
|
||||
* t2 = PropertyLoad t0 . <property2>
|
||||
* [46] LoadLocal t2
|
||||
* ```
|
||||
*
|
||||
* Note that it's possible to have additional inner chained non-optional
|
||||
* property loads at "...", from an expression like `a?.b.c.d.e`. We could
|
||||
* expand to support this case by relaxing the check on the inner sequence
|
||||
* length, ensuring all instructions after the first LoadLocal are PropertyLoad
|
||||
* and then iterating to ensure that the lvalue of the previous is always
|
||||
* the object of the next PropertyLoad, w the final lvalue as the object
|
||||
* of the sequence.value's object.
|
||||
*
|
||||
* But this case is likely rare in practice, usually once you're optional
|
||||
* chaining all property accesses are optional (not `a?.b.c` but `a?.b?.c`).
|
||||
* Also, HIR-based PropagateScopeDeps will handle this case so it doesn't
|
||||
* seem worth it to optimize for that edge-case here.
|
||||
*/
|
||||
if (
|
||||
sequence.instructions.length === 1 &&
|
||||
sequence.instructions[0].lvalue !== null &&
|
||||
sequence.instructions[0].value.kind === 'SequenceExpression' &&
|
||||
sequence.instructions[0].value.instructions.length === 1 &&
|
||||
sequence.instructions[0].value.instructions[0].lvalue !== null &&
|
||||
sequence.instructions[0].value.instructions[0].value.kind ===
|
||||
'LoadLocal' &&
|
||||
sequence.instructions[0].value.instructions[0].value.place.identifier
|
||||
.name !== null &&
|
||||
!context.isUsedOutsideDeclaringScope(
|
||||
sequence.instructions[0].value.instructions[0].lvalue,
|
||||
) &&
|
||||
sequence.instructions[0].value.value.kind === 'PropertyLoad' &&
|
||||
sequence.instructions[0].value.value.object.identifier.id ===
|
||||
sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
|
||||
sequence.value.kind === 'SequenceExpression' &&
|
||||
sequence.value.instructions.length === 1 &&
|
||||
sequence.value.instructions[0].lvalue !== null &&
|
||||
sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
|
||||
sequence.value.instructions[0].value.object.identifier.id ===
|
||||
sequence.instructions[0].lvalue.identifier.id &&
|
||||
sequence.value.value.kind === 'LoadLocal' &&
|
||||
sequence.value.value.place.identifier.id ===
|
||||
sequence.value.instructions[0].lvalue.identifier.id
|
||||
) {
|
||||
// LoadLocal <variable>
|
||||
context.declareTemporary(
|
||||
sequence.instructions[0].value.instructions[0].lvalue,
|
||||
sequence.instructions[0].value.instructions[0].value.place,
|
||||
);
|
||||
// PropertyLoad <variable> . <property1> (the inner non-optional property)
|
||||
context.declareProperty(
|
||||
sequence.instructions[0].lvalue,
|
||||
sequence.instructions[0].value.value.object,
|
||||
sequence.instructions[0].value.value.property,
|
||||
false,
|
||||
);
|
||||
const propertyLoad = sequence.value.instructions[0].value;
|
||||
return {
|
||||
lvalue,
|
||||
object: propertyLoad.object,
|
||||
property: propertyLoad.property,
|
||||
optional: optionalValue.optional,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Composed case:
|
||||
* - `<base-case> "." or "?." <property>`
|
||||
* - `<composed-case> "." or "?>" <property>`
|
||||
*
|
||||
* This case is convoluted, note how `t0` appears as an lvalue *twice*
|
||||
* and then is an operand of an intermediate LoadLocal and then the
|
||||
* object of the final PropertyLoad:
|
||||
*
|
||||
* ```
|
||||
* <lvalue> = OptionalExpression optional=false (`optionalValue` is here)
|
||||
* Sequence (`sequence` is here)
|
||||
* t0 = Sequence
|
||||
* t0 =
|
||||
* <nested>
|
||||
* LoadLocal t0
|
||||
* Sequence
|
||||
* t1 = PropertyLoad t0. <property>
|
||||
* LoadLocal t1
|
||||
* ```
|
||||
*/
|
||||
if (
|
||||
sequence.instructions.length === 1 &&
|
||||
sequence.instructions[0].value.kind === 'SequenceExpression' &&
|
||||
sequence.instructions[0].value.instructions.length === 1 &&
|
||||
sequence.instructions[0].value.instructions[0].lvalue !== null &&
|
||||
sequence.instructions[0].value.instructions[0].value.kind ===
|
||||
'OptionalExpression' &&
|
||||
sequence.instructions[0].value.value.kind === 'LoadLocal' &&
|
||||
sequence.instructions[0].value.value.place.identifier.id ===
|
||||
sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
|
||||
sequence.value.kind === 'SequenceExpression' &&
|
||||
sequence.value.instructions.length === 1 &&
|
||||
sequence.value.instructions[0].lvalue !== null &&
|
||||
sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
|
||||
sequence.value.instructions[0].value.object.identifier.id ===
|
||||
sequence.instructions[0].value.value.place.identifier.id &&
|
||||
sequence.value.value.kind === 'LoadLocal' &&
|
||||
sequence.value.value.place.identifier.id ===
|
||||
sequence.value.instructions[0].lvalue.identifier.id
|
||||
) {
|
||||
const {lvalue: innerLvalue, value: innerOptional} =
|
||||
sequence.instructions[0].value.instructions[0];
|
||||
const innerProperty = this.extractOptionalProperty(
|
||||
context,
|
||||
innerOptional,
|
||||
innerLvalue,
|
||||
);
|
||||
if (innerProperty === null) {
|
||||
return null;
|
||||
}
|
||||
context.declareProperty(
|
||||
innerProperty.lvalue,
|
||||
innerProperty.object,
|
||||
innerProperty.property,
|
||||
innerProperty.optional,
|
||||
);
|
||||
const propertyLoad = sequence.value.instructions[0].value;
|
||||
return {
|
||||
lvalue,
|
||||
object: propertyLoad.object,
|
||||
property: propertyLoad.property,
|
||||
optional: optionalValue.optional,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
visitOptionalExpression(
|
||||
context: Context,
|
||||
id: InstructionId,
|
||||
value: ReactiveOptionalCallValue,
|
||||
lvalue: Place | null,
|
||||
): void {
|
||||
/**
|
||||
* If this is the first optional=true optional in a recursive OptionalExpression
|
||||
* subtree, we check to see if the subtree is of the form:
|
||||
* ```
|
||||
* NestedOptional =
|
||||
* `<variable> . / ?. <property>`
|
||||
* `<nested-optional> . / ?. <property>`
|
||||
* ```
|
||||
*
|
||||
* Ie strictly a chain like `foo?.bar?.baz` or `a?.b.c`. If the subtree contains
|
||||
* any other types of expressions - for example `foo?.[makeKey(a)]` - then this
|
||||
* will return null and we'll go to the default handling below.
|
||||
*
|
||||
* If the tree does match the NestedOptional shape, then we'll have recorded
|
||||
* a sequence of declareProperty calls, and the final visitProperty call here
|
||||
* will record that optional chain as a dependency (since we know it's about
|
||||
* to be referenced via its lvalue which is non-null).
|
||||
*/
|
||||
if (
|
||||
lvalue !== null &&
|
||||
value.optional &&
|
||||
this.env.config.enableOptionalDependencies
|
||||
) {
|
||||
const inner = this.extractOptionalProperty(context, value, lvalue);
|
||||
if (inner !== null) {
|
||||
context.visitProperty(inner.object, inner.property, inner.optional);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise we treat everything after the optional as conditional
|
||||
const inner = value.value;
|
||||
/*
|
||||
* OptionalExpression value is a SequenceExpression where the instructions
|
||||
* represent the code prior to the `?` and the final value represents the
|
||||
* conditional code that follows.
|
||||
*/
|
||||
CompilerError.invariant(inner.kind === 'SequenceExpression', {
|
||||
reason: 'Expected OptionalExpression value to be a SequenceExpression',
|
||||
description: `Found a \`${value.kind}\``,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
// Instructions are the unconditionally executed portion before the `?`
|
||||
for (const instr of inner.instructions) {
|
||||
this.visitInstruction(instr, context);
|
||||
}
|
||||
// The final value is the conditional portion following the `?`
|
||||
context.enterConditional(() => {
|
||||
this.visitReactiveValue(context, id, inner.value, null);
|
||||
});
|
||||
}
|
||||
|
||||
visitReactiveValue(
|
||||
context: Context,
|
||||
id: InstructionId,
|
||||
value: ReactiveValue,
|
||||
lvalue: Place | null,
|
||||
): void {
|
||||
switch (value.kind) {
|
||||
case 'OptionalExpression': {
|
||||
this.visitOptionalExpression(context, id, value, lvalue);
|
||||
const inner = value.value;
|
||||
/*
|
||||
* OptionalExpression value is a SequenceExpression where the instructions
|
||||
* represent the code prior to the `?` and the final value represents the
|
||||
* conditional code that follows.
|
||||
*/
|
||||
CompilerError.invariant(inner.kind === 'SequenceExpression', {
|
||||
reason:
|
||||
'Expected OptionalExpression value to be a SequenceExpression',
|
||||
description: `Found a \`${value.kind}\``,
|
||||
loc: value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
// Instructions are the unconditionally executed portion before the `?`
|
||||
for (const instr of inner.instructions) {
|
||||
this.visitInstruction(instr, context);
|
||||
}
|
||||
// The final value is the conditional portion following the `?`
|
||||
context.enterConditional(() => {
|
||||
this.visitReactiveValue(context, id, inner.value);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'LogicalExpression': {
|
||||
this.visitReactiveValue(context, id, value.left, null);
|
||||
this.visitReactiveValue(context, id, value.left);
|
||||
context.enterConditional(() => {
|
||||
this.visitReactiveValue(context, id, value.right, null);
|
||||
this.visitReactiveValue(context, id, value.right);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'ConditionalExpression': {
|
||||
this.visitReactiveValue(context, id, value.test, null);
|
||||
this.visitReactiveValue(context, id, value.test);
|
||||
|
||||
const consequentDeps = context.enterConditional(() => {
|
||||
this.visitReactiveValue(context, id, value.consequent, null);
|
||||
this.visitReactiveValue(context, id, value.consequent);
|
||||
});
|
||||
const alternateDeps = context.enterConditional(() => {
|
||||
this.visitReactiveValue(context, id, value.alternate, null);
|
||||
this.visitReactiveValue(context, id, value.alternate);
|
||||
});
|
||||
context.promoteDepsFromExhaustiveConditionals([
|
||||
consequentDeps,
|
||||
@@ -1044,7 +771,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
break;
|
||||
}
|
||||
case 'FunctionExpression': {
|
||||
if (this.env.config.enableTreatFunctionDepsAsConditional) {
|
||||
if (this.enableTreatFunctionDepsAsConditional) {
|
||||
context.enterConditional(() => {
|
||||
for (const operand of eachInstructionValueOperand(value)) {
|
||||
context.visitOperand(operand);
|
||||
@@ -1091,9 +818,9 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
}
|
||||
} else if (value.kind === 'PropertyLoad') {
|
||||
if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) {
|
||||
context.declareProperty(lvalue, value.object, value.property, false);
|
||||
context.declareProperty(lvalue, value.object, value.property);
|
||||
} else {
|
||||
context.visitProperty(value.object, value.property, false);
|
||||
context.visitProperty(value.object, value.property);
|
||||
}
|
||||
} else if (value.kind === 'StoreLocal') {
|
||||
context.visitOperand(value.value);
|
||||
@@ -1136,7 +863,7 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.visitReactiveValue(context, id, value, lvalue);
|
||||
this.visitReactiveValue(context, id, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1187,30 +914,25 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
break;
|
||||
}
|
||||
case 'for': {
|
||||
this.visitReactiveValue(context, terminal.id, terminal.init, null);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.test, null);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.init);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.test);
|
||||
context.enterConditional(() => {
|
||||
this.visitBlock(terminal.loop, context);
|
||||
if (terminal.update !== null) {
|
||||
this.visitReactiveValue(
|
||||
context,
|
||||
terminal.id,
|
||||
terminal.update,
|
||||
null,
|
||||
);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.update);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'for-of': {
|
||||
this.visitReactiveValue(context, terminal.id, terminal.init, null);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.init);
|
||||
context.enterConditional(() => {
|
||||
this.visitBlock(terminal.loop, context);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'for-in': {
|
||||
this.visitReactiveValue(context, terminal.id, terminal.init, null);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.init);
|
||||
context.enterConditional(() => {
|
||||
this.visitBlock(terminal.loop, context);
|
||||
});
|
||||
@@ -1219,12 +941,12 @@ class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
|
||||
case 'do-while': {
|
||||
this.visitBlock(terminal.loop, context);
|
||||
context.enterConditional(() => {
|
||||
this.visitReactiveValue(context, terminal.id, terminal.test, null);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.test);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'while': {
|
||||
this.visitReactiveValue(context, terminal.id, terminal.test, null);
|
||||
this.visitReactiveValue(context, terminal.id, terminal.test);
|
||||
context.enterConditional(() => {
|
||||
this.visitBlock(terminal.loop, context);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
DeclarationId,
|
||||
Identifier,
|
||||
InstructionKind,
|
||||
ReactiveFunction,
|
||||
ReactiveInstruction,
|
||||
@@ -23,11 +23,11 @@ import {
|
||||
* original instruction kind.
|
||||
*/
|
||||
export function pruneHoistedContexts(fn: ReactiveFunction): void {
|
||||
const hoistedIdentifiers: HoistedIdentifiers = new Map();
|
||||
const hoistedIdentifiers: HoistedIdentifiers = new Set();
|
||||
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
|
||||
}
|
||||
|
||||
type HoistedIdentifiers = Map<DeclarationId, InstructionKind>;
|
||||
type HoistedIdentifiers = Set<Identifier>;
|
||||
|
||||
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
override transformInstruction(
|
||||
@@ -39,42 +39,14 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedConst'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Const,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedLet'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Let,
|
||||
);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'DeclareContext' &&
|
||||
instruction.value.lvalue.kind === 'HoistedFunction'
|
||||
) {
|
||||
state.set(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
InstructionKind.Function,
|
||||
);
|
||||
state.add(instruction.value.lvalue.place.identifier);
|
||||
return {kind: 'remove'};
|
||||
}
|
||||
|
||||
if (
|
||||
instruction.value.kind === 'StoreContext' &&
|
||||
state.has(instruction.value.lvalue.place.identifier.declarationId)
|
||||
state.has(instruction.value.lvalue.place.identifier)
|
||||
) {
|
||||
const kind = state.get(
|
||||
instruction.value.lvalue.place.identifier.declarationId,
|
||||
)!;
|
||||
return {
|
||||
kind: 'replace',
|
||||
value: {
|
||||
@@ -85,7 +57,7 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
|
||||
...instruction.value,
|
||||
lvalue: {
|
||||
...instruction.value.lvalue,
|
||||
kind,
|
||||
kind: InstructionKind.Const,
|
||||
},
|
||||
type: null,
|
||||
kind: 'StoreLocal',
|
||||
|
||||
@@ -180,8 +180,8 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
|
||||
[...scope.scope.dependencies].forEach(ident => {
|
||||
let target: undefined | IdentifierId =
|
||||
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
|
||||
ident.path.forEach(token => {
|
||||
target &&= this.paths.get(target)?.get(token.property);
|
||||
ident.path.forEach(key => {
|
||||
target &&= this.paths.get(target)?.get(key);
|
||||
});
|
||||
if (target && this.map.get(target) === 'Create') {
|
||||
scope.scope.dependencies.delete(ident);
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Environment,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
Pattern,
|
||||
Place,
|
||||
@@ -25,8 +24,8 @@ import {
|
||||
isMutableEffect,
|
||||
} from '../HIR';
|
||||
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
|
||||
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
|
||||
import {getPlaceScope} from '../HIR/HIR';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {getPlaceScope} from './BuildReactiveBlocks';
|
||||
import {
|
||||
ReactiveFunctionTransform,
|
||||
ReactiveFunctionVisitor,
|
||||
@@ -116,9 +115,9 @@ export function pruneNonEscapingScopes(fn: ReactiveFunction): void {
|
||||
const state = new State(fn.env);
|
||||
for (const param of fn.params) {
|
||||
if (param.kind === 'Identifier') {
|
||||
state.declare(param.identifier.declarationId);
|
||||
state.declare(param.identifier.id);
|
||||
} else {
|
||||
state.declare(param.place.identifier.declarationId);
|
||||
state.declare(param.place.identifier.id);
|
||||
}
|
||||
}
|
||||
visitReactiveFunction(fn, new CollectDependenciesVisitor(fn.env), state);
|
||||
@@ -194,14 +193,14 @@ function joinAliases(
|
||||
type IdentifierNode = {
|
||||
level: MemoizationLevel;
|
||||
memoized: boolean;
|
||||
dependencies: Set<DeclarationId>;
|
||||
dependencies: Set<IdentifierId>;
|
||||
scopes: Set<ScopeId>;
|
||||
seen: boolean;
|
||||
};
|
||||
|
||||
// A scope node describing its dependencies
|
||||
type ScopeNode = {
|
||||
dependencies: Array<DeclarationId>;
|
||||
dependencies: Array<IdentifierId>;
|
||||
seen: boolean;
|
||||
};
|
||||
|
||||
@@ -210,30 +209,20 @@ class State {
|
||||
env: Environment;
|
||||
/*
|
||||
* Maps lvalues for LoadLocal to the identifier being loaded, to resolve indirections
|
||||
* in subsequent lvalues/rvalues.
|
||||
*
|
||||
* NOTE: this pass uses DeclarationId rather than IdentifierId because the pass is not
|
||||
* aware of control-flow, only data flow via mutation. Instead of precisely modeling
|
||||
* control flow, we analyze all values that may flow into a particular program variable,
|
||||
* and then whether that program variable may escape (if so, the values flowing in may
|
||||
* escape too). Thus we use DeclarationId to captures all values that may flow into
|
||||
* a particular program variable, regardless of control flow paths.
|
||||
*
|
||||
* In the future when we convert to HIR everywhere this pass can account for control
|
||||
* flow and use SSA ids.
|
||||
* in subsequent lvalues/rvalues
|
||||
*/
|
||||
definitions: Map<DeclarationId, DeclarationId> = new Map();
|
||||
definitions: Map<IdentifierId, IdentifierId> = new Map();
|
||||
|
||||
identifiers: Map<DeclarationId, IdentifierNode> = new Map();
|
||||
identifiers: Map<IdentifierId, IdentifierNode> = new Map();
|
||||
scopes: Map<ScopeId, ScopeNode> = new Map();
|
||||
escapingValues: Set<DeclarationId> = new Set();
|
||||
escapingValues: Set<IdentifierId> = new Set();
|
||||
|
||||
constructor(env: Environment) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
// Declare a new identifier, used for function id and params
|
||||
declare(id: DeclarationId): void {
|
||||
declare(id: IdentifierId): void {
|
||||
this.identifiers.set(id, {
|
||||
level: MemoizationLevel.Never,
|
||||
memoized: false,
|
||||
@@ -251,16 +240,14 @@ class State {
|
||||
visitOperand(
|
||||
id: InstructionId,
|
||||
place: Place,
|
||||
identifier: DeclarationId,
|
||||
identifier: IdentifierId,
|
||||
): void {
|
||||
const scope = getPlaceScope(id, place);
|
||||
if (scope !== null) {
|
||||
let node = this.scopes.get(scope.id);
|
||||
if (node === undefined) {
|
||||
node = {
|
||||
dependencies: [...scope.dependencies].map(
|
||||
dep => dep.identifier.declarationId,
|
||||
),
|
||||
dependencies: [...scope.dependencies].map(dep => dep.identifier.id),
|
||||
seen: false,
|
||||
};
|
||||
this.scopes.set(scope.id, node);
|
||||
@@ -282,11 +269,11 @@ class State {
|
||||
* to determine which other values should be memoized. Returns a set of all identifiers
|
||||
* that should be memoized.
|
||||
*/
|
||||
function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
|
||||
const memoized = new Set<DeclarationId>();
|
||||
function computeMemoizedIdentifiers(state: State): Set<IdentifierId> {
|
||||
const memoized = new Set<IdentifierId>();
|
||||
|
||||
// Visit an identifier, optionally forcing it to be memoized
|
||||
function visit(id: DeclarationId, forceMemoize: boolean = false): boolean {
|
||||
function visit(id: IdentifierId, forceMemoize: boolean = false): boolean {
|
||||
const node = state.identifiers.get(id);
|
||||
CompilerError.invariant(node !== undefined, {
|
||||
reason: `Expected a node for all identifiers, none found for \`${id}\``,
|
||||
@@ -671,37 +658,12 @@ function computeMemoizationInputs(
|
||||
],
|
||||
};
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
const signature = getFunctionCallSignature(
|
||||
env,
|
||||
value.tag.identifier.type,
|
||||
);
|
||||
let lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
|
||||
}
|
||||
if (signature?.noAlias === true) {
|
||||
return {
|
||||
lvalues,
|
||||
rvalues: [],
|
||||
};
|
||||
}
|
||||
const operands = [...eachReactiveValueOperand(value)];
|
||||
lvalues.push(
|
||||
...operands
|
||||
.filter(operand => isMutableEffect(operand.effect, operand.loc))
|
||||
.map(place => ({place, level: MemoizationLevel.Memoized})),
|
||||
);
|
||||
return {
|
||||
lvalues,
|
||||
rvalues: operands,
|
||||
};
|
||||
}
|
||||
case 'CallExpression': {
|
||||
const signature = getFunctionCallSignature(
|
||||
env,
|
||||
value.callee.identifier.type,
|
||||
);
|
||||
const operands = [...eachReactiveValueOperand(value)];
|
||||
let lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
|
||||
@@ -712,7 +674,6 @@ function computeMemoizationInputs(
|
||||
rvalues: [],
|
||||
};
|
||||
}
|
||||
const operands = [...eachReactiveValueOperand(value)];
|
||||
lvalues.push(
|
||||
...operands
|
||||
.filter(operand => isMutableEffect(operand.effect, operand.loc))
|
||||
@@ -728,6 +689,7 @@ function computeMemoizationInputs(
|
||||
env,
|
||||
value.property.identifier.type,
|
||||
);
|
||||
const operands = [...eachReactiveValueOperand(value)];
|
||||
let lvalues = [];
|
||||
if (lvalue !== null) {
|
||||
lvalues.push({place: lvalue, level: MemoizationLevel.Memoized});
|
||||
@@ -738,7 +700,6 @@ function computeMemoizationInputs(
|
||||
rvalues: [],
|
||||
};
|
||||
}
|
||||
const operands = [...eachReactiveValueOperand(value)];
|
||||
lvalues.push(
|
||||
...operands
|
||||
.filter(operand => isMutableEffect(operand.effect, operand.loc))
|
||||
@@ -752,6 +713,7 @@ function computeMemoizationInputs(
|
||||
case 'RegExpLiteral':
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression':
|
||||
case 'TaggedTemplateExpression':
|
||||
case 'ArrayExpression':
|
||||
case 'NewExpression':
|
||||
case 'ObjectExpression':
|
||||
@@ -870,16 +832,14 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
|
||||
// Associate all the rvalues with the instruction's scope if it has one
|
||||
for (const operand of aliasing.rvalues) {
|
||||
const operandId =
|
||||
state.definitions.get(operand.identifier.declarationId) ??
|
||||
operand.identifier.declarationId;
|
||||
state.definitions.get(operand.identifier.id) ?? operand.identifier.id;
|
||||
state.visitOperand(instruction.id, operand, operandId);
|
||||
}
|
||||
|
||||
// Add the operands as dependencies of all lvalues.
|
||||
for (const {place: lvalue, level} of aliasing.lvalues) {
|
||||
const lvalueId =
|
||||
state.definitions.get(lvalue.identifier.declarationId) ??
|
||||
lvalue.identifier.declarationId;
|
||||
state.definitions.get(lvalue.identifier.id) ?? lvalue.identifier.id;
|
||||
let node = state.identifiers.get(lvalueId);
|
||||
if (node === undefined) {
|
||||
node = {
|
||||
@@ -898,8 +858,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
|
||||
*/
|
||||
for (const operand of aliasing.rvalues) {
|
||||
const operandId =
|
||||
state.definitions.get(operand.identifier.declarationId) ??
|
||||
operand.identifier.declarationId;
|
||||
state.definitions.get(operand.identifier.id) ?? operand.identifier.id;
|
||||
if (operandId === lvalueId) {
|
||||
continue;
|
||||
}
|
||||
@@ -911,8 +870,8 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
|
||||
|
||||
if (instruction.value.kind === 'LoadLocal' && instruction.lvalue !== null) {
|
||||
state.definitions.set(
|
||||
instruction.lvalue.identifier.declarationId,
|
||||
instruction.value.place.identifier.declarationId,
|
||||
instruction.lvalue.identifier.id,
|
||||
instruction.value.place.identifier.id,
|
||||
);
|
||||
} else if (
|
||||
instruction.value.kind === 'CallExpression' ||
|
||||
@@ -938,7 +897,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
|
||||
}
|
||||
for (const operand of instruction.value.args) {
|
||||
const place = operand.kind === 'Spread' ? operand.place : operand;
|
||||
state.escapingValues.add(place.identifier.declarationId);
|
||||
state.escapingValues.add(place.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,25 +910,20 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<State> {
|
||||
this.traverseTerminal(stmt, state);
|
||||
|
||||
if (stmt.terminal.kind === 'return') {
|
||||
state.escapingValues.add(stmt.terminal.value.identifier.declarationId);
|
||||
state.escapingValues.add(stmt.terminal.value.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prune reactive scopes that do not have any memoized outputs
|
||||
class PruneScopesTransform extends ReactiveFunctionTransform<
|
||||
Set<DeclarationId>
|
||||
Set<IdentifierId>
|
||||
> {
|
||||
prunedScopes: Set<ScopeId> = new Set();
|
||||
/**
|
||||
* Track reassignments so we can correctly set `pruned` flags for
|
||||
* inlined useMemos.
|
||||
*/
|
||||
reassignments: Map<DeclarationId, Set<Identifier>> = new Map();
|
||||
|
||||
override transformScope(
|
||||
scopeBlock: ReactiveScopeBlock,
|
||||
state: Set<DeclarationId>,
|
||||
state: Set<IdentifierId>,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.visitScope(scopeBlock, state);
|
||||
|
||||
@@ -991,11 +945,11 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
|
||||
}
|
||||
|
||||
const hasMemoizedOutput =
|
||||
Array.from(scopeBlock.scope.declarations.values()).some(decl =>
|
||||
state.has(decl.identifier.declarationId),
|
||||
Array.from(scopeBlock.scope.declarations.keys()).some(id =>
|
||||
state.has(id),
|
||||
) ||
|
||||
Array.from(scopeBlock.scope.reassignments).some(identifier =>
|
||||
state.has(identifier.declarationId),
|
||||
state.has(identifier.id),
|
||||
);
|
||||
if (hasMemoizedOutput) {
|
||||
return {kind: 'keep'};
|
||||
@@ -1008,45 +962,24 @@ class PruneScopesTransform extends ReactiveFunctionTransform<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we pruned the scope for a non-escaping value, we know it doesn't
|
||||
* need to be memoized. Remove associated `Memoize` instructions so that
|
||||
* we don't report false positives on "missing" memoization of these values.
|
||||
*/
|
||||
override transformInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
state: Set<DeclarationId>,
|
||||
state: Set<IdentifierId>,
|
||||
): Transformed<ReactiveStatement> {
|
||||
this.traverseInstruction(instruction, state);
|
||||
|
||||
const value = instruction.value;
|
||||
if (value.kind === 'StoreLocal' && value.lvalue.kind === 'Reassign') {
|
||||
const ids = getOrInsertDefault(
|
||||
this.reassignments,
|
||||
value.lvalue.place.identifier.declarationId,
|
||||
new Set(),
|
||||
);
|
||||
ids.add(value.value.identifier);
|
||||
} else if (value.kind === 'FinishMemoize') {
|
||||
let decls;
|
||||
if (value.decl.identifier.scope == null) {
|
||||
/**
|
||||
* If the manual memo was a useMemo that got inlined, iterate through
|
||||
* all reassignments to the iife temporary to ensure they're memoized.
|
||||
*/
|
||||
decls = this.reassignments.get(value.decl.identifier.declarationId) ?? [
|
||||
value.decl.identifier,
|
||||
];
|
||||
} else {
|
||||
decls = [value.decl.identifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* If we pruned the scope for a non-escaping value, we know it doesn't
|
||||
* need to be memoized. Remove associated `Memoize` instructions so that
|
||||
* we don't report false positives on "missing" memoization of these values.
|
||||
*/
|
||||
if (instruction.value.kind === 'FinishMemoize') {
|
||||
const identifier = instruction.value.decl.identifier;
|
||||
if (
|
||||
[...decls].every(
|
||||
decl => decl.scope == null || this.prunedScopes.has(decl.scope.id),
|
||||
)
|
||||
identifier.scope !== null &&
|
||||
this.prunedScopes.has(identifier.scope.id)
|
||||
) {
|
||||
value.pruned = true;
|
||||
instruction.value.pruned = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
DeclarationId,
|
||||
Identifier,
|
||||
InstructionId,
|
||||
Place,
|
||||
ReactiveFunction,
|
||||
@@ -18,27 +18,19 @@ import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
|
||||
* Nulls out lvalues for temporary variables that are never accessed later. This only
|
||||
* nulls out the lvalue itself, it does not remove the corresponding instructions.
|
||||
*/
|
||||
export function pruneUnusedLValues(fn: ReactiveFunction): void {
|
||||
const lvalues = new Map<DeclarationId, ReactiveInstruction>();
|
||||
export function pruneTemporaryLValues(fn: ReactiveFunction): void {
|
||||
const lvalues = new Map<Identifier, ReactiveInstruction>();
|
||||
visitReactiveFunction(fn, new Visitor(), lvalues);
|
||||
for (const [, instr] of lvalues) {
|
||||
instr.lvalue = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This pass uses DeclarationIds because the lvalue IdentifierId of a compound expression
|
||||
* (ternary, logical, optional) in ReactiveFunction may not be the same as the IdentifierId
|
||||
* of the phi, and which is referenced later. Keying by DeclarationId ensures we don't
|
||||
* delete lvalues for identifiers that are used.
|
||||
*
|
||||
* TODO LeaveSSA: once we use HIR everywhere, this can likely move back to using IdentifierId
|
||||
*/
|
||||
type LValues = Map<DeclarationId, ReactiveInstruction>;
|
||||
type LValues = Map<Identifier, ReactiveInstruction>;
|
||||
|
||||
class Visitor extends ReactiveFunctionVisitor<LValues> {
|
||||
override visitPlace(id: InstructionId, place: Place, state: LValues): void {
|
||||
state.delete(place.identifier.declarationId);
|
||||
state.delete(place.identifier);
|
||||
}
|
||||
override visitInstruction(
|
||||
instruction: ReactiveInstruction,
|
||||
@@ -49,7 +41,7 @@ class Visitor extends ReactiveFunctionVisitor<LValues> {
|
||||
instruction.lvalue !== null &&
|
||||
instruction.lvalue.identifier.name === null
|
||||
) {
|
||||
state.set(instruction.lvalue.identifier.declarationId, instruction);
|
||||
state.set(instruction.lvalue.identifier, instruction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
IdentifierName,
|
||||
InstructionId,
|
||||
Place,
|
||||
@@ -121,8 +121,8 @@ class Visitor extends ReactiveFunctionVisitor<Scopes> {
|
||||
}
|
||||
|
||||
class Scopes {
|
||||
#seen: Map<DeclarationId, IdentifierName> = new Map();
|
||||
#stack: Array<Map<string, DeclarationId>> = [new Map()];
|
||||
#seen: Map<IdentifierId, IdentifierName> = new Map();
|
||||
#stack: Array<Map<string, IdentifierId>> = [new Map()];
|
||||
#globals: Set<string>;
|
||||
names: Set<ValidIdentifierName> = new Set();
|
||||
|
||||
@@ -135,7 +135,7 @@ class Scopes {
|
||||
if (originalName === null) {
|
||||
return;
|
||||
}
|
||||
const mappedName = this.#seen.get(identifier.declarationId);
|
||||
const mappedName = this.#seen.get(identifier.id);
|
||||
if (mappedName !== undefined) {
|
||||
identifier.name = mappedName;
|
||||
return;
|
||||
@@ -158,12 +158,12 @@ class Scopes {
|
||||
}
|
||||
const identifierName = makeIdentifierName(name);
|
||||
identifier.name = identifierName;
|
||||
this.#seen.set(identifier.declarationId, identifierName);
|
||||
this.#stack.at(-1)!.set(identifierName.value, identifier.declarationId);
|
||||
this.#seen.set(identifier.id, identifierName);
|
||||
this.#stack.at(-1)!.set(identifierName.value, identifier.id);
|
||||
this.names.add(identifierName.value);
|
||||
}
|
||||
|
||||
#lookup(name: string): DeclarationId | null {
|
||||
#lookup(name: string): IdentifierId | null {
|
||||
for (let i = this.#stack.length - 1; i >= 0; i--) {
|
||||
const scope = this.#stack[i]!;
|
||||
const entry = scope.get(name);
|
||||
|
||||
@@ -6,13 +6,18 @@
|
||||
*/
|
||||
|
||||
export {alignObjectMethodScopes} from './AlignObjectMethodScopes';
|
||||
export {alignReactiveScopesToBlockScopes} from './AlignReactiveScopesToBlockScopes';
|
||||
export {assertScopeInstructionsWithinScopes} from './AssertScopeInstructionsWithinScope';
|
||||
export {assertWellFormedBreakTargets} from './AssertWellFormedBreakTargets';
|
||||
export {buildReactiveBlocks} from './BuildReactiveBlocks';
|
||||
export {buildReactiveFunction} from './BuildReactiveFunction';
|
||||
export {codegenFunction, type CodegenFunction} from './CodegenReactiveFunction';
|
||||
export {extractScopeDeclarationsFromDestructuring} from './ExtractScopeDeclarationsFromDestructuring';
|
||||
export {flattenReactiveLoops} from './FlattenReactiveLoops';
|
||||
export {flattenScopesWithHooksOrUse} from './FlattenScopesWithHooksOrUse';
|
||||
export {inferReactiveScopeVariables} from './InferReactiveScopeVariables';
|
||||
export {memoizeFbtAndMacroOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
export {memoizeFbtAndMacroOperandsInSameScope as memoizeFbtOperandsInSameScope} from './MemoizeFbtAndMacroOperandsInSameScope';
|
||||
export {mergeOverlappingReactiveScopes} from './MergeOverlappingReactiveScopes';
|
||||
export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesThatInvalidateTogether';
|
||||
export {printReactiveFunction} from './PrintReactiveFunction';
|
||||
export {promoteUsedTemporaries} from './PromoteUsedTemporaries';
|
||||
@@ -22,7 +27,7 @@ export {pruneAllReactiveScopes} from './PruneAllReactiveScopes';
|
||||
export {pruneHoistedContexts} from './PruneHoistedContexts';
|
||||
export {pruneNonEscapingScopes} from './PruneNonEscapingScopes';
|
||||
export {pruneNonReactiveDependencies} from './PruneNonReactiveDependencies';
|
||||
export {pruneUnusedLValues} from './PruneTemporaryLValues';
|
||||
export {pruneTemporaryLValues as pruneUnusedLValues} from './PruneTemporaryLValues';
|
||||
export {pruneUnusedLabels} from './PruneUnusedLabels';
|
||||
export {pruneUnusedScopes} from './PruneUnusedScopes';
|
||||
export {renameVariables} from './RenameVariables';
|
||||
|
||||
@@ -79,7 +79,6 @@ class SSABuilder {
|
||||
makeId(oldId: Identifier): Identifier {
|
||||
return {
|
||||
id: this.nextSsaId,
|
||||
declarationId: oldId.declarationId,
|
||||
name: oldId.name,
|
||||
mutableRange: {
|
||||
start: makeInstructionId(0),
|
||||
@@ -192,6 +191,7 @@ class SSABuilder {
|
||||
kind: 'Phi',
|
||||
id: newId,
|
||||
operands: predDefs,
|
||||
type: makeType(),
|
||||
};
|
||||
|
||||
block.phis.add(phi);
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
BasicBlock,
|
||||
BlockId,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
InstructionKind,
|
||||
LValue,
|
||||
LValuePattern,
|
||||
Phi,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachPatternOperand,
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
terminalFallthrough,
|
||||
} from '../HIR/visitors';
|
||||
|
||||
/*
|
||||
* Removes SSA form by converting all phis into explicit bindings and assignments. There are two main categories
|
||||
* of phis:
|
||||
*
|
||||
* ## Reassignments (operands are independently memoizable)
|
||||
*
|
||||
* These are phis that occur after some high-level control flow such as an if, switch, or loop. These phis are rewritten
|
||||
* to add a new `let` binding for the phi id prior to the control flow node (ie prior to the if/switch),
|
||||
* and to add a reassignment to that let binding in each of the phi's predecessors.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```javascript
|
||||
* // Input
|
||||
* let x1 = null;
|
||||
* if (a) {
|
||||
* x2 = b;
|
||||
* } else {
|
||||
* x3 = c;
|
||||
* }
|
||||
* x4 = phi(x2, x3);
|
||||
* return x4;
|
||||
*
|
||||
* // Output
|
||||
* const x1 = null;
|
||||
* let x4; // synthesized binding for the phi identifier
|
||||
* if (a) {
|
||||
* x2 = b;
|
||||
* x4 = x2;; // sythesized assignment to the phi identifier
|
||||
* } else {
|
||||
* x3 = c;
|
||||
* x4 = x3; // synthesized assignment
|
||||
* }
|
||||
* // phi removed
|
||||
* return x4;
|
||||
* ```
|
||||
*
|
||||
* ## Rewrites (operands are not independently memoizable)
|
||||
*
|
||||
* Phis that occur inside loop constructs cannot use the reassignment strategy, because there isn't an appropriate place
|
||||
* to add the new let binding. Instead, we select a single "canonical" id for these phis which is the operand that is
|
||||
* defined first. Then, all assignments and references for any of the phi ir and operands are rewritten to reference
|
||||
* the canonical id instead.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```javascript
|
||||
* // Input
|
||||
* for (
|
||||
* let i1 = 0;
|
||||
* { i2 = phi(i1, i2); i2 < 10 }; // note the phi in the test block
|
||||
* i2 += 1
|
||||
* ) { ... }
|
||||
*
|
||||
* // Output
|
||||
* for (
|
||||
* let i1 = 0; // i1 is defined first, so it becomes the canonical id
|
||||
* i1 < 10; // rewritten to canonical id
|
||||
* i1 += 1 // rewritten to canonical id
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function leaveSSA(fn: HIRFunction): void {
|
||||
// Maps identifier names to their original declaration.
|
||||
const declarations: Map<
|
||||
string,
|
||||
{lvalue: LValue | LValuePattern; place: Place}
|
||||
> = new Map();
|
||||
|
||||
for (const param of fn.params) {
|
||||
let place: Place = param.kind === 'Identifier' ? param : param.place;
|
||||
if (place.identifier.name !== null) {
|
||||
declarations.set(place.identifier.name.value, {
|
||||
lvalue: {
|
||||
kind: InstructionKind.Let,
|
||||
place,
|
||||
},
|
||||
place,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* For non-memoizable phis, this maps original identifiers to the identifier they should be
|
||||
* *rewritten* to. The keys are the original identifiers, and the value will be _either_ the
|
||||
* phi id or, more typically, the operand that was defined prior to the phi.
|
||||
*/
|
||||
const rewrites: Map<Identifier, Identifier> = new Map();
|
||||
|
||||
type PhiState = {
|
||||
phi: Phi;
|
||||
block: BasicBlock;
|
||||
};
|
||||
|
||||
const seen = new Set<BlockId>();
|
||||
const backEdgePhis = new Set<Phi>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
for (const [pred] of phi.operands) {
|
||||
if (!seen.has(pred)) {
|
||||
backEdgePhis.add(phi);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
seen.add(block.id);
|
||||
}
|
||||
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
/*
|
||||
* Iterate the instructions and perform any rewrites as well as promoting SSA variables to
|
||||
* `let` or `reassign` where possible.
|
||||
*/
|
||||
const {lvalue, value} = instr;
|
||||
if (value.kind === 'DeclareLocal') {
|
||||
const name = value.lvalue.place.identifier.name;
|
||||
if (name !== null) {
|
||||
CompilerError.invariant(!declarations.has(name.value), {
|
||||
reason: `Unexpected duplicate declaration`,
|
||||
description: `Found duplicate declaration for \`${name.value}\``,
|
||||
loc: value.lvalue.place.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
declarations.set(name.value, {
|
||||
lvalue: value.lvalue,
|
||||
place: value.lvalue.place,
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
value.kind === 'PrefixUpdate' ||
|
||||
value.kind === 'PostfixUpdate'
|
||||
) {
|
||||
CompilerError.invariant(value.lvalue.identifier.name !== null, {
|
||||
reason: `Expected update expression to be applied to a named variable`,
|
||||
description: null,
|
||||
loc: value.lvalue.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
const originalLVal = declarations.get(
|
||||
value.lvalue.identifier.name.value,
|
||||
);
|
||||
CompilerError.invariant(originalLVal !== undefined, {
|
||||
reason: `Expected update expression to be applied to a previously defined variable`,
|
||||
description: null,
|
||||
loc: value.lvalue.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
originalLVal.lvalue.kind = InstructionKind.Let;
|
||||
} else if (value.kind === 'StoreLocal') {
|
||||
if (value.lvalue.place.identifier.name != null) {
|
||||
const originalLVal = declarations.get(
|
||||
value.lvalue.place.identifier.name.value,
|
||||
);
|
||||
if (
|
||||
originalLVal === undefined ||
|
||||
originalLVal.lvalue === value.lvalue // in case this was pre-declared for the `for` initializer
|
||||
) {
|
||||
CompilerError.invariant(
|
||||
originalLVal !== undefined ||
|
||||
block.kind === 'block' ||
|
||||
block.kind === 'catch',
|
||||
{
|
||||
reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
|
||||
description: null,
|
||||
loc: value.lvalue.place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
declarations.set(value.lvalue.place.identifier.name.value, {
|
||||
lvalue: value.lvalue,
|
||||
place: value.lvalue.place,
|
||||
});
|
||||
value.lvalue.kind = InstructionKind.Const;
|
||||
} else {
|
||||
/*
|
||||
* This is an instance of the original id, so we need to promote the original declaration
|
||||
* to a `let` and the current lval to a `reassign`
|
||||
*/
|
||||
originalLVal.lvalue.kind = InstructionKind.Let;
|
||||
value.lvalue.kind = InstructionKind.Reassign;
|
||||
}
|
||||
} else if (rewrites.has(value.lvalue.place.identifier)) {
|
||||
value.lvalue.kind = InstructionKind.Const;
|
||||
}
|
||||
} else if (value.kind === 'Destructure') {
|
||||
let kind: InstructionKind | null = null;
|
||||
for (const place of eachPatternOperand(value.lvalue.pattern)) {
|
||||
if (place.identifier.name == null) {
|
||||
CompilerError.invariant(
|
||||
kind === null || kind === InstructionKind.Const,
|
||||
{
|
||||
reason: `Expected consistent kind for destructuring`,
|
||||
description: `other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is const`,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
kind = InstructionKind.Const;
|
||||
} else {
|
||||
const originalLVal = declarations.get(place.identifier.name.value);
|
||||
if (
|
||||
originalLVal === undefined ||
|
||||
originalLVal.lvalue === value.lvalue
|
||||
) {
|
||||
CompilerError.invariant(
|
||||
originalLVal !== undefined || block.kind !== 'value',
|
||||
{
|
||||
reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
|
||||
description: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
declarations.set(place.identifier.name.value, {
|
||||
lvalue: value.lvalue,
|
||||
place,
|
||||
});
|
||||
CompilerError.invariant(
|
||||
kind === null || kind === InstructionKind.Const,
|
||||
{
|
||||
reason: `Expected consistent kind for destructuring`,
|
||||
description: `Other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is const`,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
kind = InstructionKind.Const;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
kind === null || kind === InstructionKind.Reassign,
|
||||
{
|
||||
reason: `Expected consistent kind for destructuring`,
|
||||
description: `Other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is reassigned`,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
kind = InstructionKind.Reassign;
|
||||
originalLVal.lvalue.kind = InstructionKind.Let;
|
||||
}
|
||||
}
|
||||
}
|
||||
CompilerError.invariant(kind !== null, {
|
||||
reason: 'Expected at least one operand',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
value.lvalue.kind = kind;
|
||||
}
|
||||
rewritePlace(lvalue, rewrites, declarations);
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
rewritePlace(operand, rewrites, declarations);
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
rewritePlace(operand, rewrites, declarations);
|
||||
}
|
||||
}
|
||||
|
||||
const terminal = block.terminal;
|
||||
for (const operand of eachTerminalOperand(terminal)) {
|
||||
rewritePlace(operand, rewrites, declarations);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find any phi nodes which need a variable declaration in the current block
|
||||
* This includes phis in fallthrough nodes, or blocks that form part of control flow
|
||||
* such as for or while (and later if/switch).
|
||||
*/
|
||||
const reassignmentPhis: Array<PhiState> = [];
|
||||
const rewritePhis: Array<PhiState> = [];
|
||||
function pushPhis(phiBlock: BasicBlock): void {
|
||||
for (const phi of phiBlock.phis) {
|
||||
if (phi.id.name === null) {
|
||||
rewritePhis.push({phi, block: phiBlock});
|
||||
} else {
|
||||
reassignmentPhis.push({phi, block: phiBlock});
|
||||
}
|
||||
}
|
||||
}
|
||||
const fallthroughId = terminalFallthrough(terminal);
|
||||
if (fallthroughId !== null) {
|
||||
const fallthrough = fn.body.blocks.get(fallthroughId)!;
|
||||
pushPhis(fallthrough);
|
||||
}
|
||||
if (terminal.kind === 'while' || terminal.kind === 'for') {
|
||||
const test = fn.body.blocks.get(terminal.test)!;
|
||||
pushPhis(test);
|
||||
|
||||
const loop = fn.body.blocks.get(terminal.loop)!;
|
||||
pushPhis(loop);
|
||||
}
|
||||
if (
|
||||
terminal.kind === 'for' ||
|
||||
terminal.kind === 'for-of' ||
|
||||
terminal.kind === 'for-in'
|
||||
) {
|
||||
let init = fn.body.blocks.get(terminal.init)!;
|
||||
pushPhis(init);
|
||||
|
||||
// The first block after the end of the init
|
||||
let initContinuation =
|
||||
terminal.kind === 'for' ? terminal.test : terminal.loop;
|
||||
/*
|
||||
* To avoid generating a let binding for the initializer prior to the loop,
|
||||
* check to see if the for declares an iterator variable.
|
||||
*/
|
||||
const queue: Array<BlockId> = [init.id];
|
||||
while (queue.length !== 0) {
|
||||
const blockId = queue.shift()!;
|
||||
if (blockId === initContinuation) {
|
||||
break;
|
||||
}
|
||||
const block = fn.body.blocks.get(blockId)!;
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'StoreLocal' &&
|
||||
instr.value.lvalue.kind !== InstructionKind.Reassign
|
||||
) {
|
||||
const value = instr.value;
|
||||
if (value.lvalue.place.identifier.name !== null) {
|
||||
const originalLVal = declarations.get(
|
||||
value.lvalue.place.identifier.name.value,
|
||||
);
|
||||
if (originalLVal === undefined) {
|
||||
declarations.set(value.lvalue.place.identifier.name.value, {
|
||||
lvalue: value.lvalue,
|
||||
place: value.lvalue.place,
|
||||
});
|
||||
value.lvalue.kind = InstructionKind.Const;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (block.terminal.kind) {
|
||||
case 'maybe-throw': {
|
||||
queue.push(block.terminal.continuation);
|
||||
break;
|
||||
}
|
||||
case 'goto': {
|
||||
queue.push(block.terminal.block);
|
||||
break;
|
||||
}
|
||||
case 'branch':
|
||||
case 'logical':
|
||||
case 'optional':
|
||||
case 'ternary':
|
||||
case 'label': {
|
||||
for (const successor of eachTerminalSuccessor(block.terminal)) {
|
||||
queue.push(successor);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (terminal.kind === 'for' && terminal.update !== null) {
|
||||
const update = fn.body.blocks.get(terminal.update)!;
|
||||
pushPhis(update);
|
||||
}
|
||||
}
|
||||
|
||||
for (const {phi, block: phiBlock} of reassignmentPhis) {
|
||||
/*
|
||||
* In some cases one of the phi operands can be defined *before* the let binding
|
||||
* we will generate. For example, a variable that is only rebound in one branch of
|
||||
* an if but not another. In this case we populate the let binding with this initial
|
||||
* value rather than generate an extra assignment.
|
||||
*/
|
||||
let initOperand: Identifier | null = null;
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (operand.mutableRange.start < terminal.id) {
|
||||
if (initOperand == null) {
|
||||
initOperand = operand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the phi is mutated after its creation, then any values which flow into the phi
|
||||
* must also have their ranges extended accordingly.
|
||||
*/
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.id.mutableRange.end >
|
||||
(phiBlock.instructions.at(0)?.id ?? phiBlock.terminal.id);
|
||||
|
||||
/*
|
||||
* If we never saw a declaration for this phi, it may have been pruned by DCE, so synthesize
|
||||
* a new Let binding
|
||||
*/
|
||||
CompilerError.invariant(phi.id.name != null, {
|
||||
reason: 'Expected reassignment phis to have a name',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
const declaration = declarations.get(phi.id.name.value);
|
||||
CompilerError.invariant(declaration != null, {
|
||||
loc: null,
|
||||
reason: 'Expected a declaration for all variables',
|
||||
description: `${printIdentifier(phi.id)} in block bb${phiBlock.id}`,
|
||||
suggestions: null,
|
||||
});
|
||||
if (isPhiMutatedAfterCreation) {
|
||||
/*
|
||||
* The declaration is not guaranteed to flow into the phi, for example in the case of a variable
|
||||
* that is reassigned in all control flow paths to a given phi. The original declaration's range
|
||||
* has to be extended in this case (if the phi is later mutated) since we are reusing the original
|
||||
* declaration instead of creating a new declaration.
|
||||
*
|
||||
* NOTE: this can *only* happen if the original declaration involves an instruction that DCE does
|
||||
* not prune. Otherwise, the declaration would have been pruned and we'd synthesize a new one.
|
||||
*/
|
||||
declaration.place.identifier.mutableRange.end = phi.id.mutableRange.end;
|
||||
}
|
||||
rewrites.set(phi.id, declaration.place.identifier);
|
||||
}
|
||||
|
||||
/*
|
||||
* Similar logic for rewrite phis that occur in loops, except that instead of a new let binding
|
||||
* we pick one of the operands as the canonical id, and rewrite all references to the other
|
||||
* operands and the phi to reference this canonical id.
|
||||
*/
|
||||
for (const {phi} of rewritePhis) {
|
||||
let canonicalId = rewrites.get(phi.id);
|
||||
if (canonicalId === undefined) {
|
||||
canonicalId = phi.id;
|
||||
for (const [, operand] of phi.operands) {
|
||||
let canonicalOperand = rewrites.get(operand) ?? operand;
|
||||
if (canonicalOperand.id < canonicalId.id) {
|
||||
canonicalId = canonicalOperand;
|
||||
}
|
||||
}
|
||||
rewrites.set(phi.id, canonicalId);
|
||||
|
||||
if (canonicalId.name !== null) {
|
||||
const declaration = declarations.get(canonicalId.name.value);
|
||||
if (declaration !== undefined) {
|
||||
declaration.lvalue.kind = InstructionKind.Let;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// all versions of the variable need to be remapped to the canonical id
|
||||
for (const [, operand] of phi.operands) {
|
||||
rewrites.set(operand, canonicalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Rewrite @param place's identifier based on the given rewrite mapping, if the identifier
|
||||
* is present. Also expands the mutable range of the target identifier to include the
|
||||
* place's range.
|
||||
*/
|
||||
function rewritePlace(
|
||||
place: Place,
|
||||
rewrites: Map<Identifier, Identifier>,
|
||||
declarations: Map<string, {lvalue: LValue | LValuePattern; place: Place}>,
|
||||
): void {
|
||||
const prevIdentifier = place.identifier;
|
||||
const nextIdentifier = rewrites.get(prevIdentifier);
|
||||
|
||||
if (nextIdentifier !== undefined) {
|
||||
if (nextIdentifier === prevIdentifier) return;
|
||||
place.identifier = nextIdentifier;
|
||||
} else if (prevIdentifier.name != null) {
|
||||
const declaration = declarations.get(prevIdentifier.name.value);
|
||||
// Only rewrite identifiers that were declared within the function
|
||||
if (declaration === undefined) return;
|
||||
const originalIdentifier = declaration.place.identifier;
|
||||
prevIdentifier.id = originalIdentifier.id;
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
DeclarationId,
|
||||
HIRFunction,
|
||||
InstructionKind,
|
||||
LValue,
|
||||
LValuePattern,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
|
||||
/**
|
||||
* This pass rewrites the InstructionKind of instructions which declare/assign variables,
|
||||
* converting the first declaration to a Const/Let depending on whether it is subsequently
|
||||
* reassigned, and ensuring that subsequent reassignments are marked as a Reassign. Note
|
||||
* that declarations which were const in the original program cannot become `let`, but the
|
||||
* inverse is not true: a `let` which was reassigned in the source may be converted to a
|
||||
* `const` if the reassignment is not used and was removed by dead code elimination.
|
||||
*
|
||||
* NOTE: this is a subset of the operations previously performed by the LeaveSSA pass.
|
||||
*/
|
||||
export function rewriteInstructionKindsBasedOnReassignment(
|
||||
fn: HIRFunction,
|
||||
): void {
|
||||
const declarations = new Map<DeclarationId, LValue | LValuePattern>();
|
||||
for (const param of fn.params) {
|
||||
let place: Place = param.kind === 'Identifier' ? param : param.place;
|
||||
if (place.identifier.name !== null) {
|
||||
declarations.set(place.identifier.declarationId, {
|
||||
kind: InstructionKind.Let,
|
||||
place,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const place of fn.context) {
|
||||
if (place.identifier.name !== null) {
|
||||
declarations.set(place.identifier.declarationId, {
|
||||
kind: InstructionKind.Let,
|
||||
place,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'DeclareLocal': {
|
||||
const lvalue = value.lvalue;
|
||||
CompilerError.invariant(
|
||||
!declarations.has(lvalue.place.identifier.declarationId),
|
||||
{
|
||||
reason: `Expected variable not to be defined prior to declaration`,
|
||||
description: `${printPlace(lvalue.place)} was already defined`,
|
||||
loc: lvalue.place.loc,
|
||||
},
|
||||
);
|
||||
declarations.set(lvalue.place.identifier.declarationId, lvalue);
|
||||
break;
|
||||
}
|
||||
case 'StoreLocal': {
|
||||
const lvalue = value.lvalue;
|
||||
if (lvalue.place.identifier.name !== null) {
|
||||
const declaration = declarations.get(
|
||||
lvalue.place.identifier.declarationId,
|
||||
);
|
||||
if (declaration === undefined) {
|
||||
CompilerError.invariant(
|
||||
!declarations.has(lvalue.place.identifier.declarationId),
|
||||
{
|
||||
reason: `Expected variable not to be defined prior to declaration`,
|
||||
description: `${printPlace(lvalue.place)} was already defined`,
|
||||
loc: lvalue.place.loc,
|
||||
},
|
||||
);
|
||||
declarations.set(lvalue.place.identifier.declarationId, lvalue);
|
||||
lvalue.kind = InstructionKind.Const;
|
||||
} else {
|
||||
declaration.kind = InstructionKind.Let;
|
||||
lvalue.kind = InstructionKind.Reassign;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Destructure': {
|
||||
const lvalue = value.lvalue;
|
||||
let kind: InstructionKind | null = null;
|
||||
for (const place of eachPatternOperand(lvalue.pattern)) {
|
||||
if (place.identifier.name === null) {
|
||||
CompilerError.invariant(
|
||||
kind === null || kind === InstructionKind.Const,
|
||||
{
|
||||
reason: `Expected consistent kind for destructuring`,
|
||||
description: `other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is const`,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
kind = InstructionKind.Const;
|
||||
} else {
|
||||
const declaration = declarations.get(
|
||||
place.identifier.declarationId,
|
||||
);
|
||||
if (declaration === undefined) {
|
||||
CompilerError.invariant(block.kind !== 'value', {
|
||||
reason: `TODO: Handle reassignment in a value block where the original declaration was removed by dead code elimination (DCE)`,
|
||||
description: null,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
declarations.set(place.identifier.declarationId, lvalue);
|
||||
CompilerError.invariant(
|
||||
kind === null || kind === InstructionKind.Const,
|
||||
{
|
||||
reason: `Expected consistent kind for destructuring`,
|
||||
description: `Other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is const`,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
kind = InstructionKind.Const;
|
||||
} else {
|
||||
CompilerError.invariant(
|
||||
kind === null || kind === InstructionKind.Reassign,
|
||||
{
|
||||
reason: `Expected consistent kind for destructuring`,
|
||||
description: `Other places were \`${kind}\` but '${printPlace(
|
||||
place,
|
||||
)}' is reassigned`,
|
||||
loc: place.loc,
|
||||
suggestions: null,
|
||||
},
|
||||
);
|
||||
kind = InstructionKind.Reassign;
|
||||
declaration.kind = InstructionKind.Let;
|
||||
}
|
||||
}
|
||||
}
|
||||
CompilerError.invariant(kind !== null, {
|
||||
reason: 'Expected at least one operand',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
lvalue.kind = kind;
|
||||
break;
|
||||
}
|
||||
case 'PostfixUpdate':
|
||||
case 'PrefixUpdate': {
|
||||
const lvalue = value.lvalue;
|
||||
const declaration = declarations.get(lvalue.identifier.declarationId);
|
||||
CompilerError.invariant(declaration !== undefined, {
|
||||
reason: `Expected variable to have been defined`,
|
||||
description: `No declaration for ${printPlace(lvalue)}`,
|
||||
loc: lvalue.loc,
|
||||
});
|
||||
declaration.kind = InstructionKind.Let;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,4 @@
|
||||
|
||||
export {eliminateRedundantPhi} from './EliminateRedundantPhi';
|
||||
export {default as enterSSA} from './EnterSSA';
|
||||
export {rewriteInstructionKindsBasedOnReassignment} from './RewriteInstructionKindsBasedOnReassignment';
|
||||
export {leaveSSA} from './LeaveSSA';
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
BuiltInArrayId,
|
||||
BuiltInFunctionId,
|
||||
BuiltInJsxId,
|
||||
BuiltInMixedReadonlyId,
|
||||
BuiltInObjectId,
|
||||
BuiltInPropsId,
|
||||
BuiltInRefValueId,
|
||||
@@ -69,7 +68,7 @@ export function inferTypes(func: HIRFunction): void {
|
||||
function apply(func: HIRFunction, unifier: Unifier): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
phi.id.type = unifier.get(phi.id.type);
|
||||
phi.type = unifier.get(phi.type);
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
@@ -89,7 +88,6 @@ function apply(func: HIRFunction, unifier: Unifier): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
func.returnType = unifier.get(func.returnType);
|
||||
}
|
||||
|
||||
type TypeEquation = {
|
||||
@@ -107,7 +105,7 @@ function equation(left: Type, right: Type): TypeEquation {
|
||||
function* generate(
|
||||
func: HIRFunction,
|
||||
): Generator<TypeEquation, void, undefined> {
|
||||
if (func.fnType === 'Component') {
|
||||
if (func.env.fnType === 'Component') {
|
||||
const [props, ref] = func.params;
|
||||
if (props && props.kind === 'Identifier') {
|
||||
yield equation(props.identifier.type, {
|
||||
@@ -124,10 +122,9 @@ function* generate(
|
||||
}
|
||||
|
||||
const names = new Map();
|
||||
const returnTypes: Array<Type> = [];
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
yield equation(phi.id.type, {
|
||||
yield equation(phi.type, {
|
||||
kind: 'Phi',
|
||||
operands: [...phi.operands.values()].map(id => id.type),
|
||||
});
|
||||
@@ -136,18 +133,6 @@ function* generate(
|
||||
for (const instr of block.instructions) {
|
||||
yield* generateInstructionTypes(func.env, names, instr);
|
||||
}
|
||||
const terminal = block.terminal;
|
||||
if (terminal.kind === 'return') {
|
||||
returnTypes.push(terminal.value.identifier.type);
|
||||
}
|
||||
}
|
||||
if (returnTypes.length > 1) {
|
||||
yield equation(func.returnType, {
|
||||
kind: 'Phi',
|
||||
operands: returnTypes,
|
||||
});
|
||||
} else if (returnTypes.length === 1) {
|
||||
yield equation(func.returnType, returnTypes[0]!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +227,7 @@ function* generateInstructionTypes(
|
||||
}
|
||||
|
||||
case 'LoadGlobal': {
|
||||
const globalType = env.getGlobalDeclaration(value.binding, value.loc);
|
||||
const globalType = env.getGlobalDeclaration(value.binding);
|
||||
if (globalType) {
|
||||
yield equation(left, globalType);
|
||||
}
|
||||
@@ -250,7 +235,6 @@ function* generateInstructionTypes(
|
||||
}
|
||||
|
||||
case 'CallExpression': {
|
||||
const returnType = makeType();
|
||||
/*
|
||||
* TODO: callee could be a hook or a function, so this type equation isn't correct.
|
||||
* We should change Hook to a subtype of Function or change unifier logic.
|
||||
@@ -259,25 +243,8 @@ function* generateInstructionTypes(
|
||||
yield equation(value.callee.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId: null,
|
||||
return: returnType,
|
||||
return: left,
|
||||
});
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'TaggedTemplateExpression': {
|
||||
const returnType = makeType();
|
||||
/*
|
||||
* TODO: callee could be a hook or a function, so this type equation isn't correct.
|
||||
* We should change Hook to a subtype of Function or change unifier logic.
|
||||
* (see https://github.com/facebook/react-forget/pull/1427)
|
||||
*/
|
||||
yield equation(value.tag.identifier.type, {
|
||||
kind: 'Function',
|
||||
shapeId: null,
|
||||
return: returnType,
|
||||
});
|
||||
yield equation(left, returnType);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -379,11 +346,7 @@ function* generateInstructionTypes(
|
||||
|
||||
case 'FunctionExpression': {
|
||||
yield* generate(value.loweredFunc.func);
|
||||
yield equation(left, {
|
||||
kind: 'Function',
|
||||
shapeId: BuiltInFunctionId,
|
||||
return: value.loweredFunc.func.returnType,
|
||||
});
|
||||
yield equation(left, {kind: 'Object', shapeId: BuiltInFunctionId});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -410,6 +373,7 @@ function* generateInstructionTypes(
|
||||
case 'MetaProperty':
|
||||
case 'ComputedStore':
|
||||
case 'ComputedLoad':
|
||||
case 'TaggedTemplateExpression':
|
||||
case 'Await':
|
||||
case 'GetIterator':
|
||||
case 'IteratorNext':
|
||||
@@ -501,140 +465,30 @@ class Unifier {
|
||||
}
|
||||
|
||||
if (type.kind === 'Phi') {
|
||||
CompilerError.invariant(type.operands.length > 0, {
|
||||
const operands = new Set(type.operands.map(i => this.get(i).kind));
|
||||
|
||||
CompilerError.invariant(operands.size > 0, {
|
||||
reason: 'there should be at least one operand',
|
||||
description: null,
|
||||
loc: null,
|
||||
suggestions: null,
|
||||
});
|
||||
const kind = operands.values().next().value;
|
||||
|
||||
let candidateType: Type | null = null;
|
||||
for (const operand of type.operands) {
|
||||
const resolved = this.get(operand);
|
||||
if (candidateType === null) {
|
||||
candidateType = resolved;
|
||||
} else if (!typeEquals(resolved, candidateType)) {
|
||||
const unionType = tryUnionTypes(resolved, candidateType);
|
||||
if (unionType === null) {
|
||||
candidateType = null;
|
||||
break;
|
||||
} else {
|
||||
candidateType = unionType;
|
||||
}
|
||||
} // else same type, continue
|
||||
}
|
||||
|
||||
if (candidateType !== null) {
|
||||
this.unify(v, candidateType);
|
||||
// there's only one unique type and it's not a type var
|
||||
if (operands.size === 1 && kind !== 'Type') {
|
||||
this.unify(v, type.operands[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.occursCheck(v, type)) {
|
||||
const resolvedType = this.tryResolveType(v, type);
|
||||
if (resolvedType !== null) {
|
||||
this.substitutions.set(v.id, resolvedType);
|
||||
return;
|
||||
}
|
||||
throw new Error('cycle detected');
|
||||
}
|
||||
|
||||
this.substitutions.set(v.id, type);
|
||||
}
|
||||
|
||||
tryResolveType(v: TypeVar, type: Type): Type | null {
|
||||
switch (type.kind) {
|
||||
case 'Phi': {
|
||||
/**
|
||||
* Resolve the type of the phi by recursively removing `v` as an operand.
|
||||
* For example we can end up with types like this:
|
||||
*
|
||||
* v = Phi [
|
||||
* T1
|
||||
* T2
|
||||
* Phi [
|
||||
* T3
|
||||
* Phi [
|
||||
* T4
|
||||
* v <-- cycle!
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* By recursively removing `v`, we end up with:
|
||||
*
|
||||
* v = Phi [
|
||||
* T1
|
||||
* T2
|
||||
* Phi [
|
||||
* T3
|
||||
* Phi [
|
||||
* T4
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Which avoids the cycle
|
||||
*/
|
||||
const operands = [];
|
||||
for (const operand of type.operands) {
|
||||
if (operand.kind === 'Type' && operand.id === v.id) {
|
||||
continue;
|
||||
}
|
||||
const resolved = this.tryResolveType(v, operand);
|
||||
if (resolved === null) {
|
||||
return null;
|
||||
}
|
||||
operands.push(resolved);
|
||||
}
|
||||
return {kind: 'Phi', operands};
|
||||
}
|
||||
case 'Type': {
|
||||
const substitution = this.get(type);
|
||||
if (substitution !== type) {
|
||||
const resolved = this.tryResolveType(v, substitution);
|
||||
if (resolved !== null) {
|
||||
this.substitutions.set(type.id, resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
case 'Property': {
|
||||
const objectType = this.tryResolveType(v, this.get(type.objectType));
|
||||
if (objectType === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: 'Property',
|
||||
objectName: type.objectName,
|
||||
objectType,
|
||||
propertyName: type.propertyName,
|
||||
};
|
||||
}
|
||||
case 'Function': {
|
||||
const returnType = this.tryResolveType(v, this.get(type.return));
|
||||
if (returnType === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: 'Function',
|
||||
return: returnType,
|
||||
shapeId: type.shapeId,
|
||||
};
|
||||
}
|
||||
case 'ObjectMethod':
|
||||
case 'Object':
|
||||
case 'Primitive':
|
||||
case 'Poly': {
|
||||
return type;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(type, `Unexpected type kind '${(type as any).kind}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
occursCheck(v: TypeVar, type: Type): boolean {
|
||||
if (typeEquals(v, type)) return true;
|
||||
|
||||
@@ -673,39 +527,3 @@ const RefLikeNameRE = /^(?:[a-zA-Z$_][a-zA-Z$_0-9]*)Ref$|^ref$/;
|
||||
function isRefLikeName(t: PropType): boolean {
|
||||
return RefLikeNameRE.test(t.objectName) && t.propertyName === 'current';
|
||||
}
|
||||
|
||||
function tryUnionTypes(ty1: Type, ty2: Type): Type | null {
|
||||
let readonlyType: Type;
|
||||
let otherType: Type;
|
||||
if (ty1.kind === 'Object' && ty1.shapeId === BuiltInMixedReadonlyId) {
|
||||
readonlyType = ty1;
|
||||
otherType = ty2;
|
||||
} else if (ty2.kind === 'Object' && ty2.shapeId === BuiltInMixedReadonlyId) {
|
||||
readonlyType = ty2;
|
||||
otherType = ty1;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (otherType.kind === 'Primitive') {
|
||||
/**
|
||||
* Union(Primitive | MixedReadonly) = MixedReadonly
|
||||
*
|
||||
* For example, `data ?? null` could return `data`, the fact that RHS
|
||||
* is a primitive doesn't guarantee the result is a primitive.
|
||||
*/
|
||||
return readonlyType;
|
||||
} else if (
|
||||
otherType.kind === 'Object' &&
|
||||
otherType.shapeId === BuiltInArrayId
|
||||
) {
|
||||
/**
|
||||
* Union(Array | MixedReadonly) = Array
|
||||
*
|
||||
* In practice this pattern means the result is always an array. Given
|
||||
* that this behavior requires opting-in to the mixedreadonly type
|
||||
* (via moduleTypeProvider) this seems like a reasonable heuristic.
|
||||
*/
|
||||
return otherType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
|
||||
|
||||
/**
|
||||
* Temporary workaround for InferTypes not propagating the types of phis.
|
||||
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
|
||||
* the phi itself) with a single "canonical" identifier, generally chosen as the first
|
||||
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
|
||||
* sometimes be an operand from the earlier phi.
|
||||
*
|
||||
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
|
||||
* could end up replacing the phi Identifier with another identifer from an operand,
|
||||
* which _did_ have a type inferred.
|
||||
*
|
||||
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
|
||||
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
|
||||
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
|
||||
* will always invalidate with its input.
|
||||
*
|
||||
* The long-term correct approach is to update InferTypes to infer the types of phis,
|
||||
* but this is complicated because InferMutableRanges inadvertently depends on phis
|
||||
* never having a known type, such that a Store effect cannot occur on a phi value.
|
||||
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
|
||||
* to handle this case.
|
||||
*
|
||||
* As a temporary workaround, this pass propagates the type of phis and can be called
|
||||
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
|
||||
* type if all operands have the same type, it's its more correct.
|
||||
*/
|
||||
export function propagatePhiTypes(fn: HIRFunction): void {
|
||||
/**
|
||||
* We track which SSA ids have had their types propagated to handle nested ternaries,
|
||||
* see the StoreLocal handling below
|
||||
*/
|
||||
const propagated = new Set<IdentifierId>();
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
/*
|
||||
* We replicate the previous LeaveSSA behavior and only propagate types for
|
||||
* unnamed variables. LeaveSSA would have chosen one of the operands as the
|
||||
* canonical id and taken its type as the type of all identifiers. We're
|
||||
* more conservative and only propagate if the types are the same and the
|
||||
* phi didn't have a type inferred.
|
||||
*
|
||||
* Note that this can change output slightly in cases such as
|
||||
* `cond ? <div /> : null`.
|
||||
*
|
||||
* Previously the first operand's type (BuiltInJsx) would have been propagated,
|
||||
* and this expression may have been merged with subsequent reactive scopes
|
||||
* since it appears (based on that type) to always invalidate.
|
||||
*
|
||||
* But the correct type is `BuiltInJsx | null`, which we can't express and
|
||||
* so leave as a generic `Type`, which does not always invalidate and therefore
|
||||
* does not merge with subsequent scopes.
|
||||
*
|
||||
* We also don't propagate scopes for named variables, to preserve compatibility
|
||||
* with previous LeaveSSA behavior.
|
||||
*/
|
||||
if (phi.id.type.kind !== 'Type' || phi.id.name !== null) {
|
||||
continue;
|
||||
}
|
||||
let type: Type | null = null;
|
||||
for (const [, operand] of phi.operands) {
|
||||
if (type === null) {
|
||||
type = operand.type;
|
||||
} else if (!typeEquals(type, operand.type)) {
|
||||
type = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type !== null) {
|
||||
phi.id.type = type;
|
||||
propagated.add(phi.id.id);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
const {value} = instr;
|
||||
switch (value.kind) {
|
||||
case 'StoreLocal': {
|
||||
/**
|
||||
* Nested ternaries can lower to a form with an intermediate StoreLocal where
|
||||
* the value.lvalue is the temporary of the outer ternary, and the value.value
|
||||
* is the result of the inner ternary.
|
||||
*
|
||||
* This is a common pattern in practice and easy enough to support. Again, the
|
||||
* long-term approach is to update InferTypes and InferMutableRanges.
|
||||
*/
|
||||
const lvalue = value.lvalue.place;
|
||||
if (
|
||||
propagated.has(value.value.identifier.id) &&
|
||||
lvalue.identifier.type.kind === 'Type' &&
|
||||
lvalue.identifier.name === null
|
||||
) {
|
||||
lvalue.identifier.type = value.value.identifier.type;
|
||||
propagated.add(lvalue.identifier.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,76 +82,23 @@ export function getOrInsertDefault<U, V>(
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
export function Set_equal<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const item of a) {
|
||||
if (!b.has(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function Set_union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
|
||||
const union = new Set<T>(a);
|
||||
for (const item of b) {
|
||||
union.add(item);
|
||||
export function Set_union<T>(a: Set<T>, b: Set<T>): Set<T> {
|
||||
const union = new Set<T>();
|
||||
for (const item of a) {
|
||||
if (b.has(item)) {
|
||||
union.add(item);
|
||||
}
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
|
||||
if (sets.length === 0 || sets.some(s => s.size === 0)) {
|
||||
return new Set();
|
||||
} else if (sets.length === 1) {
|
||||
return new Set(sets[0]);
|
||||
}
|
||||
const result: Set<T> = new Set();
|
||||
const first = sets[0];
|
||||
outer: for (const e of first) {
|
||||
for (let i = 1; i < sets.length; i++) {
|
||||
if (!sets[i].has(e)) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
result.add(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function Iterable_some<T>(
|
||||
iter: Iterable<T>,
|
||||
pred: (item: T) => boolean,
|
||||
): boolean {
|
||||
for (const item of iter) {
|
||||
if (pred(item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function nonNull<T extends NonNullable<U>, U>(
|
||||
value: T | null | undefined,
|
||||
): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function Set_filter<T>(
|
||||
source: ReadonlySet<T>,
|
||||
fn: (arg: T) => boolean,
|
||||
): Set<T> {
|
||||
const result = new Set<T>();
|
||||
for (const entry of source) {
|
||||
if (fn(entry)) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function hasNode<T>(
|
||||
input: NodePath<T | null | undefined>,
|
||||
): input is NodePath<NonNullable<T>> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user