Compare commits

..

1 Commits

Author SHA1 Message Date
Joe Savona
e1f907d68d [compiler] Allow assigning ref-accessing functions to objects if not mutated
Allows assigning a ref-accessing function to an object so long as that object is not subsequently transitively mutated. We should likely rewrite the ref validation to use the new mutation/aliasing effects, which would provide a more consistent behavior across instruction types and require fewer special cases like this.
2025-07-29 10:57:08 -07:00
1425 changed files with 21551 additions and 73781 deletions

View File

@@ -1,7 +1,7 @@
{
"packages": ["packages/react", "packages/react-dom", "packages/react-server-dom-webpack", "packages/scheduler"],
"buildCommand": "download-build-in-codesandbox-ci",
"node": "20",
"node": "18",
"publishDirectory": {
"react": "build/oss-experimental/react",
"react-dom": "build/oss-experimental/react-dom",

View File

@@ -28,6 +28,3 @@ packages/react-devtools-shared/src/hooks/__tests__/__source__/__untransformed__/
packages/react-devtools-shell/dist
packages/react-devtools-timeline/dist
packages/react-devtools-timeline/static
# Imported third-party Flow types
flow-typed/

View File

@@ -468,7 +468,6 @@ module.exports = {
files: ['packages/react-server-dom-webpack/**/*.js'],
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_get_script_filename__: 'readonly',
__webpack_require__: 'readonly',
},
},
@@ -517,14 +516,6 @@ module.exports = {
__IS_INTERNAL_VERSION__: 'readonly',
},
},
{
files: ['packages/react-devtools-*/**/*.js'],
excludedFiles: '**/__tests__/**/*.js',
plugins: ['eslint-plugin-react-hooks-published'],
rules: {
'react-hooks-published/rules-of-hooks': ERROR,
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
@@ -555,10 +546,13 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',
$Keys: 'readonly',
$NonMaybeType: 'readonly',
$PropertyType: 'readonly',
$ReadOnly: 'readonly',
$ReadOnlyArray: 'readonly',
$ArrayBufferView: 'readonly',
@@ -573,7 +567,6 @@ module.exports = {
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -585,19 +578,16 @@ module.exports = {
$AsyncIterator: 'readonly',
Iterator: 'readonly',
AsyncIterator: 'readonly',
IntervalID: 'readonly',
IteratorResult: 'readonly',
JSONValue: 'readonly',
JSResourceReference: 'readonly',
mixin$Animatable: 'readonly',
MouseEventHandler: 'readonly',
NavigateEvent: 'readonly',
PerformanceMeasureOptions: 'readonly',
PropagationPhases: 'readonly',
PropertyDescriptor: 'readonly',
PropertyDescriptorMap: 'readonly',
Proxy$traps: 'readonly',
React$AbstractComponent: 'readonly',
React$Component: 'readonly',
React$ComponentType: 'readonly',
React$Config: 'readonly',
React$Context: 'readonly',
React$Element: 'readonly',
@@ -629,6 +619,7 @@ module.exports = {
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',

View File

@@ -11,7 +11,7 @@ body:
options:
- label: React Compiler core (the JS output is incorrect, or your app works incorrectly after optimization)
- label: babel-plugin-react-compiler (build issue installing or using the Babel plugin)
- label: eslint-plugin-react-hooks (build issue installing or using the eslint plugin)
- label: eslint-plugin-react-compiler (build issue installing or using the eslint plugin)
- label: react-compiler-healthcheck (build issue installing or using the healthcheck script)
- type: input
attributes:

View File

@@ -57,6 +57,8 @@ jobs:
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- run: npx playwright install --with-deps chromium
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
- run: npx playwright install-deps
if: steps.cache_playwright_browsers.outputs.cache-hit == 'true'
- run: CI=true yarn test
- run: ls -R test-results
if: '!cancelled()'

View File

@@ -19,9 +19,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
secrets:
NPM_TOKEN:
required: true
@@ -58,13 +55,7 @@ jobs:
key: compiler-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('compiler/yarn.lock') }}
- run: yarn install --frozen-lockfile
if: steps.node_modules.outputs.cache-hit != 'true'
- if: inputs.dry_run == true
name: Publish packages to npm (dry run)
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --debug --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}
- if: inputs.dry_run != true
name: Publish packages to npm
- name: Publish packages to npm
run: |
cp ./scripts/release/ci-npmrc ~/.npmrc
scripts/release/publish.js --frfr --ci --versionName=${{ inputs.version_name }} --tag=${{ inputs.dist_tag }} ${{ inputs.tag_version && format('--tagVersion={0}', inputs.tag_version) || '' }}

View File

@@ -17,9 +17,6 @@ on:
tag_version:
required: false
type: string
dry_run:
required: false
type: boolean
permissions: {}
@@ -36,6 +33,5 @@ jobs:
dist_tag: ${{ inputs.dist_tag }}
version_name: ${{ inputs.version_name }}
tag_version: ${{ inputs.tag_version }}
dry_run: ${{ inputs.dry_run }}
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -19,6 +19,5 @@ jobs:
release_channel: experimental
dist_tag: experimental
version_name: '0.0.0'
dry_run: false
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,49 +0,0 @@
name: (DevTools) Discord Notify
on:
pull_request_target:
types: [opened, ready_for_review]
paths:
- packages/react-devtools**
- .github/workflows/devtools_**.yml
permissions: {}
jobs:
check_access:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
outputs:
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
steps:
- run: echo ${{ github.event.pull_request.author_association }}
- name: Check is member or collaborator
id: check_is_member_or_collaborator
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT"
check_maintainer:
if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }}
needs: [check_access]
uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main
permissions:
# Used by check_maintainer
contents: read
with:
actor: ${{ github.event.pull_request.user.login }}
notify:
if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }}
needs: check_maintainer
runs-on: ubuntu-latest
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
with:
webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }}
embed-author-name: ${{ github.event.pull_request.user.login }}
embed-author-url: ${{ github.event.pull_request.user.html_url }}
embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }}
embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}'
embed-description: ${{ github.event.pull_request.body }}
embed-url: ${{ github.event.pull_request.html_url }}

View File

@@ -92,7 +92,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: react-devtools
path: build/devtools
path: build/devtools.tgz
if-no-files-found: error
# Simplifies getting the extension for local testing
- name: Archive chrome extension
@@ -201,5 +201,5 @@ jobs:
- uses: actions/upload-artifact@v4
with:
name: screenshots
path: ./tmp/playwright-artifacts
path: ./tmp/screenshots
if-no-files-found: warn

View File

@@ -194,7 +194,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: |
yarn generate-inline-fizz-runtime
git diff --exit-code || (echo "There was a change to the Fizz runtime. Run \`yarn generate-inline-fizz-runtime\` and check in the result." && false)
git diff --quiet || (echo "There was a change to the Fizz runtime. Run `yarn generate-inline-fizz-runtime` and check in the result." && false)
# ----- FEATURE FLAGS -----
flags:
@@ -316,7 +316,7 @@ jobs:
if: steps.node_modules.outputs.cache-hit != 'true'
- run: ./scripts/react-compiler/build-compiler.sh && ./scripts/react-compiler/link-compiler.sh
- run: yarn workspace eslint-plugin-react-hooks test
# ----- BUILD -----
build_and_lint:
name: yarn build and lint
@@ -567,7 +567,7 @@ jobs:
- name: Search build artifacts for unminified errors
run: |
yarn extract-errors
git diff --exit-code || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
git diff --quiet || (echo "Found unminified errors. Either update the error codes map or disable error minification for the affected build, if appropriate." && false)
check_release_dependencies:
name: Check release dependencies
@@ -766,11 +766,6 @@ jobs:
name: react-devtools-${{ matrix.browser }}-extension
path: build/devtools/${{ matrix.browser }}-extension.zip
if-no-files-found: error
- name: Archive ${{ matrix.browser }} metadata
uses: actions/upload-artifact@v4
with:
name: react-devtools-${{ matrix.browser }}-metadata
path: build/devtools/webpack-stats.*.json
merge_devtools_artifacts:
name: Merge DevTools artifacts
@@ -781,7 +776,7 @@ jobs:
uses: actions/upload-artifact/merge@v4
with:
name: react-devtools
pattern: react-devtools-*
pattern: react-devtools-*-extension
run_devtools_e2e_tests:
name: Run DevTools e2e tests
@@ -816,27 +811,12 @@ jobs:
pattern: _build_*
path: build
merge-multiple: true
- name: Check Playwright version
id: playwright_version
run: echo "playwright_version=$(npm ls @playwright/test | grep @playwright | sed 's/.*@//' | head -1)" >> "$GITHUB_OUTPUT"
- name: Cache Playwright Browsers for version ${{ steps.playwright_version.outputs.playwright_version }}
id: cache_playwright_browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-v6-${{ runner.arch }}-${{ runner.os }}-${{ steps.playwright_version.outputs.playwright_version }}
- name: Playwright install deps
if: steps.cache_playwright_browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- run: |
npx playwright install
sudo npx playwright install-deps
- run: ./scripts/ci/run_devtools_e2e_tests.js
env:
RELEASE_CHANNEL: experimental
- name: Archive Playwright report
uses: actions/upload-artifact@v4
with:
name: devtools-playwright-artifacts
path: tmp/playwright-artifacts
if-no-files-found: warn
# ----- SIZEBOT -----
sizebot:

View File

@@ -162,13 +162,10 @@ jobs:
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/
# Delete the OSS renderers, these are sync'd to RN separately.
# Delete OSS renderer. OSS renderer is synced through internal script.
RENDERER_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/implementations/
rm $RENDERER_FOLDER/ReactFabric-{dev,prod,profiling}.js
# Delete the legacy renderer shim, this is not sync'd and will get deleted in the future.
SHIM_FOLDER=$BASE_FOLDER/react-native-github/Libraries/Renderer/shims/
rm $SHIM_FOLDER/ReactNative.js
rm $RENDERER_FOLDER/ReactNativeRenderer-{dev,prod,profiling}.js
# Copy eslint-plugin-react-hooks
# NOTE: This is different from www, here we include the full package

View File

@@ -4,10 +4,8 @@ on:
pull_request_target:
types: [opened, ready_for_review]
paths-ignore:
- packages/react-devtools**
- compiler/**
- .github/workflows/compiler_**.yml
- .github/workflows/devtools**.yml
permissions: {}

View File

@@ -82,6 +82,7 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
@@ -90,10 +91,11 @@ jobs:
run: |
scripts/release/publish.js \
--ci \
--skipTests \
--tags=${{ inputs.dist_tag }} \
--skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
${{ inputs.dry && '--dry' || '' }}
- if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
- if: '${{ !(inputs.skip_packages && inputs.only_packages) }}'
name: 'Publish all packages'
run: |
scripts/release/publish.js \

View File

@@ -18,7 +18,6 @@ jobs:
permissions:
# Used to create a review and close PRs
pull-requests: write
contents: write
steps:
- name: Close PR
uses: actions/github-script@v7

1
.gitignore vendored
View File

@@ -23,7 +23,6 @@ chrome-user-data
.vscode
*.swp
*.swo
/tmp
packages/react-devtools-core/dist
packages/react-devtools-extensions/chrome/build

View File

@@ -1,76 +1,3 @@
## 19.2.0 (October 1st, 2025)
Below is a list of all new features, APIs, and bug fixes.
Read the [React 19.2 release post](https://react.dev/blog/2025/10/01/react-19-2) for more information.
### New React Features
- [`<Activity>`](https://react.dev/reference/react/Activity): A new API to hide and restore the UI and internal state of its children.
- [`useEffectEvent`](https://react.dev/reference/react/useEffectEvent) is a React Hook that lets you extract non-reactive logic into an [Effect Event](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event).
- [`cacheSignal`](https://react.dev/reference/react/cacheSignal) (for RSCs) lets your know when the `cache()` lifetime is over.
- [React Performance tracks](https://react.dev/reference/dev-tools/react-performance-tracks) appear on the Performance panels timeline in your browser developer tools
### New React DOM Features
- Added resume APIs for partial pre-rendering with Web Streams:
- [`resume`](https://react.dev/reference/react-dom/server/resume): to resume a prerender to a stream.
- [`resumeAndPrerender`](https://react.dev/reference/react-dom/static/resumeAndPrerender): to resume a prerender to HTML.
- Added resume APIs for partial pre-rendering with Node Streams:
- [`resumeToPipeableStream`](https://react.dev/reference/react-dom/server/resumeToPipeableStream): to resume a prerender to a stream.
- [`resumeAndPrerenderToNodeStream`](https://react.dev/reference/react-dom/static/resumeAndPrerenderToNodeStream): to resume a prerender to HTML.
- Updated [`prerender`](https://react.dev/reference/react-dom/static/prerender) APIs to return a `postponed` state that can be passed to the `resume` APIs.
### Notable changes
- React DOM now batches suspense boundary reveals, matching the behavior of client side rendering. This change is especially noticeable when animating the reveal of Suspense boundaries e.g. with the upcoming `<ViewTransition>` Component. React will batch as much reveals as possible before the first paint while trying to hit popular first-contentful paint metrics.
- Add Node Web Streams (`prerender`, `renderToReadableStream`) to server-side-rendering APIs for Node.js
- Use underscore instead of `:` IDs generated by useId
### All Changes
#### React
- `<Activity />` was developed over many years, starting before `ClassComponent.setState` (@acdlite @sebmarkbage and many others)
- Stringify context as "SomeContext" instead of "SomeContext.Provider" (@kassens [#33507](https://github.com/facebook/react/pull/33507))
- Include stack of cause of React instrumentation errors with `%o` placeholder (@eps1lon [#34198](https://github.com/facebook/react/pull/34198))
- Fix infinite `useDeferredValue` loop in popstate event (@acdlite [#32821](https://github.com/facebook/react/pull/32821))
- Fix a bug when an initial value was passed to `useDeferredValue` (@acdlite [#34376](https://github.com/facebook/react/pull/34376))
- Fix a crash when submitting forms with Client Actions (@sebmarkbage [#33055](https://github.com/facebook/react/pull/33055))
- Hide/unhide the content of dehydrated suspense boundaries if they resuspend (@sebmarkbage [#32900](https://github.com/facebook/react/pull/32900))
- Avoid stack overflow on wide trees during Hot Reload (@sophiebits [#34145](https://github.com/facebook/react/pull/34145))
- Improve Owner and Component stacks in various places (@sebmarkbage, @eps1lon: [#33629](https://github.com/facebook/react/pull/33629), [#33724](https://github.com/facebook/react/pull/33724), [#32735](https://github.com/facebook/react/pull/32735), [#33723](https://github.com/facebook/react/pull/33723))
- Add `cacheSignal` (@sebmarkbage [#33557](https://github.com/facebook/react/pull/33557))
#### React DOM
- Block on Suspensey Fonts during reveal of server-side-rendered content (@sebmarkbage [#33342](https://github.com/facebook/react/pull/33342))
- Use underscore instead of `:` for IDs generated by `useId` (@sebmarkbage, @eps1lon: [#32001](https://github.com/facebook/react/pull/32001), [https://github.com/facebook/react/pull/33342](https://github.com/facebook/react/pull/33342)[#33099](https://github.com/facebook/react/pull/33099), [#33422](https://github.com/facebook/react/pull/33422))
- Stop warning when ARIA 1.3 attributes are used (@Abdul-Omira [#34264](https://github.com/facebook/react/pull/34264))
- Allow `nonce` to be used on hoistable styles (@Andarist [#32461](https://github.com/facebook/react/pull/32461))
- Warn for using a React owned node as a Container if it also has text content (@sebmarkbage [#32774](https://github.com/facebook/react/pull/32774))
- s/HTML/text for for error messages if text hydration mismatches (@rickhanlonii [#32763](https://github.com/facebook/react/pull/32763))
- Fix a bug with `React.use` inside `React.lazy`\-ed Component (@hi-ogawa [#33941](https://github.com/facebook/react/pull/33941))
- Enable the `progressiveChunkSize` option for server-side-rendering APIs (@sebmarkbage [#33027](https://github.com/facebook/react/pull/33027))
- Fix a bug with deeply nested Suspense inside Suspense fallback when server-side-rendering (@gnoff [#33467](https://github.com/facebook/react/pull/33467))
- Avoid hanging when suspending after aborting while rendering (@gnoff [#34192](https://github.com/facebook/react/pull/34192))
- Add Node Web Streams to server-side-rendering APIs for Node.js (@sebmarkbage [#33475](https://github.com/facebook/react/pull/33475))
#### React Server Components
- Preload `<img>` and `<link>` using hints before they're rendered (@sebmarkbage [#34604](https://github.com/facebook/react/pull/34604))
- Log error if production elements are rendered during development (@eps1lon [#34189](https://github.com/facebook/react/pull/34189))
- Fix a bug when returning a Temporary reference (e.g. a Client Reference) from Server Functions (@sebmarkbage [#34084](https://github.com/facebook/react/pull/34084), @denk0403 [#33761](https://github.com/facebook/react/pull/33761))
- Pass line/column to `filterStackFrame` (@eps1lon [#33707](https://github.com/facebook/react/pull/33707))
- Support Async Modules in Turbopack Server References (@lubieowoce [#34531](https://github.com/facebook/react/pull/34531))
- Add support for .mjs file extension in Webpack (@jennyscript [#33028](https://github.com/facebook/react/pull/33028))
- Fix a wrong missing key warning (@unstubbable [#34350](https://github.com/facebook/react/pull/34350))
- Make console log resolve in predictable order (@sebmarkbage [#33665](https://github.com/facebook/react/pull/33665))
#### React Reconciler
- [createContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L255-L261) and [createHydrationContainer](https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiberReconciler.js#L305-L312) had their parameter order adjusted after `on*` handlers to account for upcoming experimental APIs
## 19.1.1 (July 28, 2025)
### React

View File

@@ -1,6 +1,5 @@
acdlite
eps1lon
EugeneChoi4
gaearon
gnoff
unstubbable

View File

@@ -7,18 +7,18 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 19.3.0
// 19.1.0
//
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 19.3.0-canary-a1c2d3e4
// 19.1.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.3.0';
const ReactVersion = '19.2.0';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -33,8 +33,8 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '7.1.0',
'jest-react': '0.18.0',
'eslint-plugin-react-hooks': '6.1.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,
@@ -42,12 +42,12 @@ const stablePackages = {
'react-server-dom-turbopack': ReactVersion,
'react-server-dom-parcel': ReactVersion,
'react-is': ReactVersion,
'react-reconciler': '0.34.0',
'react-refresh': '0.19.0',
'react-reconciler': '0.33.0',
'react-refresh': '0.18.0',
'react-test-renderer': ReactVersion,
'use-subscription': '1.13.0',
'use-sync-external-store': '1.7.0',
scheduler: '0.28.0',
'use-subscription': '1.12.0',
'use-sync-external-store': '1.6.0',
scheduler: '0.27.0',
};
// These packages do not exist in the @canary or @latest channel, only

View File

@@ -8,7 +8,6 @@ module.exports = {
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-transform-class-properties', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
'@babel/plugin-transform-classes',
],
presets: [

View File

@@ -1,4 +1,5 @@
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all"
import { c as _c } from "react/compiler-runtime"; // 
@compilationMode:"all"
function nonReactFn() {
  const $ = _c(1);
  let t0;

View File

@@ -1,5 +0,0 @@
import type { PluginOptions } from 
'babel-plugin-react-compiler/dist';
({
  //compilationMode: "all"
} satisfies PluginOptions);

View File

@@ -1,14 +0,0 @@
import { c as _c } from "react/compiler-runtime";
export default function TestComponent(t0) {
const $ = _c(2);
const { x } = t0;
let t1;
if ($[0] !== x || true) {
t1 = <Button>{x}</Button>;
$[0] = x;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}

View File

@@ -5,9 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
import {expect, test, type Page} from '@playwright/test';
import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
import {defaultConfig} from '../../lib/defaultStore';
import {format} from 'prettier';
function isMonacoLoaded(): boolean {
@@ -21,16 +20,6 @@ function formatPrint(data: Array<string>): Promise<string> {
return format(data.join(''), {parser: 'babel'});
}
async function expandConfigs(page: Page): Promise<void> {
const expandButton = page.locator('[title="Expand config editor"]');
await expandButton.click();
await page.waitForSelector('.monaco-editor-config', {state: 'visible'});
}
const TEST_SOURCE = `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}`;
const TEST_CASE_INPUTS = [
{
name: 'module-scope-use-memo',
@@ -132,9 +121,10 @@ test('editor should open successfully', async ({page}) => {
test('editor should compile from hash successfully', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -146,7 +136,7 @@ test('editor should compile from hash successfully', async ({page}) => {
path: 'test-results/01-compiles-from-hash.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
@@ -155,9 +145,10 @@ test('editor should compile from hash successfully', async ({page}) => {
test('reset button works', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: defaultConfig,
showInternals: false,
source: `export default function TestComponent({ x }) {
return <Button>{x}</Button>;
}
`,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
@@ -166,201 +157,33 @@ test('reset button works', async ({page}) => {
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/02-reset-button-works.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
const output = await formatPrint(text);
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('02-default-output.txt');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
});
test('defaults load when only source is in Store', async ({page}) => {
// Test for backwards compatibility
const partial = {
source: TEST_SOURCE,
};
const hash = encodeStore(partial as Store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/03-missing-defaults.png',
});
// Config editor has default config
const configText =
(await page.locator('.monaco-editor-config').allInnerTexts()) ?? [];
const configOutput = configText.join('');
expect(configOutput).not.toEqual('');
expect(configOutput).toMatchSnapshot('default-config.txt');
const checkbox = page.locator('label.show-internals');
await expect(checkbox).not.toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).not.toBeVisible();
});
test('show internals button toggles correctly', async ({page}) => {
await page.goto(`/`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
// show internals should be off
const checkbox = page.locator('label.show-internals');
await checkbox.click();
await page.screenshot({
fullPage: true,
path: 'test-results/04-show-internals-on.png',
});
await expect(checkbox).toBeChecked();
const ssaTab = page.locator('text=SSA');
await expect(ssaTab).toBeVisible();
});
test('error is displayed when config has syntax error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `compilationMode: `,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/05-config-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
// Remove hidden chars
expect(output.replace(/\s+/g, ' ')).toContain('Invalid override format');
});
test('error is displayed when config has validation error', async ({page}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
compilationMode: "123"
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/06-config-validation-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain('Unexpected compilationMode');
});
test('disableMemoizationForDebugging flag works as expected', async ({
page,
}) => {
const store: Store = {
source: TEST_SOURCE,
config: `import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
environment: {
disableMemoizationForDebugging: true
}
} satisfies PluginOptions);`,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/07-config-disableMemoizationForDebugging-flag.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = await formatPrint(text);
expect(output).not.toEqual('');
expect(output).toMatchSnapshot('disableMemoizationForDebugging-output.txt');
});
test('error is displayed when source has syntax error', async ({page}) => {
const syntaxErrorSource = `function TestComponent(props) {
const oops = props.
return (
<>{oops}</>
);
}`;
const store: Store = {
source: syntaxErrorSource,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`);
await page.waitForFunction(isMonacoLoaded);
await expandConfigs(page);
await page.screenshot({
fullPage: true,
path: 'test-results/08-source-syntax-error.png',
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
const output = text.join('');
expect(output.replace(/\s+/g, ' ')).toContain(
'Expected identifier to be defined before being used',
);
});
TEST_CASE_INPUTS.forEach((t, idx) =>
test(`playground compiles: ${t.name}`, async ({page}) => {
const store: Store = {
source: t.input,
config: defaultConfig,
showInternals: false,
};
const hash = encodeStore(store);
await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
await page.waitForFunction(isMonacoLoaded);
await page.screenshot({
fullPage: true,
path: `test-results/08-0${idx}-${t.name}.png`,
path: `test-results/03-0${idx}-${t.name}.png`,
});
const text =
(await page.locator('.monaco-editor-output').allInnerTexts()) ?? [];
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
let output: string;
if (t.noFormat) {
output = text.join('');

View File

@@ -0,0 +1,56 @@
/**
* 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 type {NextPage} from 'next';
import Head from 'next/head';
import {SnackbarProvider} from 'notistack';
import {Editor, Header, StoreProvider} from '../components';
import MessageSnackbar from '../components/Message';
const Home: NextPage = () => {
return (
<div className="flex flex-col w-screen h-screen font-light">
<Head>
<title>
{process.env.NODE_ENV === 'development'
? '[DEV] React Compiler Playground'
: 'React Compiler Playground'}
</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="manifest" href="/site.webmanifest" />
<link
rel="preload"
href="/fonts/Source-Code-Pro-Regular.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/fonts/Optimistic_Display_W_Lt.woff2"
as="font"
type="font/woff2"
crossOrigin="anonymous"
/>
</Head>
<StoreProvider>
<SnackbarProvider
preventDuplicate
maxSnack={10}
Components={{message: MessageSnackbar}}>
<Header />
<Editor />
</SnackbarProvider>
</StoreProvider>
</div>
);
};
export default Home;

View File

@@ -1,126 +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 {Resizable} from 're-resizable';
import React, {
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
type TabsRecord = Map<string, React.ReactNode>;
export default function AccordionWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-row h-full">
{Array.from(props.tabs.keys()).map(name => {
return (
<AccordionWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
</div>
);
}
function AccordionWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
isFailure: boolean;
}): React.ReactElement {
const id = useId();
const isShow = tabsOpen.has(name);
const transitionName = `accordion-window-item-${id}`;
const toggleTabs = (): void => {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
setTabsOpen(nextState);
});
};
// Replace spaces with non-breaking spaces
const displayName = name.replace(/ /g, '\u00A0');
return (
<div key={name} className="flex flex-row">
{isShow ? (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
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}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
</ViewTransition>
) : (
<ViewTransition
name={transitionName}
update={{
[EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
default: 'none',
}}>
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
</ViewTransition>
)}
</div>
);
}

View File

@@ -1,210 +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 MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import React, {
useState,
useRef,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
Activity,
} from 'react';
import {Resizable} from 're-resizable';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoConfigOptions} from './monacoOptions';
import {IconChevron} from '../Icons/IconChevron';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error - webpack asset/source loader handles .d.ts files as strings
import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts';
loader.config({monaco});
export default function ConfigEditor({
formattedAppliedConfig,
}: {
formattedAppliedConfig: string;
}): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
return (
<>
<Activity mode={isExpanded ? 'visible' : 'hidden'}>
<ExpandedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(false);
});
}}
formattedAppliedConfig={formattedAppliedConfig}
/>
</Activity>
<Activity mode={isExpanded ? 'hidden' : 'visible'}>
<CollapsedEditor
onToggle={() => {
startTransition(() => {
addTransitionType(CONFIG_PANEL_TRANSITION);
setIsExpanded(true);
});
}}
/>
</Activity>
</>
);
}
function ExpandedEditor({
onToggle,
formattedAppliedConfig,
}: {
onToggle: (expanded: boolean) => void;
formattedAppliedConfig: string;
}): React.ReactElement {
const store = useStore();
const dispatchStore = useStoreDispatch();
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const handleChange: (value: string | undefined) => void = (
value: string | undefined,
) => {
if (value === undefined) return;
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
debounceTimerRef.current = setTimeout(() => {
dispatchStore({
type: 'updateConfig',
payload: {
config: value,
},
});
}, 500); // 500ms debounce delay
};
const handleMount: (
_: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => void = (_, monaco) => {
// Add the babel-plugin-react-compiler type definitions to Monaco
monaco.languages.typescript.typescriptDefaults.addExtraLib(
//@ts-expect-error - compilerTypeDefs is a string
compilerTypeDefs,
'file:///node_modules/babel-plugin-react-compiler/dist/index.d.ts',
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.Latest,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
strict: false,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
jsx: monaco.languages.typescript.JsxEmit.React,
});
};
return (
<ViewTransition
enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}>
<Resizable
minWidth={300}
maxWidth={600}
defaultSize={{width: 350}}
enable={{right: true, bottom: false}}>
<div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
<div
className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
title="Minimize config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
right: '-32px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="left" className="text-blue-50" />
</div>
<div className="flex-1 flex flex-col m-2 mb-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Config Overrides
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
loading={''}
className="monaco-editor-config"
options={monacoConfigOptions}
/>
</div>
</div>
<div className="flex-1 flex flex-col m-2">
<div className="pb-2">
<h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
Applied Configs
</h2>
</div>
<div className="flex-1 border border-gray-300">
<MonacoEditor
path={'applied-config.js'}
language={'javascript'}
value={formattedAppliedConfig}
loading={''}
className="monaco-editor-applied-config"
options={{
...monacoConfigOptions,
readOnly: true,
}}
/>
</div>
</div>
</div>
</Resizable>
</ViewTransition>
);
}
function CollapsedEditor({
onToggle,
}: {
onToggle: () => void;
}): React.ReactElement {
return (
<div
className="w-4 !h-[calc(100vh_-_3.5rem)]"
style={{position: 'relative'}}>
<div
className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
title="Expand config editor"
onClick={onToggle}
style={{
top: '50%',
marginTop: '-32px',
left: '-8px',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}>
<IconChevron displayDirection="right" className="text-blue-50" />
</div>
</div>
);
}

View File

@@ -5,30 +5,313 @@
* LICENSE file in the root directory of this source tree.
*/
import {
import {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorSeverity,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
} from 'babel-plugin-react-compiler';
import {useDeferredValue, useMemo, useState} from 'react';
import {useStore} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import clsx from 'clsx';
import invariant from 'invariant';
import {useSnackbar} from 'notistack';
import {useDeferredValue, useMemo} from 'react';
import {useMountEffect} from '../../hooks';
import {defaultStore} from '../../lib/defaultStore';
import {
createMessage,
initStoreFromUrlOrLocalStorage,
MessageLevel,
MessageSource,
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import Input from './Input';
import {CompilerOutput, default as Output} from './Output';
import {compile} from '../../lib/compilation';
import prettyFormat from 'pretty-format';
import {
CompilerOutput,
CompilerTransformOutput,
default as Output,
PrintedCompilerPipelineValue,
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function compile(
source: string,
mode: 'compiler' | 'linter',
): [CompilerOutput, 'flow' | 'typescript'] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
try {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
const parsedOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
const opts: PluginOptions = parsePluginOptions({
...parsedOptions,
environment: {
...parsedOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent) => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
});
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* 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.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
console.error(err);
error.details.push(
new CompilerErrorDetail({
severity: ErrorSeverity.Invariant,
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
}),
);
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors()) {
return [{kind: 'err', results, error}, language];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
];
}
export default function Editor(): JSX.Element {
const store = useStore();
const deferredStore = useDeferredValue(store);
const [compilerOutput, language, appliedOptions] = useMemo(
() => compile(deferredStore.source, 'compiler', deferredStore.config),
[deferredStore.source, deferredStore.config],
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const [compilerOutput, language] = useMemo(
() => compile(deferredStore.source, 'compiler'),
[deferredStore.source],
);
const [linterOutput] = useMemo(
() => compile(deferredStore.source, 'linter', deferredStore.config),
[deferredStore.source, deferredStore.config],
() => compile(deferredStore.source, 'linter'),
[deferredStore.source],
);
const [formattedAppliedConfig, setFormattedAppliedConfig] = useState('');
useMountEffect(() => {
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
invariant(e instanceof Error, 'Only Error may be caught.');
enqueueSnackbar(e.message, {
variant: 'warning',
...createMessage(
'Bad URL - fell back to the default Playground.',
MessageLevel.Info,
MessageSource.Playground,
),
});
mountStore = defaultStore;
}
dispatchStore({
type: 'setStore',
payload: {store: mountStore},
});
});
let mergedOutput: CompilerOutput;
let errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
@@ -42,25 +325,13 @@ export default function Editor(): JSX.Element {
mergedOutput = compilerOutput;
errors = compilerOutput.error.details;
}
if (appliedOptions) {
const formatted = prettyFormat(appliedOptions, {
printFunctionName: false,
printBasicPrototype: false,
});
if (formatted !== formattedAppliedConfig) {
setFormattedAppliedConfig(formatted);
}
}
return (
<>
<div className="relative flex top-14">
<div className="flex-shrink-0">
<ConfigEditor formattedAppliedConfig={formattedAppliedConfig} />
</div>
<div className="flex flex-1 min-w-0">
<div className="relative flex basis top-14">
<div className={clsx('relative sm:basis-1/4')}>
<Input language={language} errors={errors} />
</div>
<div className={clsx('flex sm:flex flex-wrap')}>
<Output store={deferredStore} compilerOutput={mergedOutput} />
</div>
</div>

View File

@@ -6,31 +6,22 @@
*/
import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
import {
CompilerErrorDetail,
CompilerDiagnostic,
} from 'babel-plugin-react-compiler';
import {CompilerErrorDetail} from 'babel-plugin-react-compiler';
import invariant from 'invariant';
import type {editor} from 'monaco-editor';
import * as monaco from 'monaco-editor';
import {
useEffect,
useState,
unstable_ViewTransition as ViewTransition,
} from 'react';
import {Resizable} from 're-resizable';
import {useEffect, useState} from 'react';
import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics';
import {useStore, useStoreDispatch} from '../StoreContext';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
loader.config({monaco});
type Props = {
errors: Array<CompilerErrorDetail | CompilerDiagnostic>;
errors: Array<CompilerErrorDetail>;
language: 'flow' | 'typescript';
};
@@ -51,6 +42,11 @@ export default function Input({errors, language}: Props): JSX.Element {
details: errors,
source: store.source,
});
/**
* 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, store.source]);
useEffect(() => {
@@ -83,11 +79,11 @@ export default function Input({errors, language}: Props): JSX.Element {
});
}, [monaco, language]);
const handleChange: (value: string | undefined) => void = async value => {
const handleChange: (value: string | undefined) => void = value => {
if (!value) return;
dispatchStore({
type: 'updateSource',
type: 'updateFile',
payload: {
source: value,
},
@@ -139,42 +135,30 @@ export default function Input({errors, language}: Props): JSX.Element {
});
};
const editorContent = (
<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.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
className="monaco-editor-input"
options={monacoOptions}
loading={''}
/>
);
const tabs = new Map([['Input', editorContent]]);
const [activeTab, setActiveTab] = useState('Input');
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
default: 'none',
}}>
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full !h-[calc(100vh_-_3.5rem)] border-r border-gray-200">
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</div>
</div>
</ViewTransition>
<div className="relative flex flex-col flex-none border-r border-gray-200">
<Resizable
minWidth={650}
enable={{right: true}}
/**
* 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.
*/
language={'javascript'}
value={store.source}
onMount={handleMount}
onChange={handleChange}
options={monacoOptions}
/>
</Resizable>
</div>
);
}

View File

@@ -19,38 +19,15 @@ import {
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 {type Store} from '../../lib/stores';
import {
memo,
ReactNode,
use,
useState,
Suspense,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
startTransition,
} from 'react';
import AccordionWindow from '../AccordionWindow';
import TabbedWindow from '../TabbedWindow';
import {monacoOptions} from './monacoOptions';
import {BabelFileResult} from '@babel/core';
import {
CONFIG_PANEL_TRANSITION,
TOGGLE_INTERNALS_TRANSITION,
EXPAND_ACCORDION_TRANSITION,
} from '../../lib/transitionTypes';
import {LRUCache} from 'lru-cache';
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
max: 5,
});
export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
@@ -87,16 +64,12 @@ type Props = {
async function tabify(
source: string,
compilerOutput: CompilerOutput,
showInternals: boolean,
): Promise<Map<string, ReactNode>> {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) {
continue;
}
for (const result of results) {
switch (result.kind) {
case 'hir': {
@@ -219,25 +192,6 @@ ${code}
return reorderedTabs;
}
function tabifyCached(
store: Store,
compilerOutput: CompilerOutput,
): Promise<Map<string, ReactNode>> {
const cached = tabifyCache.get(store);
if (cached) return cached;
const result = tabify(store.source, compilerOutput, store.showInternals);
tabifyCache.set(store, result);
return result;
}
function Fallback(): JSX.Element {
return (
<div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
Loading...
</div>
);
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
@@ -251,18 +205,12 @@ function getSourceMapUrl(code: string, map: string): string | null {
}
function Output({store, compilerOutput}: Props): JSX.Element {
return (
<Suspense fallback={<Fallback />}>
<OutputContent store={store} compilerOutput={compilerOutput} />
</Suspense>
);
}
function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
() => new Set(['Output']),
);
const [activeTab, setActiveTab] = useState<string>('Output');
const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
() => new Map(),
);
/*
* Update the active tab back to the output or errors tab when the compilation state
@@ -271,19 +219,17 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
const [previousOutputKind, setPreviousOutputKind] = useState(
compilerOutput.kind,
);
const isFailure = compilerOutput.kind !== 'ok';
if (compilerOutput.kind !== previousOutputKind) {
setPreviousOutputKind(compilerOutput.kind);
if (isFailure) {
startTransition(() => {
addTransitionType(EXPAND_ACCORDION_TRANSITION);
setTabsOpen(prev => new Set(prev).add('Output'));
setActiveTab('Output');
});
}
setTabsOpen(new Set(['Output']));
}
useEffect(() => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput]);
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
for (const [passName, results] of compilerOutput.results) {
@@ -298,40 +244,17 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
lastResult = currResult;
}
}
const tabs = use(tabifyCached(store, compilerOutput));
if (!store.showInternals) {
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<TabbedWindow
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
</ViewTransition>
);
}
return (
<ViewTransition
update={{
[CONFIG_PANEL_TRANSITION]: 'accordion-container',
[TOGGLE_INTERNALS_TRANSITION]: '',
default: 'none',
}}>
<AccordionWindow
defaultTab={store.showInternals ? 'HIR' : 'Output'}
<>
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
changedPasses={changedPasses}
/>
</ViewTransition>
</>
);
}
@@ -383,29 +306,20 @@ function TextTabContent({
<DiffEditor
original={diff}
modified={output}
loading={''}
options={{
...monacoOptions,
scrollbar: {
vertical: 'hidden',
},
dimension: {
width: 0,
height: 0,
},
readOnly: true,
lineNumbers: 'off',
glyphMargin: false,
// Undocumented see https://github.com/Microsoft/vscode/issues/30795#issuecomment-410998882
overviewRulerLanes: 0,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
}}
/>
) : (
<MonacoEditor
language={language ?? 'javascript'}
value={output}
loading={''}
className="monaco-editor-output"
options={{
...monacoOptions,
readOnly: true,

View File

@@ -29,17 +29,4 @@ export const monacoOptions: Partial<EditorProps['options']> = {
automaticLayout: true,
wordWrap: 'on',
wrappingIndent: 'same',
tabSize: 2,
};
export const monacoConfigOptions: Partial<EditorProps['options']> = {
...monacoOptions,
lineNumbers: 'off',
renderLineHighlight: 'none',
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
scrollBeyondLastLine: false,
glyphMargin: false,
};

View File

@@ -10,20 +10,14 @@ import {CheckIcon} from '@heroicons/react/solid';
import clsx from 'clsx';
import Link from 'next/link';
import {useSnackbar} from 'notistack';
import {
useState,
startTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStore, useStoreDispatch} from './StoreContext';
import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
import {useStoreDispatch} from './StoreContext';
export default function Header(): JSX.Element {
const [showCheck, setShowCheck] = useState(false);
const store = useStore();
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar, closeSnackbar} = useSnackbar();
@@ -62,32 +56,6 @@ export default function Header(): JSX.Element {
<p className="hidden select-none sm:block">React Compiler Playground</p>
</div>
<div className="flex items-center text-[15px] gap-4">
<div className="flex items-center gap-2">
<label className="show-internals relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}
onChange={() =>
startTransition(() => {
addTransitionType(TOGGLE_INTERNALS_TRANSITION);
dispatchStore({type: 'toggleInternals'});
})
}
className="absolute opacity-0 cursor-pointer h-full w-full m-0"
/>
<span
className={clsx(
'absolute inset-0 rounded-full cursor-pointer transition-all duration-250',
"before:content-[''] before:absolute before:w-4 before:h-4 before:left-0.5 before:bottom-0.5",
'before:bg-white before:rounded-full before:transition-transform before:duration-250',
'focus-within:shadow-[0_0_1px_#2196F3]',
store.showInternals
? 'bg-link before:translate-x-3.5'
: 'bg-gray-300',
)}></span>
</label>
<span className="text-secondary">Show Internals</span>
</div>
<button
title="Reset Playground"
aria-label="Reset Playground"

View File

@@ -1,41 +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 {memo} from 'react';
export const IconChevron = memo<
JSX.IntrinsicElements['svg'] & {
/**
* The direction the arrow should point.
*/
displayDirection: 'right' | 'left';
}
>(function IconChevron({className, displayDirection, ...props}) {
const rotationClass =
displayDirection === 'left' ? 'rotate-90' : '-rotate-90';
const classes = className ? `${rotationClass} ${className}` : rotationClass;
return (
<svg
className={classes}
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
{...props}>
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
<path
fill="currentColor"
fillRule="nonzero"
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
transform="translate(356.5 164.5)"
/>
<polygon points="446 418 466 418 466 398 446 398" />
</g>
</svg>
);
});

View File

@@ -6,14 +6,10 @@
*/
import type {Dispatch, ReactNode} from 'react';
import {useState, useEffect, useReducer} from 'react';
import {useEffect, useReducer} from 'react';
import createContext from '../lib/createContext';
import {emptyStore, defaultStore} from '../lib/defaultStore';
import {
saveStore,
initStoreFromUrlOrLocalStorage,
type Store,
} from '../lib/stores';
import {emptyStore} from '../lib/defaultStore';
import {saveStore, type Store} from '../lib/stores';
const StoreContext = createContext<Store>();
@@ -34,20 +30,6 @@ export const useStoreDispatch = StoreDispatchContext.useContext;
*/
export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
const [store, dispatch] = useReducer(storeReducer, emptyStore);
const [isPageReady, setIsPageReady] = useState<boolean>(false);
useEffect(() => {
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
} catch (e) {
console.error('Failed to initialize store from URL or local storage', e);
mountStore = defaultStore;
}
dispatch({type: 'setStore', payload: {store: mountStore}});
setIsPageReady(true);
}, []);
useEffect(() => {
if (store !== emptyStore) {
saveStore(store);
@@ -57,7 +39,7 @@ export function StoreProvider({children}: {children: ReactNode}): JSX.Element {
return (
<StoreContext.Provider value={store}>
<StoreDispatchContext.Provider value={dispatch}>
{isPageReady ? children : null}
{children}
</StoreDispatchContext.Provider>
</StoreContext.Provider>
);
@@ -71,19 +53,10 @@ type ReducerAction =
};
}
| {
type: 'updateSource';
type: 'updateFile';
payload: {
source: string;
};
}
| {
type: 'updateConfig';
payload: {
config: string;
};
}
| {
type: 'toggleInternals';
};
function storeReducer(store: Store, action: ReducerAction): Store {
@@ -92,28 +65,13 @@ function storeReducer(store: Store, action: ReducerAction): Store {
const newStore = action.payload.store;
return newStore;
}
case 'updateSource': {
const source = action.payload.source;
case 'updateFile': {
const {source} = action.payload;
const newStore = {
...store,
source,
};
return newStore;
}
case 'updateConfig': {
const config = action.payload.config;
const newStore = {
...store,
config,
};
return newStore;
}
case 'toggleInternals': {
const newStore = {
...store,
showInternals: !store.showInternals,
};
return newStore;
}
}
}

View File

@@ -4,78 +4,103 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {
startTransition,
useId,
unstable_ViewTransition as ViewTransition,
unstable_addTransitionType as addTransitionType,
} from 'react';
import clsx from 'clsx';
import {TOGGLE_TAB_TRANSITION} from '../lib/transitionTypes';
export default function TabbedWindow({
tabs,
activeTab,
onTabChange,
}: {
tabs: Map<string, React.ReactNode>;
activeTab: string;
onTabChange: (tab: string) => void;
import {Resizable} from 're-resizable';
import React, {useCallback} from 'react';
type TabsRecord = Map<string, React.ReactNode>;
export default function TabbedWindow(props: {
defaultTab: string | null;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
changedPasses: Set<string>;
}): React.ReactElement {
const id = useId();
const transitionName = `tab-highlight-${id}`;
const handleTabChange = (tab: string): void => {
startTransition(() => {
addTransitionType(TOGGLE_TAB_TRANSITION);
onTabChange(tab);
});
};
return (
<div className="flex-1 min-w-[550px] sm:min-w-0">
<div className="flex flex-col h-full max-w-full">
<div className="flex p-2 flex-shrink-0">
{Array.from(tabs.keys()).map(tab => {
const isActive = activeTab === tab;
return (
<button
key={tab}
onClick={() => handleTabChange(tab)}
className={clsx(
'transition-transform py-1.5 px-1.5 xs:px-3 sm:px-4 rounded-full text-sm relative',
isActive ? 'text-link' : 'hover:bg-primary/5',
)}>
{isActive && (
<ViewTransition
name={transitionName}
enter={{default: 'none'}}
exit={{default: 'none'}}
share={{
[TOGGLE_TAB_TRANSITION]: 'tab-highlight',
default: 'none',
}}
update={{default: 'none'}}>
<div className="absolute inset-0 bg-highlight rounded-full" />
</ViewTransition>
)}
<ViewTransition
enter={{default: 'none'}}
exit={{default: 'none'}}
update={{
[TOGGLE_TAB_TRANSITION]: 'tab-text',
default: 'none',
}}>
<span className="relative z-1">{tab}</span>
</ViewTransition>
</button>
);
})}
</div>
<div className="flex-1 overflow-hidden w-full h-full">
{tabs.get(activeTab)}
</div>
if (props.tabs.size === 0) {
return (
<div
className="flex items-center justify-center"
style={{width: 'calc(100vw - 650px)'}}>
No compiler output detected, see errors below
</div>
);
}
return (
<div className="flex flex-row">
{Array.from(props.tabs.keys()).map(name => {
return (
<TabbedWindowItem
name={name}
key={name}
tabs={props.tabs}
tabsOpen={props.tabsOpen}
setTabsOpen={props.setTabsOpen}
hasChanged={props.changedPasses.has(name)}
/>
);
})}
</div>
);
}
function TabbedWindowItem({
name,
tabs,
tabsOpen,
setTabsOpen,
hasChanged,
}: {
name: string;
tabs: TabsRecord;
tabsOpen: Set<string>;
setTabsOpen: (newTab: Set<string>) => void;
hasChanged: boolean;
}): React.ReactElement {
const isShow = tabsOpen.has(name);
const toggleTabs = useCallback(() => {
const nextState = new Set(tabsOpen);
if (nextState.has(name)) {
nextState.delete(name);
} else {
nextState.add(name);
}
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 ? (
<Resizable className="border-r" minWidth={550} enable={{right: true}}>
<h2
title="Minimize tab"
aria-label="Minimize tab"
onClick={toggleTabs}
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}
</h2>
{tabs.get(name) ?? <div>No output for {name}</div>}
</Resizable>
) : (
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title={`Expand compiler tab: ${name}`}
aria-label={`Expand compiler tab: ${name}`}
style={{transform: 'rotate(90deg) translate(-50%)'}}
onClick={toggleTabs}
className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
hasChanged ? 'font-bold' : 'font-light'
} text-secondary hover:text-link`}>
{displayName}
</button>
</div>
)}
</div>
);
}

View File

@@ -1,308 +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 {parse as babelParse, ParseResult} from '@babel/parser';
import * as HermesParser from 'hermes-parser';
import * as t from '@babel/types';
import BabelPluginReactCompiler, {
CompilerError,
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
parseConfigPragmaForTests,
ValueKind,
type Hook,
PluginOptions,
CompilerPipelineValue,
parsePluginOptions,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
type LoggerEvent,
} from 'babel-plugin-react-compiler';
import {transformFromAstSync} from '@babel/core';
import type {
CompilerOutput,
CompilerTransformOutput,
PrintedCompilerPipelineValue,
} from '../components/Editor/Output';
function parseInput(
input: string,
language: 'flow' | 'typescript',
): ParseResult<t.File> {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
return HermesParser.parse(input, {
babel: true,
flow: 'all',
sourceType: 'module',
enableExperimentalComponentSyntax: true,
});
} else {
return babelParse(input, {
plugins: ['typescript', 'jsx'],
sourceType: 'module',
}) as ParseResult<t.File>;
}
}
function invokeCompiler(
source: string,
language: 'flow' | 'typescript',
options: PluginOptions,
): CompilerTransformOutput {
const ast = parseInput(source, language);
let result = transformFromAstSync(ast, source, {
filename: '_playgroundFile.js',
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
ast: true,
sourceType: 'module',
configFile: false,
sourceMaps: true,
babelrc: false,
});
if (result?.ast == null || result?.code == null || result?.map == null) {
throw new Error('Expected successful compilation');
}
return {
code: result.code,
sourceMaps: result.map,
language,
};
}
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePaginationFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useRefetchableFragment',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'useLazyLoadQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
[
'usePreloadedQuery',
{
valueKind: ValueKind.Frozen,
effectKind: Effect.Freeze,
noAlias: true,
transitiveMixedData: true,
},
],
];
function parseOptions(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): PluginOptions {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
compilationMode: 'infer',
environment:
mode === 'linter'
? {
// enabled in compiler
validateRefAccessDuringRender: false,
// enabled in linter
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
}
: {
/* use defaults for compiler mode */
},
});
// Parse config overrides from config editor
let configOverrideOptions: any = {};
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
if (configOverrides.trim()) {
if (configMatch && configMatch[1]) {
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
configOverrideOptions = new Function(`return (${configString})`)();
} else {
throw new Error('Invalid override format');
}
}
const opts: PluginOptions = parsePluginOptions({
...parsedPragmaOptions,
...configOverrideOptions,
environment: {
...parsedPragmaOptions.environment,
...configOverrideOptions.environment,
customHooks: new Map([...COMMON_HOOKS]),
},
});
return opts;
}
export function compile(
source: string,
mode: 'compiler' | 'linter',
configOverrides: string,
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
const error = new CompilerError();
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
let language: 'flow' | 'typescript';
if (source.match(/\@flow/)) {
language = 'flow';
} else {
language = 'typescript';
}
let transformOutput;
let baseOpts: PluginOptions | null = null;
try {
baseOpts = parseOptions(source, mode, configOverrides);
} catch (err) {
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Config,
reason: `Unexpected failure when transforming configs! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
if (baseOpts) {
try {
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
upsert({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
upsert({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
upsert({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
// Add logger options to the parsed options
const opts = {
...baseOpts,
logger: {
debugLogIRs: logIR,
logEvent: (_filename: string | null, event: LoggerEvent): void => {
if (event.kind === 'CompileError') {
otherErrors.push(event.detail);
}
},
},
};
transformOutput = invokeCompiler(source, language, opts);
} catch (err) {
/**
* 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.merge(err);
} else {
/**
* Handle unexpected failures by logging (to get a stack trace)
* and reporting
*/
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
reason: `Unexpected failure when transforming input! \n${err}`,
loc: null,
suggestions: null,
}),
);
}
}
}
// Only include logger errors if there weren't other errors
if (!error.hasErrors() && otherErrors.length !== 0) {
otherErrors.forEach(e => error.details.push(e));
}
if (error.hasErrors() || !transformOutput) {
return [{kind: 'err', results, error}, language, baseOpts];
}
return [
{kind: 'ok', results, transformOutput, errors: error.details},
language,
baseOpts,
];
}

View File

@@ -13,21 +13,10 @@ export default function MyApp() {
}
`;
export const defaultConfig = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
//compilationMode: "all"
} satisfies PluginOptions);`;
export const defaultStore: Store = {
source: index,
config: defaultConfig,
showInternals: false,
};
export const emptyStore: Store = {
source: '',
config: '',
showInternals: false,
};

View File

@@ -10,20 +10,18 @@ import {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent,
} from 'lz-string';
import {defaultStore, defaultConfig} from '../defaultStore';
import {defaultStore} from '../defaultStore';
/**
* Global Store for Playground
*/
export interface Store {
source: string;
config: string;
showInternals: boolean;
}
export function encodeStore(store: Store): string {
return compressToEncodedURIComponent(JSON.stringify(store));
}
export function decodeStore(hash: string): any {
export function decodeStore(hash: string): Store {
return JSON.parse(decompressFromEncodedURIComponent(hash));
}
@@ -64,14 +62,8 @@ export function initStoreFromUrlOrLocalStorage(): Store {
*/
if (!encodedSource) return defaultStore;
const raw: any = decodeStore(encodedSource);
const raw = decodeStore(encodedSource);
invariant(isValidStore(raw), 'Invalid Store');
// Make sure all properties are populated
return {
source: raw.source,
config: 'config' in raw && raw['config'] ? raw.config : defaultConfig,
showInternals: 'showInternals' in raw ? raw.showInternals : false,
};
return raw;
}

View File

@@ -1,11 +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.
*/
export const CONFIG_PANEL_TRANSITION = 'config-panel';
export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';

View File

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -11,7 +11,6 @@ const path = require('path');
const nextConfig = {
experimental: {
reactCompiler: true,
viewTransition: true,
},
reactStrictMode: true,
webpack: (config, options) => {

View File

@@ -26,39 +26,34 @@
"@babel/traverse": "^7.18.9",
"@babel/types": "7.26.3",
"@heroicons/react": "^1.0.6",
"@monaco-editor/react": "^4.8.0-rc.2",
"@playwright/test": "^1.56.1",
"@monaco-editor/react": "^4.4.6",
"@playwright/test": "^1.51.1",
"@use-gesture/react": "^10.2.22",
"hermes-eslint": "^0.25.0",
"hermes-parser": "^0.25.0",
"invariant": "^2.2.4",
"lru-cache": "^11.2.2",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.6.0-canary.7",
"next": "^15.2.0-canary.64",
"notistack": "^3.0.0-alpha.7",
"prettier": "^3.3.3",
"pretty-format": "^29.3.1",
"re-resizable": "^6.9.16",
"react": "19.2",
"react-dom": "19.2"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.2",
"@types/react-dom": "19.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"concurrently": "^7.4.0",
"eslint": "^8.28.0",
"eslint-config-next": "15.5.2",
"eslint-config-next": "^15.0.1",
"monaco-editor-webpack-plugin": "^7.1.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.2.4",
"wait-on": "^7.2.0"
},
"resolutions": {
"@types/react": "19.2",
"@types/react-dom": "19.2"
}
}

View File

@@ -55,16 +55,12 @@ export default defineConfig({
// contextOptions: {
// ignoreHTTPSErrors: true,
// },
viewport: {width: 1920, height: 1080},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
viewport: {width: 1920, height: 1080},
},
use: {...devices['Desktop Chrome']},
},
// {
// name: 'Desktop Firefox',

View File

@@ -69,75 +69,3 @@
scrollbar-width: none; /* Firefox */
}
}
::view-transition-old(.slide-in) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-in) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-in) {
z-index: 1;
}
::view-transition-old(.slide-out) {
animation-name: slideOutLeft;
}
::view-transition-new(.slide-out) {
animation-name: slideInLeft;
}
::view-transition-group(.slide-out) {
z-index: 1;
}
@keyframes slideOutLeft {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
::view-transition-old(.container),
::view-transition-new(.container) {
height: 100%;
}
::view-transition-old(.accordion-container),
::view-transition-new(.accordion-container) {
height: 100%;
object-fit: none;
object-position: left;
}
::view-transition-old(.tab-highlight),
::view-transition-new(.tab-highlight) {
height: 100%;
}
::view-transition-group(.tab-text) {
z-index: 1;
}
::view-transition-old(.expand-accordion),
::view-transition-new(.expand-accordion) {
width: auto;
}
::view-transition-group(.expand-accordion) {
overflow: clip;
}
/**
* For some reason, the original Monaco editor is still visible to the
* left of the DiffEditor. This is a workaround for better visual clarity.
*/
.monaco-diff-editor .editor.original{
visibility: hidden !important;
}

View File

@@ -6,9 +6,6 @@
"dom.iterable",
"esnext"
],
"types": [
"react/experimental"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -19,7 +16,7 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"jsx": "preserve",
"incremental": true,
"plugins": [
{

File diff suppressed because it is too large Load Diff

View File

@@ -19,8 +19,7 @@
"test": "yarn workspaces run test",
"snap": "yarn workspace babel-plugin-react-compiler run snap",
"snap:build": "yarn workspace snap run build",
"npm:publish": "node scripts/release/publish",
"eslint-docs": "yarn workspace babel-plugin-react-compiler build && node scripts/build-eslint-docs.js"
"npm:publish": "node scripts/release/publish"
},
"dependencies": {
"fs-extra": "^4.0.2",

View File

@@ -52,8 +52,8 @@
"react-dom": "0.0.0-experimental-4beb1fd8-20241118",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
"zod": "^3.22.4",
"zod-validation-error": "^2.1.0"
},
"resolutions": {
"./**/@babel/parser": "7.7.4",

View File

@@ -17,7 +17,7 @@ export function runBabelPluginReactCompiler(
text: string,
file: string,
language: 'flow' | 'typescript',
options: PluginOptions | null,
options: Partial<PluginOptions> | null,
includeAst: boolean = false,
): BabelCore.BabelFileResult {
const ast = BabelParser.parse(text, {

View File

@@ -51,26 +51,12 @@ function insertAdditionalFunctionDeclaration(
CompilerError.invariant(originalFnName != null && compiled.id != null, {
reason:
'Expected function declarations that are referenced elsewhere to have a named identifier',
description: null,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
CompilerError.invariant(originalFnParams.length === compiledParams.length, {
reason:
'Expected React Compiler optimized function declarations to have the same number of parameters as source',
description: null,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
const gatingCondition = t.identifier(
@@ -154,13 +140,7 @@ export function insertGatedFunctionDeclaration(
CompilerError.invariant(compiled.type === 'FunctionDeclaration', {
reason: 'Expected compiled node type to match input type',
description: `Got ${compiled.type} but expected FunctionDeclaration`,
details: [
{
kind: 'error',
loc: fnPath.node.loc ?? null,
message: null,
},
],
loc: fnPath.node.loc ?? null,
});
insertAdditionalFunctionDeclaration(
fnPath,

View File

@@ -9,7 +9,7 @@ import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {Scope as BabelScope} from '@babel/traverse';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
EnvironmentConfig,
GeneratedSource,
@@ -18,7 +18,7 @@ import {
import {getOrInsertWith} from '../Utils/utils';
import {ExternalFunction, isHookName} from '../HIR/Environment';
import {Err, Ok, Result} from '../Utils/Result';
import {LoggerEvent, ParsedPluginOptions} from './Options';
import {LoggerEvent, PluginOptions} from './Options';
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
import {SuppressionRange} from './Suppression';
@@ -38,7 +38,7 @@ export function validateRestrictedImports(
ImportDeclaration(importDeclPath) {
if (restrictedImports.has(importDeclPath.node.source.value)) {
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Bailing out due to blocklisted import',
description: `Import from module ${importDeclPath.node.source.value}`,
loc: importDeclPath.node.loc ?? null,
@@ -46,7 +46,7 @@ export function validateRestrictedImports(
}
},
});
if (error.hasAnyErrors()) {
if (error.hasErrors()) {
return error;
} else {
return null;
@@ -56,7 +56,7 @@ export function validateRestrictedImports(
type ProgramContextOptions = {
program: NodePath<t.Program>;
suppressions: Array<SuppressionRange>;
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
code: string | null;
hasModuleScopeOptOut: boolean;
@@ -66,7 +66,7 @@ export class ProgramContext {
* Program and environment context
*/
scope: BabelScope;
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
code: string | null;
reactRuntimeModule: string;
@@ -205,7 +205,7 @@ export class ProgramContext {
}
const error = new CompilerError();
error.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Encountered conflicting global in generated program',
description: `Conflict from local binding ${name}`,
loc: scope.getBinding(name)?.path.node.loc ?? null,
@@ -240,7 +240,7 @@ export function addImportsToProgram(
programContext: ProgramContext,
): void {
const existingImports = getExistingImports(path);
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
const stmts: Array<t.ImportDeclaration> = [];
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
a.localeCompare(b),
);
@@ -256,14 +256,8 @@ export function addImportsToProgram(
{
reason:
'Encountered conflicting import specifiers in generated program',
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name})`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
description: `Conflict from import ${loweredImport.module}:(${loweredImport.imported} as ${loweredImport.name}).`,
loc: GeneratedSource,
suggestions: null,
},
);
@@ -274,13 +268,7 @@ export function addImportsToProgram(
reason:
'Found inconsistent import specifier. This is an internal bug.',
description: `Expected import ${moduleName}:${specifierName} but found ${loweredImport.module}:${loweredImport.imported}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
}
@@ -303,29 +291,9 @@ export function addImportsToProgram(
if (maybeExistingImports != null) {
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
} else {
if (path.node.sourceType === 'module') {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
} else {
stmts.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
sortedImport.map(specifier => {
return t.objectProperty(
t.identifier(specifier.imported),
t.identifier(specifier.name),
);
}),
),
t.callExpression(t.identifier('require'), [
t.stringLiteral(moduleName),
]),
),
]),
);
}
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
}
}
path.unshiftContainer('body', stmts);

View File

@@ -6,7 +6,7 @@
*/
import * as t from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {
CompilerDiagnostic,
CompilerError,
@@ -20,7 +20,7 @@ import {
tryParseExternalFunction,
} from '../HIR/Environment';
import {hasOwnProperty} from '../Utils/utils';
import {fromZodError} from 'zod-validation-error/v4';
import {fromZodError} from 'zod-validation-error';
import {CompilerPipelineValue} from './Pipeline';
const PanicThresholdOptionsSchema = z.enum([
@@ -51,8 +51,8 @@ const CustomOptOutDirectiveSchema = z
.default(null);
type CustomOptOutDirective = z.infer<typeof CustomOptOutDirectiveSchema>;
export type PluginOptions = Partial<{
environment: Partial<EnvironmentConfig>;
export type PluginOptions = {
environment: EnvironmentConfig;
logger: Logger | null;
@@ -102,25 +102,14 @@ export type PluginOptions = Partial<{
panicThreshold: PanicThresholdOptions;
/**
* @deprecated
*
/*
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
* passes.
*
* NOTE: ignored if `outputMode` is specified
*
* Defaults to false
*/
noEmit: boolean;
/**
* If specified, overrides `noEmit` and controls the output mode of the compiler.
*
* Defaults to null
*/
outputMode: CompilerOutputMode | null;
/*
* Determines the strategy for determining which functions to compile. Note that regardless of
* which mode is enabled, a component can be opted out by adding the string literal
@@ -146,12 +135,7 @@ export type PluginOptions = Partial<{
*/
eslintSuppressionRules: Array<string> | null | undefined;
/**
* Whether to report "suppression" errors for Flow suppressions. If false, suppression errors
* are only emitted for ESLint suppressions
*/
flowSuppressions: boolean;
/*
* Ignore 'use no forget' annotations. Helpful during testing but should not be used in production.
*/
@@ -177,11 +161,7 @@ export type PluginOptions = Partial<{
* a userspace approximation of runtime APIs.
*/
target: CompilerReactTarget;
}>;
export type ParsedPluginOptions = Required<
Omit<PluginOptions, 'environment'>
> & {environment: EnvironmentConfig};
};
const CompilerReactTargetSchema = z.union([
z.literal('17'),
@@ -223,19 +203,6 @@ const CompilationModeSchema = z.enum([
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
const CompilerOutputModeSchema = z.enum([
// Build optimized for SSR, with client features removed
'ssr',
// Build optimized for the client, with auto memoization
'client',
// Build optimized for the client without auto memo
'client-no-memo',
// Lint mode, the output is unused but validations should run
'lint',
]);
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
/**
* Represents 'events' that may occur during compilation. Events are only
* recorded when a logger is set (through the config).
@@ -310,14 +277,13 @@ export type Logger = {
debugLogIRs?: (value: CompilerPipelineValue) => void;
};
export const defaultOptions: ParsedPluginOptions = {
export const defaultOptions: PluginOptions = {
compilationMode: 'infer',
panicThreshold: 'none',
environment: parseEnvironmentConfig({}).unwrap(),
logger: null,
gating: null,
noEmit: false,
outputMode: null,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
@@ -328,9 +294,9 @@ export const defaultOptions: ParsedPluginOptions = {
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
};
} as const;
export function parsePluginOptions(obj: unknown): ParsedPluginOptions {
export function parsePluginOptions(obj: unknown): PluginOptions {
if (obj == null || typeof obj !== 'object') {
return defaultOptions;
}

View File

@@ -8,7 +8,7 @@
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import prettyFormat from 'pretty-format';
import {CompilerOutputMode, Logger, ProgramContext} from '.';
import {Logger, ProgramContext} from '.';
import {
HIRFunction,
ReactiveFunction,
@@ -24,6 +24,7 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -32,7 +33,9 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -89,6 +92,7 @@ import {
} from '../Validation';
import {validateLocalsNotReassignedAfterRender} from '../Validation/ValidateLocalsNotReassignedAfterRender';
import {outlineFunctions} from '../Optimization/OutlineFunctions';
import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes';
import {lowerContextAccess} from '../Optimization/LowerContextAccess';
import {validateNoSetStateInEffects} from '../Validation/ValidateNoSetStateInEffects';
import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement';
@@ -97,16 +101,11 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureFunctionsInRender';
import {CompilerError} from '..';
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
import {validateNoDerivedComputationsInEffects} from '../Validation/ValidateNoDerivedComputationsInEffects';
import {validateNoDerivedComputationsInEffects_exp} from '../Validation/ValidateNoDerivedComputationsInEffects_exp';
import {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
import {optimizeForSSR} from '../Optimization/OptimizeForSSR';
import {validateExhaustiveDependencies} from '../Validation/ValidateExhaustiveDependencies';
import {validateSourceLocations} from '../Validation/ValidateSourceLocations';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -120,7 +119,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,
@@ -170,7 +169,7 @@ function runWithEnvironment(
validateUseMemo(hir).unwrap();
if (
env.enableDropManualMemoization &&
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +205,7 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir).unwrap();
}
@@ -230,17 +229,26 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
if (!env.config.enableNewMutationAliasingModel) {
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
}
}
if (env.outputMode === 'ssr') {
optimizeForSSR(hir);
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
if (!env.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
@@ -255,18 +263,23 @@ function runWithEnvironment(
pruneMaybeThrows(hir);
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
const mutabilityAliasingRangeErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.enableValidations) {
if (mutabilityAliasingRangeErrors.isErr()) {
throw mutabilityAliasingRangeErrors.unwrapErr();
if (!env.config.enableNewMutationAliasingModel) {
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
} else {
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
isFunctionExpression: false,
});
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
if (env.isInferredMemoEnabled) {
if (mutabilityAliasingErrors.isErr()) {
throw mutabilityAliasingErrors.unwrapErr();
}
validateLocalsNotReassignedAfterRender(hir);
}
validateLocalsNotReassignedAfterRender(hir);
}
if (env.enableValidations) {
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
@@ -279,14 +292,8 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects_exp) {
env.logErrors(validateNoDerivedComputationsInEffects_exp(hir));
} else if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir, env));
env.logErrors(validateNoSetStateInEffects(hir));
}
if (env.config.validateNoJSXInTryStatements) {
@@ -297,17 +304,17 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}
inferReactivePlaces(hir);
log({kind: 'hir', name: 'InferReactivePlaces', value: hir});
if (env.config.validateExhaustiveMemoizationDependencies) {
// NOTE: this relies on reactivity inference running first
validateExhaustiveDependencies(hir).unwrap();
}
rewriteInstructionKindsBasedOnReassignment(hir);
log({
kind: 'hir',
@@ -315,11 +322,18 @@ function runWithEnvironment(
value: hir,
});
if (env.enableValidations && env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
}
if (env.enableMemoization) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
@@ -340,15 +354,6 @@ function runWithEnvironment(
outlineJSX(hir);
}
if (env.config.enableNameAnonymousFunctions) {
nameAnonymousFunctions(hir);
log({
kind: 'hir',
name: 'NameAnonymousFunctions',
value: hir,
});
}
if (env.config.enableFunctionOutlining) {
outlineFunctions(hir, fbtOperands);
log({kind: 'hir', name: 'OutlineFunctions', value: hir});
@@ -569,10 +574,6 @@ function runWithEnvironment(
log({kind: 'ast', name: 'Codegen (outlined)', value: outlined.fn});
}
if (env.config.validateSourceLocations) {
validateSourceLocations(func, ast).unwrap();
}
/**
* This flag should be only set for unit / fixture tests to check
* that Forget correctly handles unexpected errors (e.g. exceptions
@@ -591,7 +592,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerOutputMode,
mode: CompilerMode,
programContext: ProgramContext,
logger: Logger | null,
filename: string | null,

View File

@@ -10,7 +10,7 @@ import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
@@ -23,12 +23,7 @@ import {
ProgramContext,
validateRestrictedImports,
} from './Imports';
import {
CompilerOutputMode,
CompilerReactTarget,
ParsedPluginOptions,
PluginOptions,
} from './Options';
import {CompilerReactTarget, PluginOptions} from './Options';
import {compileFn} from './Pipeline';
import {
filterSuppressionsThatAffectFunction,
@@ -39,7 +34,7 @@ import {GeneratedSource} from '../HIR';
import {Err, Ok, Result} from '../Utils/Result';
export type CompilerPass = {
opts: ParsedPluginOptions;
opts: PluginOptions;
filename: string | null;
comments: Array<t.CommentBlock | t.CommentLine>;
code: string | null;
@@ -50,7 +45,7 @@ const DYNAMIC_GATING_DIRECTIVE = new RegExp('^use memo if\\(([^\\)]*)\\)$');
export function tryFindDirectiveEnablingMemoization(
directives: Array<t.Directive>,
opts: ParsedPluginOptions,
opts: PluginOptions,
): Result<t.Directive | null, CompilerError> {
const optIn = directives.find(directive =>
OPT_IN_DIRECTIVES.has(directive.value.value),
@@ -86,7 +81,7 @@ export function findDirectiveDisablingMemoization(
}
function findDirectivesDynamicGating(
directives: Array<t.Directive>,
opts: ParsedPluginOptions,
opts: PluginOptions,
): Result<
{
gating: ExternalFunction;
@@ -109,14 +104,14 @@ function findDirectivesDynamicGating(
errors.push({
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
description: `Found '${directive.value.value}'`,
category: ErrorCategory.Gating,
severity: ErrorSeverity.InvalidReact,
loc: directive.loc ?? null,
suggestions: null,
});
}
}
}
if (errors.hasAnyErrors()) {
if (errors.hasErrors()) {
return Err(errors);
} else if (result.length > 1) {
const error = new CompilerError();
@@ -125,7 +120,7 @@ function findDirectivesDynamicGating(
description: `Expected a single directive but found [${result
.map(r => r.directive.value.value)
.join(', ')}]`,
category: ErrorCategory.Gating,
severity: ErrorSeverity.InvalidReact,
loc: result[0].directive.loc ?? null,
suggestions: null,
});
@@ -143,13 +138,15 @@ function findDirectivesDynamicGating(
}
}
function isError(err: unknown): boolean {
return !(err instanceof CompilerError) || err.hasErrors();
function isCriticalError(err: unknown): boolean {
return !(err instanceof CompilerError) || err.isCritical();
}
function isConfigError(err: unknown): boolean {
if (err instanceof CompilerError) {
return err.details.some(detail => detail.category === ErrorCategory.Config);
return err.details.some(
detail => detail.severity === ErrorSeverity.InvalidConfig,
);
}
return false;
}
@@ -214,7 +211,8 @@ function handleError(
logError(err, context, fnLoc);
if (
context.opts.panicThreshold === 'all_errors' ||
(context.opts.panicThreshold === 'critical_errors' && isError(err)) ||
(context.opts.panicThreshold === 'critical_errors' &&
isCriticalError(err)) ||
isConfigError(err) // Always throws regardless of panic threshold
) {
throw err;
@@ -315,13 +313,7 @@ function insertNewOutlinedFunctionNode(
CompilerError.invariant(insertedFuncDecl.isFunctionDeclaration(), {
reason: 'Expected inserted function declaration',
description: `Got: ${insertedFuncDecl}`,
details: [
{
kind: 'error',
loc: insertedFuncDecl.node?.loc ?? null,
message: null,
},
],
loc: insertedFuncDecl.node?.loc ?? null,
});
return insertedFuncDecl;
}
@@ -400,15 +392,7 @@ export function compileProgram(
*/
const suppressions = findProgramSuppressions(
pass.comments,
/*
* If the compiler is validating hooks rules and exhaustive memo dependencies, we don't need to check
* for React ESLint suppressions
*/
pass.opts.environment.validateExhaustiveMemoizationDependencies &&
pass.opts.environment.validateHooksUsage
? null
: (pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS),
// Always bail on Flow suppressions
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
pass.opts.flowSuppressions,
);
@@ -430,30 +414,15 @@ export function compileProgram(
);
const compiledFns: Array<CompileResult> = [];
// outputMode takes precedence if specified
const outputMode: CompilerOutputMode =
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
while (queue.length !== 0) {
const current = queue.shift()!;
const compiled = processFn(
current.fn,
current.fnType,
programContext,
outputMode,
);
const compiled = processFn(current.fn, current.fnType, programContext);
if (compiled != null) {
for (const outlined of compiled.outlined) {
CompilerError.invariant(outlined.fn.outlined.length === 0, {
reason: 'Unexpected nested outlined functions',
description: null,
details: [
{
kind: 'error',
loc: outlined.fn.loc,
message: null,
},
],
loc: outlined.fn.loc,
});
const fn = insertNewOutlinedFunctionNode(
program,
@@ -486,7 +455,7 @@ export function compileProgram(
new CompilerErrorDetail({
reason:
'Unexpected compiled functions when module scope opt-out is present',
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: null,
}),
);
@@ -521,20 +490,7 @@ function findFunctionsToCompile(
): Array<CompileSource> {
const queue: Array<CompileSource> = [];
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
// In 'all' mode, compile only top level functions
if (
pass.opts.compilationMode === 'all' &&
fn.scope.getProgramParent() !== fn.scope.parent
) {
return;
}
const fnType = getReactFunctionType(fn, pass);
if (pass.opts.environment.validateNoDynamicallyCreatedComponentsOrHooks) {
validateNoDynamicallyCreatedComponentsOrHooks(fn, pass, programContext);
}
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
return;
}
@@ -598,7 +554,6 @@ function processFn(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
): null | CodegenFunction {
let directives: {
optIn: t.Directive | null;
@@ -634,27 +589,18 @@ function processFn(
}
let compiledFn: CodegenFunction;
const compileResult = tryCompileFunction(
fn,
fnType,
programContext,
outputMode,
);
const compileResult = tryCompileFunction(fn, fnType, programContext);
if (compileResult.kind === 'error') {
if (directives.optOut != null) {
logError(compileResult.error, programContext, fn.node.loc ?? null);
} else {
handleError(compileResult.error, programContext, fn.node.loc ?? null);
}
if (outputMode === 'client') {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
const retryResult = retryCompileFunction(fn, fnType, programContext);
if (retryResult == null) {
return null;
}
compiledFn = retryResult;
} else {
compiledFn = compileResult.compiledFn;
}
@@ -690,7 +636,7 @@ function processFn(
if (programContext.hasModuleScopeOptOut) {
return null;
} else if (programContext.opts.outputMode === 'lint') {
} else if (programContext.opts.noEmit) {
/**
* inferEffectDependencies + noEmit is currently only used for linting. In
* this mode, add source locations for where the compiler *can* infer effect
@@ -720,7 +666,6 @@ function tryCompileFunction(
fn: BabelFn,
fnType: ReactFunctionType,
programContext: ProgramContext,
outputMode: CompilerOutputMode,
):
| {kind: 'compile'; compiledFn: CodegenFunction}
| {kind: 'error'; error: unknown} {
@@ -747,7 +692,7 @@ function tryCompileFunction(
fn,
programContext.opts.environment,
fnType,
outputMode,
'all_features',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -785,7 +730,7 @@ function retryCompileFunction(
fn,
environment,
fnType,
'client-no-memo',
'no_inferred_memo',
programContext,
programContext.opts.logger,
programContext.filename,
@@ -865,7 +810,7 @@ function shouldSkipCompilation(
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",
category: ErrorCategory.Config,
severity: ErrorSeverity.InvalidConfig,
loc: null,
}),
);
@@ -889,72 +834,6 @@ function shouldSkipCompilation(
return false;
}
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope reference
* errors that occur when the compiler attempts to optimize the nested component/hook while its
* parent function remains uncompiled.
*/
function validateNoDynamicallyCreatedComponentsOrHooks(
fn: BabelFn,
pass: CompilerPass,
programContext: ProgramContext,
): void {
const parentNameExpr = getFunctionName(fn);
const parentName =
parentNameExpr !== null && parentNameExpr.isIdentifier()
? parentNameExpr.node.name
: '<anonymous>';
const validateNestedFunction = (
nestedFn: NodePath<
t.FunctionDeclaration | t.FunctionExpression | t.ArrowFunctionExpression
>,
): void => {
if (
nestedFn.node === fn.node ||
programContext.alreadyCompiled.has(nestedFn.node)
) {
return;
}
if (nestedFn.scope.getProgramParent() !== nestedFn.scope.parent) {
const nestedFnType = getReactFunctionType(nestedFn as BabelFn, pass);
const nestedFnNameExpr = getFunctionName(nestedFn as BabelFn);
const nestedName =
nestedFnNameExpr !== null && nestedFnNameExpr.isIdentifier()
? nestedFnNameExpr.node.name
: '<anonymous>';
if (nestedFnType === 'Component' || nestedFnType === 'Hook') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Factories,
reason: `Components and hooks cannot be created dynamically`,
description: `The function \`${nestedName}\` appears to be a React ${nestedFnType.toLowerCase()}, but it's defined inside \`${parentName}\`. Components and Hooks should always be declared at module scope`,
details: [
{
kind: 'error',
message: 'this function dynamically created a component/hook',
loc: parentNameExpr?.node.loc ?? fn.node.loc ?? null,
},
{
kind: 'error',
message: 'the component is created here',
loc: nestedFnNameExpr?.node.loc ?? nestedFn.node.loc ?? null,
},
],
});
}
}
nestedFn.skip();
};
fn.traverse({
FunctionDeclaration: validateNestedFunction,
FunctionExpression: validateNestedFunction,
ArrowFunctionExpression: validateNestedFunction,
});
}
function getReactFunctionType(
fn: BabelFn,
pass: CompilerPass,
@@ -993,6 +872,11 @@ function getReactFunctionType(
return componentSyntaxType;
}
case 'all': {
// Compile only top level functions
if (fn.scope.getProgramParent() !== fn.scope.parent) {
return null;
}
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
}
default: {
@@ -1452,13 +1336,7 @@ export function getReactCompilerRuntimeModule(
{
reason: 'Expected target to already be validated',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);

View File

@@ -7,7 +7,7 @@
import type * as BabelCore from '@babel/core';
import {hasOwnProperty} from '../Utils/utils';
import {ParsedPluginOptions} from './Options';
import {PluginOptions} from './Options';
function hasModule(name: string): boolean {
if (typeof require === 'undefined') {
@@ -52,9 +52,7 @@ export function pipelineUsesReanimatedPlugin(
return hasModule('react-native-reanimated');
}
export function injectReanimatedFlag(
options: ParsedPluginOptions,
): ParsedPluginOptions {
export function injectReanimatedFlag(options: PluginOptions): PluginOptions {
return {
...options,
environment: {

View File

@@ -11,7 +11,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
@@ -78,7 +78,7 @@ export function filterSuppressionsThatAffectFunction(
export function findProgramSuppressions(
programComments: Array<t.Comment>,
ruleNames: Array<string> | null,
ruleNames: Array<string>,
flowSuppressions: boolean,
): Array<SuppressionRange> {
const suppressionRanges: Array<SuppressionRange> = [];
@@ -86,18 +86,12 @@ export function findProgramSuppressions(
let enableComment: t.Comment | null = null;
let source: SuppressionSource | null = null;
let disableNextLinePattern: RegExp | null = null;
let disablePattern: RegExp | null = null;
let enablePattern: RegExp | null = null;
if (ruleNames != null && ruleNames.length !== 0) {
const rulePattern = `(${ruleNames.join('|')})`;
disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
);
disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
}
const rulePattern = `(${ruleNames.join('|')})`;
const disableNextLinePattern = new RegExp(
`eslint-disable-next-line ${rulePattern}`,
);
const disablePattern = new RegExp(`eslint-disable ${rulePattern}`);
const enablePattern = new RegExp(`eslint-enable ${rulePattern}`);
const flowSuppressionPattern = new RegExp(
'\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule',
);
@@ -113,7 +107,6 @@ export function findProgramSuppressions(
* CommentLine within the block.
*/
disableComment == null &&
disableNextLinePattern != null &&
disableNextLinePattern.test(comment.value)
) {
disableComment = comment;
@@ -131,16 +124,12 @@ export function findProgramSuppressions(
source = 'Flow';
}
if (disablePattern != null && disablePattern.test(comment.value)) {
if (disablePattern.test(comment.value)) {
disableComment = comment;
source = 'Eslint';
}
if (
enablePattern != null &&
enablePattern.test(comment.value) &&
source === 'Eslint'
) {
if (enablePattern.test(comment.value) && source === 'Eslint') {
enableComment = comment;
}
@@ -163,14 +152,7 @@ export function suppressionsToCompilerError(
): CompilerError {
CompilerError.invariant(suppressionRanges.length !== 0, {
reason: `Expected at least suppression comment source range`,
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
const error = new CompilerError();
for (const suppressionRange of suppressionRanges) {
@@ -201,9 +183,9 @@ export function suppressionsToCompilerError(
}
error.pushDiagnostic(
CompilerDiagnostic.create({
reason: reason,
category: reason,
description: `React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. Found suppression \`${suppressionRange.disableComment.value.trim()}\``,
category: ErrorCategory.Suppression,
severity: ErrorSeverity.InvalidReact,
suggestions: [
{
description: suggestion,
@@ -214,7 +196,7 @@ export function suppressionsToCompilerError(
op: CompilerSuggestionOperation.Remove,
},
],
}).withDetails({
}).withDetail({
kind: 'error',
loc: suppressionRange.disableComment.loc ?? null,
message: 'Found React rule suppression',

View File

@@ -8,27 +8,27 @@
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
import {CompilerError, EnvironmentConfig, Logger} from '..';
import {CompilerError, EnvironmentConfig, ErrorSeverity, Logger} from '..';
import {getOrInsertWith} from '../Utils/utils';
import {Environment, GeneratedSource} from '../HIR';
import {DEFAULT_EXPORT} from '../HIR/Environment';
import {CompileProgramMetadata} from './Program';
import {
CompilerDiagnostic,
CompilerDiagnosticOptions,
ErrorCategory,
} from '../CompilerError';
import {CompilerDiagnostic, CompilerDiagnosticOptions} from '../CompilerError';
function throwInvalidReact(
options: CompilerDiagnosticOptions,
options: Omit<CompilerDiagnosticOptions, 'severity'>,
{logger, filename}: TraversalState,
): never {
const detail: CompilerDiagnosticOptions = {
severity: ErrorSeverity.InvalidReact,
...options,
};
logger?.logEvent(filename, {
kind: 'CompileError',
fnLoc: null,
detail: new CompilerDiagnostic(options),
detail: new CompilerDiagnostic(detail),
});
CompilerError.throwDiagnostic(options);
CompilerError.throwDiagnostic(detail);
}
function isAutodepsSigil(
@@ -92,11 +92,10 @@ function assertValidEffectImportReference(
*/
throwInvalidReact(
{
category: ErrorCategory.AutomaticEffectDependencies,
reason:
category:
'Cannot infer dependencies of this effect. This will break your build!',
description:
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics' +
'To resolve, either pass a dependency array or fix reported compiler bailout diagnostics.' +
(maybeErrorDiagnostic ? ` ${maybeErrorDiagnostic}` : ''),
details: [
{
@@ -124,11 +123,13 @@ function assertValidFireImportReference(
);
throwInvalidReact(
{
category: ErrorCategory.Fire,
reason: '[Fire] Untransformed reference to compiler-required feature.',
category:
'[Fire] Untransformed reference to compiler-required feature.',
description:
'Either remove this `fire` call or ensure it is successfully transformed by the compiler' +
(maybeErrorDiagnostic != null ? ` ${maybeErrorDiagnostic}` : ''),
maybeErrorDiagnostic
? ` ${maybeErrorDiagnostic}`
: '',
details: [
{
kind: 'error',
@@ -215,14 +216,7 @@ function validateImportSpecifier(
const binding = local.scope.getBinding(local.node.name);
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
description: null,
details: [
{
kind: 'error',
loc: local.node.loc ?? null,
message: null,
},
],
loc: local.node.loc ?? null,
});
checkFn(binding.referencePaths, state);
}
@@ -242,14 +236,7 @@ function validateNamespacedImport(
CompilerError.invariant(binding != null, {
reason: 'Expected binding to be found for import specifier',
description: null,
details: [
{
kind: 'error',
loc: local.node.loc ?? null,
message: null,
},
],
loc: local.node.loc ?? null,
});
const filteredReferences = new Map<
CheckInvalidReferenceFn,

View File

@@ -1,752 +0,0 @@
/**
* TypeScript definitions for Flow type JSON representations
* Based on the output of /data/sandcastle/boxes/fbsource/fbcode/flow/src/typing/convertTypes.ml
*/
// Base type for all Flow types with a kind field
export interface BaseFlowType {
kind: string;
}
// Type for representing polarity
export type Polarity = 'positive' | 'negative' | 'neutral';
// Type for representing a name that might be null
export type OptionalName = string | null;
// Open type
export interface OpenType extends BaseFlowType {
kind: 'Open';
}
// Def type
export interface DefType extends BaseFlowType {
kind: 'Def';
def: DefT;
}
// Eval type
export interface EvalType extends BaseFlowType {
kind: 'Eval';
type: FlowType;
destructor: Destructor;
}
// Generic type
export interface GenericType extends BaseFlowType {
kind: 'Generic';
name: string;
bound: FlowType;
no_infer: boolean;
}
// ThisInstance type
export interface ThisInstanceType extends BaseFlowType {
kind: 'ThisInstance';
instance: InstanceT;
is_this: boolean;
name: string;
}
// ThisTypeApp type
export interface ThisTypeAppType extends BaseFlowType {
kind: 'ThisTypeApp';
t1: FlowType;
t2: FlowType;
t_list?: Array<FlowType>;
}
// TypeApp type
export interface TypeAppType extends BaseFlowType {
kind: 'TypeApp';
type: FlowType;
targs: Array<FlowType>;
from_value: boolean;
use_desc: boolean;
}
// FunProto type
export interface FunProtoType extends BaseFlowType {
kind: 'FunProto';
}
// ObjProto type
export interface ObjProtoType extends BaseFlowType {
kind: 'ObjProto';
}
// NullProto type
export interface NullProtoType extends BaseFlowType {
kind: 'NullProto';
}
// FunProtoBind type
export interface FunProtoBindType extends BaseFlowType {
kind: 'FunProtoBind';
}
// Intersection type
export interface IntersectionType extends BaseFlowType {
kind: 'Intersection';
members: Array<FlowType>;
}
// Union type
export interface UnionType extends BaseFlowType {
kind: 'Union';
members: Array<FlowType>;
}
// Maybe type
export interface MaybeType extends BaseFlowType {
kind: 'Maybe';
type: FlowType;
}
// Optional type
export interface OptionalType extends BaseFlowType {
kind: 'Optional';
type: FlowType;
use_desc: boolean;
}
// Keys type
export interface KeysType extends BaseFlowType {
kind: 'Keys';
type: FlowType;
}
// Annot type
export interface AnnotType extends BaseFlowType {
kind: 'Annot';
type: FlowType;
use_desc: boolean;
}
// Opaque type
export interface OpaqueType extends BaseFlowType {
kind: 'Opaque';
opaquetype: {
opaque_id: string;
underlying_t: FlowType | null;
super_t: FlowType | null;
opaque_type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
opaque_name: string;
};
}
// Namespace type
export interface NamespaceType extends BaseFlowType {
kind: 'Namespace';
namespace_symbol: {
symbol: string;
};
values_type: FlowType;
types_tmap: PropertyMap;
}
// Any type
export interface AnyType extends BaseFlowType {
kind: 'Any';
}
// StrUtil type
export interface StrUtilType extends BaseFlowType {
kind: 'StrUtil';
op: 'StrPrefix' | 'StrSuffix';
prefix?: string;
suffix?: string;
remainder?: FlowType;
}
// TypeParam definition
export interface TypeParam {
name: string;
bound: FlowType;
polarity: Polarity;
default: FlowType | null;
}
// EnumInfo types
export type EnumInfo = ConcreteEnum | AbstractEnum;
export interface ConcreteEnum {
kind: 'ConcreteEnum';
enum_name: string;
enum_id: string;
members: Array<string>;
representation_t: FlowType;
has_unknown_members: boolean;
}
export interface AbstractEnum {
kind: 'AbstractEnum';
representation_t: FlowType;
}
// CanonicalRendersForm types
export type CanonicalRendersForm =
| InstrinsicRenders
| NominalRenders
| StructuralRenders
| DefaultRenders;
export interface InstrinsicRenders {
kind: 'InstrinsicRenders';
name: string;
}
export interface NominalRenders {
kind: 'NominalRenders';
renders_id: string;
renders_name: string;
renders_super: FlowType;
}
export interface StructuralRenders {
kind: 'StructuralRenders';
renders_variant: 'RendersNormal' | 'RendersMaybe' | 'RendersStar';
renders_structural_type: FlowType;
}
export interface DefaultRenders {
kind: 'DefaultRenders';
}
// InstanceT definition
export interface InstanceT {
inst: InstType;
static: FlowType;
super: FlowType;
implements: Array<FlowType>;
}
// InstType definition
export interface InstType {
class_name: string | null;
class_id: string;
type_args: Array<{
name: string;
type: FlowType;
polarity: Polarity;
}>;
own_props: PropertyMap;
proto_props: PropertyMap;
call_t: null | {
id: number;
call: FlowType;
};
}
// DefT types
export type DefT =
| NumGeneralType
| StrGeneralType
| BoolGeneralType
| BigIntGeneralType
| EmptyType
| MixedType
| NullType
| VoidType
| SymbolType
| FunType
| ObjType
| ArrType
| ClassType
| InstanceType
| SingletonStrType
| NumericStrKeyType
| SingletonNumType
| SingletonBoolType
| SingletonBigIntType
| TypeType
| PolyType
| ReactAbstractComponentType
| RendersType
| EnumValueType
| EnumObjectType;
export interface NumGeneralType extends BaseFlowType {
kind: 'NumGeneral';
}
export interface StrGeneralType extends BaseFlowType {
kind: 'StrGeneral';
}
export interface BoolGeneralType extends BaseFlowType {
kind: 'BoolGeneral';
}
export interface BigIntGeneralType extends BaseFlowType {
kind: 'BigIntGeneral';
}
export interface EmptyType extends BaseFlowType {
kind: 'Empty';
}
export interface MixedType extends BaseFlowType {
kind: 'Mixed';
}
export interface NullType extends BaseFlowType {
kind: 'Null';
}
export interface VoidType extends BaseFlowType {
kind: 'Void';
}
export interface SymbolType extends BaseFlowType {
kind: 'Symbol';
}
export interface FunType extends BaseFlowType {
kind: 'Fun';
static: FlowType;
funtype: FunTypeObj;
}
export interface ObjType extends BaseFlowType {
kind: 'Obj';
objtype: ObjTypeObj;
}
export interface ArrType extends BaseFlowType {
kind: 'Arr';
arrtype: ArrTypeObj;
}
export interface ClassType extends BaseFlowType {
kind: 'Class';
type: FlowType;
}
export interface InstanceType extends BaseFlowType {
kind: 'Instance';
instance: InstanceT;
}
export interface SingletonStrType extends BaseFlowType {
kind: 'SingletonStr';
from_annot: boolean;
value: string;
}
export interface NumericStrKeyType extends BaseFlowType {
kind: 'NumericStrKey';
number: string;
string: string;
}
export interface SingletonNumType extends BaseFlowType {
kind: 'SingletonNum';
from_annot: boolean;
number: string;
string: string;
}
export interface SingletonBoolType extends BaseFlowType {
kind: 'SingletonBool';
from_annot: boolean;
value: boolean;
}
export interface SingletonBigIntType extends BaseFlowType {
kind: 'SingletonBigInt';
from_annot: boolean;
value: string;
}
export interface TypeType extends BaseFlowType {
kind: 'Type';
type_kind: TypeTKind;
type: FlowType;
}
export type TypeTKind =
| 'TypeAliasKind'
| 'TypeParamKind'
| 'OpaqueKind'
| 'ImportTypeofKind'
| 'ImportClassKind'
| 'ImportEnumKind'
| 'InstanceKind'
| 'RenderTypeKind';
export interface PolyType extends BaseFlowType {
kind: 'Poly';
tparams: Array<TypeParam>;
t_out: FlowType;
id: string;
}
export interface ReactAbstractComponentType extends BaseFlowType {
kind: 'ReactAbstractComponent';
config: FlowType;
renders: FlowType;
instance: ComponentInstance;
component_kind: ComponentKind;
}
export type ComponentInstance =
| {kind: 'RefSetterProp'; type: FlowType}
| {kind: 'Omitted'};
export type ComponentKind =
| {kind: 'Structural'}
| {kind: 'Nominal'; id: string; name: string; types: Array<FlowType> | null};
export interface RendersType extends BaseFlowType {
kind: 'Renders';
form: CanonicalRendersForm;
}
export interface EnumValueType extends BaseFlowType {
kind: 'EnumValue';
enum_info: EnumInfo;
}
export interface EnumObjectType extends BaseFlowType {
kind: 'EnumObject';
enum_value_t: FlowType;
enum_info: EnumInfo;
}
// ObjKind types
export type ObjKind =
| {kind: 'Exact'}
| {kind: 'Inexact'}
| {kind: 'Indexed'; dicttype: DictType};
// DictType definition
export interface DictType {
dict_name: string | null;
key: FlowType;
value: FlowType;
dict_polarity: Polarity;
}
// ArrType types
export type ArrTypeObj = ArrayAT | TupleAT | ROArrayAT;
export interface ArrayAT {
kind: 'ArrayAT';
elem_t: FlowType;
}
export interface TupleAT {
kind: 'TupleAT';
elem_t: FlowType;
elements: Array<TupleElement>;
min_arity: number;
max_arity: number;
inexact: boolean;
}
export interface ROArrayAT {
kind: 'ROArrayAT';
elem_t: FlowType;
}
// TupleElement definition
export interface TupleElement {
name: string | null;
t: FlowType;
polarity: Polarity;
optional: boolean;
}
// Flags definition
export interface Flags {
obj_kind: ObjKind;
}
// Property types
export type Property =
| FieldProperty
| GetProperty
| SetProperty
| GetSetProperty
| MethodProperty;
export interface FieldProperty {
kind: 'Field';
type: FlowType;
polarity: Polarity;
}
export interface GetProperty {
kind: 'Get';
type: FlowType;
}
export interface SetProperty {
kind: 'Set';
type: FlowType;
}
export interface GetSetProperty {
kind: 'GetSet';
get_type: FlowType;
set_type: FlowType;
}
export interface MethodProperty {
kind: 'Method';
type: FlowType;
}
// PropertyMap definition
export interface PropertyMap {
[key: string]: Property; // For other properties in the map
}
// ObjType definition
export interface ObjTypeObj {
flags: Flags;
props: PropertyMap;
proto_t: FlowType;
call_t: number | null;
}
// FunType definition
export interface FunTypeObj {
this_t: {
type: FlowType;
status: ThisStatus;
};
params: Array<{
name: string | null;
type: FlowType;
}>;
rest_param: null | {
name: string | null;
type: FlowType;
};
return_t: FlowType;
type_guard: null | {
inferred: boolean;
param_name: string;
type_guard: FlowType;
one_sided: boolean;
};
effect: Effect;
}
// ThisStatus types
export type ThisStatus =
| {kind: 'This_Method'; unbound: boolean}
| {kind: 'This_Function'};
// Effect types
export type Effect =
| {kind: 'HookDecl'; id: string}
| {kind: 'HookAnnot'}
| {kind: 'ArbitraryEffect'}
| {kind: 'AnyEffect'};
// Destructor types
export type Destructor =
| NonMaybeTypeDestructor
| PropertyTypeDestructor
| ElementTypeDestructor
| OptionalIndexedAccessNonMaybeTypeDestructor
| OptionalIndexedAccessResultTypeDestructor
| ExactTypeDestructor
| ReadOnlyTypeDestructor
| PartialTypeDestructor
| RequiredTypeDestructor
| SpreadTypeDestructor
| SpreadTupleTypeDestructor
| RestTypeDestructor
| ValuesTypeDestructor
| ConditionalTypeDestructor
| TypeMapDestructor
| ReactElementPropsTypeDestructor
| ReactElementConfigTypeDestructor
| ReactCheckComponentConfigDestructor
| ReactDRODestructor
| MakeHooklikeDestructor
| MappedTypeDestructor
| EnumTypeDestructor;
export interface NonMaybeTypeDestructor {
kind: 'NonMaybeType';
}
export interface PropertyTypeDestructor {
kind: 'PropertyType';
name: string;
}
export interface ElementTypeDestructor {
kind: 'ElementType';
index_type: FlowType;
}
export interface OptionalIndexedAccessNonMaybeTypeDestructor {
kind: 'OptionalIndexedAccessNonMaybeType';
index: OptionalIndexedAccessIndex;
}
export type OptionalIndexedAccessIndex =
| {kind: 'StrLitIndex'; name: string}
| {kind: 'TypeIndex'; type: FlowType};
export interface OptionalIndexedAccessResultTypeDestructor {
kind: 'OptionalIndexedAccessResultType';
}
export interface ExactTypeDestructor {
kind: 'ExactType';
}
export interface ReadOnlyTypeDestructor {
kind: 'ReadOnlyType';
}
export interface PartialTypeDestructor {
kind: 'PartialType';
}
export interface RequiredTypeDestructor {
kind: 'RequiredType';
}
export interface SpreadTypeDestructor {
kind: 'SpreadType';
target: SpreadTarget;
operands: Array<SpreadOperand>;
operand_slice: Slice | null;
}
export type SpreadTarget =
| {kind: 'Value'; make_seal: 'Sealed' | 'Frozen' | 'As_Const'}
| {kind: 'Annot'; make_exact: boolean};
export type SpreadOperand = {kind: 'Type'; type: FlowType} | Slice;
export interface Slice {
kind: 'Slice';
prop_map: PropertyMap;
generics: Array<string>;
dict: DictType | null;
reachable_targs: Array<{
type: FlowType;
polarity: Polarity;
}>;
}
export interface SpreadTupleTypeDestructor {
kind: 'SpreadTupleType';
inexact: boolean;
resolved_rev: string;
unresolved: string;
}
export interface RestTypeDestructor {
kind: 'RestType';
merge_mode: RestMergeMode;
type: FlowType;
}
export type RestMergeMode =
| {kind: 'SpreadReversal'}
| {kind: 'ReactConfigMerge'; polarity: Polarity}
| {kind: 'Omit'};
export interface ValuesTypeDestructor {
kind: 'ValuesType';
}
export interface ConditionalTypeDestructor {
kind: 'ConditionalType';
distributive_tparam_name: string | null;
infer_tparams: string;
extends_t: FlowType;
true_t: FlowType;
false_t: FlowType;
}
export interface TypeMapDestructor {
kind: 'ObjectKeyMirror';
}
export interface ReactElementPropsTypeDestructor {
kind: 'ReactElementPropsType';
}
export interface ReactElementConfigTypeDestructor {
kind: 'ReactElementConfigType';
}
export interface ReactCheckComponentConfigDestructor {
kind: 'ReactCheckComponentConfig';
props: {
[key: string]: Property;
};
}
export interface ReactDRODestructor {
kind: 'ReactDRO';
dro_type:
| 'HookReturn'
| 'HookArg'
| 'Props'
| 'ImmutableAnnot'
| 'DebugAnnot';
}
export interface MakeHooklikeDestructor {
kind: 'MakeHooklike';
}
export interface MappedTypeDestructor {
kind: 'MappedType';
homomorphic: Homomorphic;
distributive_tparam_name: string | null;
property_type: FlowType;
mapped_type_flags: {
variance: Polarity;
optional: 'MakeOptional' | 'RemoveOptional' | 'KeepOptionality';
};
}
export type Homomorphic =
| {kind: 'Homomorphic'}
| {kind: 'Unspecialized'}
| {kind: 'SemiHomomorphic'; type: FlowType};
export interface EnumTypeDestructor {
kind: 'EnumType';
}
// Union of all possible Flow types
export type FlowType =
| OpenType
| DefType
| EvalType
| GenericType
| ThisInstanceType
| ThisTypeAppType
| TypeAppType
| FunProtoType
| ObjProtoType
| NullProtoType
| FunProtoBindType
| IntersectionType
| UnionType
| MaybeType
| OptionalType
| KeysType
| AnnotType
| OpaqueType
| NamespaceType
| AnyType
| StrUtilType;

View File

@@ -1,138 +0,0 @@
import {CompilerError, SourceLocation} from '..';
import {
ConcreteType,
printConcrete,
printType,
StructuralValue,
Type,
VariableId,
} from './Types';
export function unsupportedLanguageFeature(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support language feature: ${desc}`,
loc,
});
}
export type UnificationError =
| {
kind: 'TypeUnification';
left: ConcreteType<Type>;
right: ConcreteType<Type>;
}
| {
kind: 'StructuralUnification';
left: StructuralValue;
right: ConcreteType<Type>;
};
function printUnificationError(err: UnificationError): string {
if (err.kind === 'TypeUnification') {
return `${printConcrete(err.left, printType)} is incompatible with ${printConcrete(err.right, printType)}`;
} else {
return `structural ${err.left.kind} is incompatible with ${printConcrete(err.right, printType)}`;
}
}
export function raiseUnificationErrors(
errs: null | Array<UnificationError>,
loc: SourceLocation,
): void {
if (errs != null) {
if (errs.length === 0) {
CompilerError.invariant(false, {
reason: 'Should not have array of zero errors',
description: null,
details: [
{
kind: 'error',
loc,
message: null,
},
],
});
} else if (errs.length === 1) {
CompilerError.throwInvalidJS({
reason: `Unable to unify types because ${printUnificationError(errs[0])}`,
loc,
});
} else {
const messages = errs
.map(err => `\t* ${printUnificationError(err)}`)
.join('\n');
CompilerError.throwInvalidJS({
reason: `Unable to unify types because:\n${messages}`,
loc,
});
}
}
}
export function unresolvableTypeVariable(
id: VariableId,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Unable to resolve free variable ${id} to a concrete type`,
loc,
});
}
export function cannotAddVoid(explicit: boolean, loc: SourceLocation): never {
if (explicit) {
CompilerError.throwInvalidJS({
reason: `Undefined is not a valid operand of \`+\``,
loc,
});
} else {
CompilerError.throwInvalidJS({
reason: `Value may be undefined, which is not a valid operand of \`+\``,
loc,
});
}
}
export function unsupportedTypeAnnotation(
desc: string,
loc: SourceLocation,
): never {
CompilerError.throwInvalidJS({
reason: `Typedchecker does not currently support type annotation: ${desc}`,
loc,
});
}
export function checkTypeArgumentArity(
desc: string,
expected: number,
actual: number,
loc: SourceLocation,
): void {
if (expected !== actual) {
CompilerError.throwInvalidJS({
reason: `Expected ${desc} to have ${expected} type parameters, got ${actual}`,
loc,
});
}
}
export function notAFunction(desc: string, loc: SourceLocation): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} because it is not a function`,
loc,
});
}
export function notAPolymorphicFunction(
desc: string,
loc: SourceLocation,
): void {
CompilerError.throwInvalidJS({
reason: `Cannot call ${desc} with type arguments because it is not a polymorphic function`,
loc,
});
}

View File

@@ -1,312 +0,0 @@
import {GeneratedSource} from '../HIR';
import {assertExhaustive} from '../Utils/utils';
import {unsupportedLanguageFeature} from './TypeErrors';
import {
ConcreteType,
ResolvedType,
TypeParameter,
TypeParameterId,
DEBUG,
printConcrete,
printType,
} from './Types';
export function substitute(
type: ConcreteType<ResolvedType>,
typeParameters: Array<TypeParameter<ResolvedType>>,
typeArguments: Array<ResolvedType>,
): ResolvedType {
const substMap = new Map<TypeParameterId, ResolvedType>();
for (let i = 0; i < typeParameters.length; i++) {
// TODO: Length checks to make sure type params match up with args
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substMap.set(typeParameter.id, typeArgument);
}
const substitutionFunction = (t: ResolvedType): ResolvedType => {
// TODO: We really want a stateful mapper or visitor here so that we can model nested polymorphic types
if (t.type.kind === 'Generic' && substMap.has(t.type.id)) {
const substitutedType = substMap.get(t.type.id)!;
return substitutedType;
}
return {
kind: 'Concrete',
type: mapType(substitutionFunction, t.type),
platform: t.platform,
};
};
const substituted = mapType(substitutionFunction, type);
if (DEBUG) {
let substs = '';
for (let i = 0; i < typeParameters.length; i++) {
const typeParameter = typeParameters[i];
const typeArgument = typeArguments[i];
substs += `[${typeParameter.name}${typeParameter.id} := ${printType(typeArgument)}]`;
}
console.log(
`${printConcrete(type, printType)}${substs} = ${printConcrete(substituted, printType)}`,
);
}
return {kind: 'Concrete', type: substituted, platform: /* TODO */ 'shared'};
}
export function mapType<T, U>(
f: (t: T) => U,
type: ConcreteType<T>,
): ConcreteType<U> {
switch (type.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
return type;
case 'Nullable':
return {
kind: 'Nullable',
type: f(type.type),
};
case 'Array':
return {
kind: 'Array',
element: f(type.element),
};
case 'Set':
return {
kind: 'Set',
element: f(type.element),
};
case 'Map':
return {
kind: 'Map',
key: f(type.key),
value: f(type.value),
};
case 'Function':
return {
kind: 'Function',
typeParameters:
type.typeParameters?.map(param => ({
id: param.id,
name: param.name,
bound: f(param.bound),
})) ?? null,
params: type.params.map(f),
returnType: f(type.returnType),
};
case 'Component': {
return {
kind: 'Component',
children: type.children != null ? f(type.children) : null,
props: new Map([...type.props.entries()].map(([k, v]) => [k, f(v)])),
};
}
case 'Generic':
return {
kind: 'Generic',
id: type.id,
bound: f(type.bound),
};
case 'Object':
return type;
case 'Tuple':
return {
kind: 'Tuple',
id: type.id,
members: type.members.map(f),
};
case 'Structural':
return type;
case 'Enum':
case 'Union':
case 'Instance':
unsupportedLanguageFeature(type.kind, GeneratedSource);
default:
assertExhaustive(type, 'Unknown type kind');
}
}
export function diff<R, T>(
a: ConcreteType<T>,
b: ConcreteType<T>,
onChild: (a: T, b: T) => R,
onChildMismatch: (child: R, cur: R) => R,
onMismatch: (a: ConcreteType<T>, b: ConcreteType<T>, cur: R) => R,
init: R,
): R {
let errors = init;
// Check if kinds match
if (a.kind !== b.kind) {
errors = onMismatch(a, b, errors);
return errors;
}
// Based on kind, check other properties
switch (a.kind) {
case 'Mixed':
case 'Number':
case 'String':
case 'Boolean':
case 'Void':
// Simple types, no further checks needed
break;
case 'Nullable':
// Check the nested type
errors = onChildMismatch(onChild(a.type, (b as typeof a).type), errors);
break;
case 'Array':
case 'Set':
// Check the element type
errors = onChildMismatch(
onChild(a.element, (b as typeof a).element),
errors,
);
break;
case 'Map':
// Check both key and value types
errors = onChildMismatch(onChild(a.key, (b as typeof a).key), errors);
errors = onChildMismatch(onChild(a.value, (b as typeof a).value), errors);
break;
case 'Function': {
const bFunc = b as typeof a;
// Check type parameters
if ((a.typeParameters == null) !== (bFunc.typeParameters == null)) {
errors = onMismatch(a, b, errors);
}
if (a.typeParameters != null && bFunc.typeParameters != null) {
if (a.typeParameters.length !== bFunc.typeParameters.length) {
errors = onMismatch(a, b, errors);
}
// Type parameters are just numbers, so we can compare them directly
for (let i = 0; i < a.typeParameters.length; i++) {
if (a.typeParameters[i] !== bFunc.typeParameters[i]) {
errors = onMismatch(a, b, errors);
}
}
}
// Check parameters
if (a.params.length !== bFunc.params.length) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.params.length; i++) {
errors = onChildMismatch(onChild(a.params[i], bFunc.params[i]), errors);
}
// Check return type
errors = onChildMismatch(onChild(a.returnType, bFunc.returnType), errors);
break;
}
case 'Component': {
const bComp = b as typeof a;
// Check children
if (a.children !== bComp.children) {
errors = onMismatch(a, b, errors);
}
// Check props
if (a.props.size !== bComp.props.size) {
errors = onMismatch(a, b, errors);
}
for (const [k, v] of a.props) {
const bProp = bComp.props.get(k);
if (bProp == null) {
errors = onMismatch(a, b, errors);
} else {
errors = onChildMismatch(onChild(v, bProp), errors);
}
}
break;
}
case 'Generic': {
// Check that the type parameter IDs match
if (a.id !== (b as typeof a).id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Structural': {
const bStruct = b as typeof a;
// Check that the structural IDs match
if (a.id !== bStruct.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Object': {
const bNom = b as typeof a;
// Check that the nominal IDs match
if (a.id !== bNom.id) {
errors = onMismatch(a, b, errors);
}
break;
}
case 'Tuple': {
const bTuple = b as typeof a;
// Check that the tuple IDs match
if (a.id !== bTuple.id) {
errors = onMismatch(a, b, errors);
}
for (let i = 0; i < a.members.length; i++) {
errors = onChildMismatch(
onChild(a.members[i], bTuple.members[i]),
errors,
);
}
break;
}
case 'Enum':
case 'Instance':
case 'Union': {
unsupportedLanguageFeature(a.kind, GeneratedSource);
}
default:
assertExhaustive(a, 'Unknown type kind');
}
return errors;
}
export function filterOptional(t: ResolvedType): ResolvedType {
if (t.kind === 'Concrete' && t.type.kind === 'Nullable') {
return t.type.type;
}
return t;
}

View File

@@ -38,13 +38,7 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
CompilerError.invariant(instr.lvalue.identifier.name === null, {
reason: `Expected all lvalues to be temporaries`,
description: `Found named lvalue \`${instr.lvalue.identifier.name}\``,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
});
CompilerError.invariant(!assignments.has(instr.lvalue.identifier.id), {
@@ -52,13 +46,7 @@ export function assertConsistentIdentifiers(fn: HIRFunction): void {
description: `Found duplicate assignment of '${printPlace(
instr.lvalue,
)}'`,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
});
assignments.add(instr.lvalue.identifier.id);
@@ -89,13 +77,7 @@ function validate(
CompilerError.invariant(identifier === previous, {
reason: `Duplicate identifier object`,
description: `Found duplicate identifier object for id ${identifier.id}`,
details: [
{
kind: 'error',
loc: loc ?? GeneratedSource,
message: null,
},
],
loc: loc ?? GeneratedSource,
suggestions: null,
});
}

View File

@@ -18,13 +18,7 @@ export function assertTerminalSuccessorsExist(fn: HIRFunction): void {
description: `Block bb${successor} does not exist for terminal '${printTerminal(
block.terminal,
)}'`,
details: [
{
kind: 'error',
loc: (block.terminal as any).loc ?? GeneratedSource,
message: null,
},
],
loc: (block.terminal as any).loc ?? GeneratedSource,
suggestions: null,
});
return successor;
@@ -39,26 +33,14 @@ export function assertTerminalPredsExist(fn: HIRFunction): void {
CompilerError.invariant(predBlock != null, {
reason: 'Expected predecessor block to exist',
description: `Block ${block.id} references non-existent ${pred}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
CompilerError.invariant(
[...eachTerminalSuccessor(predBlock.terminal)].includes(block.id),
{
reason: 'Terminal successor does not reference correct predecessor',
description: `Block bb${block.id} has bb${predBlock.id} as a predecessor, but bb${predBlock.id}'s successors do not include bb${block.id}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
}

View File

@@ -131,13 +131,7 @@ export function recursivelyTraverseItems<T, TContext>(
CompilerError.invariant(disjoint || nested, {
reason: 'Invalid nesting in program blocks or scopes',
description: `Items overlap but are not nested: ${maybeParentRange.start}:${maybeParentRange.end}(${currRange.start}:${currRange.end})`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
if (disjoint) {
exit(maybeParent, context);

View File

@@ -57,13 +57,7 @@ function validateMutableRange(
{
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
description: `${printPlace(place)} in ${description}`,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
loc: place.loc,
},
);
}

View File

@@ -12,7 +12,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
import {assertExhaustive, hasNode} from '../Utils/utils';
@@ -47,7 +47,6 @@ import {
makePropertyLiteral,
makeType,
promoteTemporary,
validateIdentifierName,
} from './HIR';
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
import {BuiltInArrayId} from './ObjectShape';
@@ -108,10 +107,10 @@ export function lower(
if (binding.kind !== 'Identifier') {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Invariant,
reason: 'Could not find binding',
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\``,
}).withDetails({
severity: ErrorSeverity.Invariant,
category: 'Could not find binding',
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Could not find binding',
@@ -172,10 +171,10 @@ export function lower(
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Todo,
reason: `Handle ${param.node.type} parameters`,
description: `[BuildHIR] Add support for ${param.node.type} parameters`,
}).withDetails({
severity: ErrorSeverity.Todo,
category: `Handle ${param.node.type} parameters`,
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
}).withDetail({
kind: 'error',
loc: param.node.loc ?? null,
message: 'Unsupported parameter type',
@@ -203,10 +202,10 @@ export function lower(
} else {
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
}).withDetails({
severity: ErrorSeverity.InvalidJS,
category: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
}).withDetail({
kind: 'error',
loc: body.node.loc ?? null,
message: 'Expected a block statement or expression',
@@ -214,17 +213,7 @@ export function lower(
);
}
let validatedId: HIRFunction['id'] = null;
if (id != null) {
const idResult = validateIdentifierName(id);
if (idResult.isErr()) {
builder.errors.merge(idResult.unwrapErr());
} else {
validatedId = idResult.unwrap().value;
}
}
if (builder.errors.hasAnyErrors()) {
if (builder.errors.hasErrors()) {
return Err(builder.errors);
}
@@ -245,8 +234,7 @@ export function lower(
);
return Ok({
id: validatedId,
nameHint: null,
id,
params,
fnType: bindings == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
@@ -284,7 +272,7 @@ function lowerStatement(
builder.errors.push({
reason:
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -451,13 +439,7 @@ function lowerStatement(
reason: 'Expected to find binding for hoisted identifier',
description: `Could not find a binding for ${id.node.name}`,
suggestions: null,
details: [
{
kind: 'error',
loc: id.node.loc ?? GeneratedSource,
message: null,
},
],
loc: id.node.loc ?? GeneratedSource,
});
if (builder.environment.isHoistedIdentifier(binding.identifier)) {
// Already hoisted
@@ -477,7 +459,7 @@ function lowerStatement(
kind = InstructionKind.HoistedFunction;
} else if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Unsupported declaration type for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
suggestions: null,
@@ -486,7 +468,7 @@ function lowerStatement(
continue;
} else {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason: 'Handle non-const declarations for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
suggestions: null,
@@ -499,14 +481,7 @@ function lowerStatement(
CompilerError.invariant(identifier.kind === 'Identifier', {
reason:
'Expected hoisted binding to be a local identifier, not a global',
description: null,
details: [
{
kind: 'error',
loc: id.node.loc ?? GeneratedSource,
message: null,
},
],
loc: id.node.loc ?? GeneratedSource,
});
const place: Place = {
effect: Effect.Unknown,
@@ -573,7 +548,7 @@ function lowerStatement(
builder.errors.push({
reason:
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -645,7 +620,7 @@ function lowerStatement(
if (test.node == null) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -796,7 +771,7 @@ function lowerStatement(
if (hasDefault) {
builder.errors.push({
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: case_.node.loc ?? null,
suggestions: null,
});
@@ -868,7 +843,7 @@ function lowerStatement(
if (nodeKind === 'var') {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -896,7 +871,7 @@ function lowerStatement(
if (binding.kind !== 'Identifier') {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: id.node.loc ?? null,
suggestions: null,
});
@@ -913,7 +888,7 @@ function lowerStatement(
const declRangeStart = declaration.parentPath.node.start!;
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: id.node.loc ?? null,
suggestions: [
{
@@ -960,7 +935,7 @@ function lowerStatement(
builder.errors.push({
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1039,13 +1014,7 @@ function lowerStatement(
CompilerError.invariant(stmt.get('id').type === 'Identifier', {
reason: 'function declarations must have a name',
description: null,
details: [
{
kind: 'error',
loc: stmt.node.loc ?? null,
message: null,
},
],
loc: stmt.node.loc ?? null,
suggestions: null,
});
const id = stmt.get('id') as NodePath<t.Identifier>;
@@ -1074,7 +1043,7 @@ function lowerStatement(
if (stmt.node.await) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1145,13 +1114,7 @@ function lowerStatement(
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
description: null,
details: [
{
kind: 'error',
loc: left.node.loc ?? null,
message: null,
},
],
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get('id');
@@ -1166,15 +1129,8 @@ function lowerStatement(
test = lowerValueToTemporary(builder, assign);
} else {
CompilerError.invariant(left.isLVal(), {
loc: leftLoc,
reason: 'Expected ForOf init to be a variable declaration or lval',
description: null,
details: [
{
kind: 'error',
loc: leftLoc,
message: null,
},
],
});
const assign = lowerAssignment(
builder,
@@ -1251,13 +1207,7 @@ function lowerStatement(
CompilerError.invariant(declarations.length === 1, {
reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
description: null,
details: [
{
kind: 'error',
loc: left.node.loc ?? null,
message: null,
},
],
loc: left.node.loc ?? null,
suggestions: null,
});
const id = declarations[0].get('id');
@@ -1272,15 +1222,8 @@ function lowerStatement(
test = lowerValueToTemporary(builder, assign);
} else {
CompilerError.invariant(left.isLVal(), {
loc: leftLoc,
reason: 'Expected ForIn init to be a variable declaration or lval',
description: null,
details: [
{
kind: 'error',
loc: leftLoc,
message: null,
},
],
});
const assign = lowerAssignment(
builder,
@@ -1332,7 +1275,7 @@ function lowerStatement(
if (!hasNode(handlerPath)) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1341,7 +1284,7 @@ function lowerStatement(
if (hasNode(stmt.get('finalizer'))) {
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1434,7 +1377,7 @@ function lowerStatement(
builder.errors.push({
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
category: ErrorCategory.UnsupportedSyntax,
severity: ErrorSeverity.UnsupportedJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1454,7 +1397,7 @@ function lowerStatement(
builder.errors.push({
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
category: ErrorCategory.UnsupportedSyntax,
severity: ErrorSeverity.UnsupportedJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1483,7 +1426,7 @@ function lowerStatement(
builder.errors.push({
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1498,7 +1441,7 @@ function lowerStatement(
builder.errors.push({
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1568,6 +1511,20 @@ function lowerObjectPropertyKey(
name: key.node.value,
};
} else if (property.node.computed && key.isExpression()) {
if (!key.isIdentifier() && !key.isMemberExpression()) {
/*
* NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
* see fixture
* error.object-expression-computed-key-modified-during-after-construction.js
*/
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
return null;
}
const place = lowerExpressionToTemporary(builder, key);
return {
kind: 'computed',
@@ -1587,7 +1544,7 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1644,7 +1601,7 @@ function lowerExpression(
if (!valuePath.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: valuePath.node.loc ?? null,
suggestions: null,
});
@@ -1670,7 +1627,7 @@ function lowerExpression(
if (propertyPath.node.kind !== 'method') {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1691,7 +1648,7 @@ function lowerExpression(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1724,7 +1681,7 @@ function lowerExpression(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -1744,7 +1701,7 @@ function lowerExpression(
builder.errors.push({
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
description: `Got a \`${calleePath.node.type}\``,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1770,7 +1727,7 @@ function lowerExpression(
if (!calleePath.isExpression()) {
builder.errors.push({
reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1804,7 +1761,7 @@ function lowerExpression(
if (!leftPath.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1816,7 +1773,7 @@ function lowerExpression(
if (operator === '|>') {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1845,7 +1802,7 @@ function lowerExpression(
if (last === null) {
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2057,7 +2014,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
description: `Expected an LVal, got: ${left.type}`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
@@ -2085,7 +2042,7 @@ function lowerExpression(
if (binaryOperator == null) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2184,7 +2141,7 @@ function lowerExpression(
default: {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2223,7 +2180,7 @@ function lowerExpression(
if (!attribute.isJSXAttribute()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: attribute.node.loc ?? null,
suggestions: null,
});
@@ -2236,7 +2193,7 @@ function lowerExpression(
if (propName.indexOf(':') !== -1) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: namePath.node.loc ?? null,
suggestions: null,
});
@@ -2245,13 +2202,7 @@ function lowerExpression(
CompilerError.invariant(namePath.isJSXNamespacedName(), {
reason: 'Refinement',
description: null,
details: [
{
kind: 'error',
loc: namePath.node.loc ?? null,
message: null,
},
],
loc: namePath.node.loc ?? null,
suggestions: null,
});
const namespace = namePath.node.namespace.name;
@@ -2272,7 +2223,7 @@ function lowerExpression(
if (!valueExpr.isJSXExpressionContainer()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: valueExpr.node?.loc ?? null,
suggestions: null,
});
@@ -2282,7 +2233,7 @@ function lowerExpression(
if (!expression.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: valueExpr.node.loc ?? null,
suggestions: null,
});
@@ -2305,14 +2256,8 @@ function lowerExpression(
// This is already checked in builder.resolveIdentifier
CompilerError.invariant(tagIdentifier.kind !== 'Identifier', {
reason: `<${tagName}> tags should be module-level imports`,
loc: openingIdentifier.node.loc ?? GeneratedSource,
description: null,
details: [
{
kind: 'error',
loc: openingIdentifier.node.loc ?? GeneratedSource,
message: null,
},
],
suggestions: null,
});
}
@@ -2345,8 +2290,8 @@ function lowerExpression(
for (const [name, locations] of Object.entries(fbtLocations)) {
if (locations.length > 1) {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support duplicate fbt tags',
severity: ErrorSeverity.Todo,
category: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
@@ -2406,7 +2351,7 @@ function lowerExpression(
builder.errors.push({
reason:
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2416,13 +2361,7 @@ function lowerExpression(
reason:
"there should be only one quasi as we don't support interpolations yet",
description: null,
details: [
{
kind: 'error',
loc: expr.node.loc ?? null,
message: null,
},
],
loc: expr.node.loc ?? null,
suggestions: null,
});
const value = expr.get('quasi').get('quasis').at(0)!.node.value;
@@ -2430,7 +2369,7 @@ function lowerExpression(
builder.errors.push({
reason:
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2452,7 +2391,7 @@ function lowerExpression(
if (subexprs.length !== quasis.length - 1) {
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2462,7 +2401,7 @@ function lowerExpression(
if (subexprs.some(e => !e.isExpression())) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2504,7 +2443,7 @@ function lowerExpression(
} else {
builder.errors.push({
reason: `Only object properties can be deleted`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2519,7 +2458,7 @@ function lowerExpression(
} else if (expr.node.operator === 'throw') {
builder.errors.push({
reason: `Throw expressions are not supported`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2640,7 +2579,7 @@ function lowerExpression(
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2648,7 +2587,7 @@ function lowerExpression(
} else if (builder.isContextIdentifier(argument)) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2665,10 +2604,10 @@ function lowerExpression(
* lowerIdentifierForAssignment should have already reported an error if it returned null,
* we check here just in case
*/
if (!builder.errors.hasAnyErrors()) {
if (!builder.errors.hasErrors()) {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: exprLoc,
suggestions: null,
});
@@ -2677,7 +2616,7 @@ function lowerExpression(
} else if (lvalue.kind === 'Global') {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprLoc,
suggestions: null,
});
@@ -2732,7 +2671,7 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2741,7 +2680,7 @@ function lowerExpression(
default: {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2820,13 +2759,7 @@ function lowerOptionalMemberExpression(
CompilerError.invariant(object !== null, {
reason: 'Satisfy type checker',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
@@ -3044,7 +2977,7 @@ function lowerReorderableExpression(
if (!isReorderableExpression(builder, expr, true)) {
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -3067,12 +3000,6 @@ function isReorderableExpression(
return true;
}
}
case 'TSInstantiationExpression': {
const innerExpr = (expr as NodePath<t.TSInstantiationExpression>).get(
'expression',
) as NodePath<t.Expression>;
return isReorderableExpression(builder, innerExpr, allowLocalIdentifiers);
}
case 'RegExpLiteral':
case 'StringLiteral':
case 'NumericLiteral':
@@ -3246,7 +3173,7 @@ function lowerArguments(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: argPath.node.loc ?? null,
suggestions: null,
});
@@ -3281,7 +3208,7 @@ function lowerMemberExpression(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3302,7 +3229,7 @@ function lowerMemberExpression(
if (!propertyNode.isExpression()) {
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3361,7 +3288,7 @@ function lowerJsxElementName(
builder.errors.push({
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
description: `Got \`${namespace}\` : \`${name}\``,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3375,7 +3302,7 @@ function lowerJsxElementName(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3400,13 +3327,7 @@ function lowerJsxMemberExpression(
CompilerError.invariant(object.isJSXIdentifier(), {
reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
description: null,
details: [
{
kind: 'error',
loc: object.node.loc ?? null,
message: null,
},
],
loc: object.node.loc ?? null,
suggestions: null,
});
@@ -3448,13 +3369,7 @@ function lowerJsxElement(
CompilerError.invariant(expression.isExpression(), {
reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
description: null,
details: [
{
kind: 'error',
loc: expression.node.loc ?? null,
message: null,
},
],
loc: expression.node.loc ?? null,
suggestions: null,
});
return lowerExpressionToTemporary(builder, expression);
@@ -3485,7 +3400,7 @@ function lowerJsxElement(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3567,14 +3482,17 @@ function lowerFunctionToValue(
): InstructionValue {
const exprNode = expr.node;
const exprLoc = exprNode.loc ?? GeneratedSource;
let name: string | null = null;
if (expr.isFunctionExpression()) {
name = expr.get('id')?.node?.name ?? null;
}
const loweredFunc = lowerFunction(builder, expr);
if (!loweredFunc) {
return {kind: 'UnsupportedNode', node: exprNode, loc: exprLoc};
}
return {
kind: 'FunctionExpression',
name: loweredFunc.func.id,
nameHint: null,
name,
type: expr.node.type,
loc: exprLoc,
loweredFunc,
@@ -3669,7 +3587,7 @@ function lowerIdentifier(
reason: `The 'eval' function is not supported`,
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
category: ErrorCategory.UnsupportedSyntax,
severity: ErrorSeverity.UnsupportedJS,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3725,7 +3643,7 @@ function lowerIdentifierForAssignment(
// Else its an internal error bc we couldn't find the binding
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
loc: path.node.loc ?? null,
suggestions: null,
});
@@ -3737,7 +3655,7 @@ function lowerIdentifierForAssignment(
) {
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
@@ -3794,7 +3712,7 @@ function lowerAssignment(
if (kind === InstructionKind.Const && !isHoistedIdentifier) {
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3808,7 +3726,7 @@ function lowerAssignment(
) {
builder.errors.push({
reason: `Unexpected context variable kind`,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3852,13 +3770,7 @@ function lowerAssignment(
CompilerError.invariant(kind === InstructionKind.Reassign, {
reason: 'MemberExpression may only appear in an assignment expression',
description: null,
details: [
{
kind: 'error',
loc: lvaluePath.node.loc ?? null,
message: null,
},
],
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});
const lvalue = lvaluePath as NodePath<t.MemberExpression>;
@@ -3885,7 +3797,7 @@ function lowerAssignment(
} else {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3897,7 +3809,7 @@ function lowerAssignment(
builder.errors.push({
reason:
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3962,7 +3874,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4001,7 +3913,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4074,7 +3986,7 @@ function lowerAssignment(
if (!argument.isIdentifier()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: argument.node.loc ?? null,
suggestions: null,
});
@@ -4105,7 +4017,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: property.node.loc ?? GeneratedSource,
@@ -4122,7 +4034,7 @@ function lowerAssignment(
if (!property.isObjectProperty()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4131,7 +4043,7 @@ function lowerAssignment(
if (property.node.computed) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4145,7 +4057,7 @@ function lowerAssignment(
if (!element.isLVal()) {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -4167,7 +4079,7 @@ function lowerAssignment(
continue;
} else if (identifier.kind === 'Global') {
builder.errors.push({
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4316,7 +4228,7 @@ function lowerAssignment(
default: {
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});

View File

@@ -234,14 +234,7 @@ function pushEndScopeTerminal(
const fallthroughId = context.fallthroughs.get(scope.id);
CompilerError.invariant(fallthroughId != null, {
reason: 'Expected scope to exist',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
context.rewrites.push({
kind: 'EndScope',

View File

@@ -269,14 +269,7 @@ class PropertyPathRegistry {
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
reason:
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
description: null,
details: [
{
kind: 'error',
loc: identifier.loc,
message: null,
},
],
loc: identifier.loc,
});
}
return rootNode;
@@ -454,32 +447,6 @@ function collectNonNullsInBlocks(
assumedNonNullObjects.add(entry);
}
}
} else if (
fn.env.config.enablePreserveExistingMemoizationGuarantees &&
instr.value.kind === 'StartMemoize' &&
instr.value.deps != null
) {
for (const dep of instr.value.deps) {
if (dep.root.kind === 'NamedLocal') {
if (
!isImmutableAtInstr(dep.root.value.identifier, instr.id, context)
) {
continue;
}
for (let i = 0; i < dep.path.length; i++) {
const pathEntry = dep.path[i]!;
if (pathEntry.optional) {
break;
}
const depNode = context.registry.getOrCreateProperty({
identifier: dep.root.value.identifier,
path: dep.path.slice(0, i),
reactive: dep.root.value.reactive,
});
assumedNonNullObjects.add(depNode);
}
}
}
}
}
@@ -531,14 +498,7 @@ function propagateNonNull(
if (node == null) {
CompilerError.invariant(false, {
reason: `Bad node ${nodeId}, kind: ${direction}`,
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
}
const neighbors = Array.from(
@@ -610,14 +570,7 @@ function propagateNonNull(
CompilerError.invariant(i++ < 100, {
reason:
'[CollectHoistablePropertyLoads] fixed point iteration did not terminate after 100 loops',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
changed = false;
@@ -649,13 +602,7 @@ export function assertNonNull<T extends NonNullable<U>, U>(
CompilerError.invariant(value != null, {
reason: 'Unexpected null',
description: source != null ? `(from ${source})` : null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
return value;
}

View File

@@ -186,13 +186,7 @@ function matchOptionalTestBlock(
reason:
'[OptionalChainDeps] Inconsistent optional chaining property load',
description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`,
details: [
{
kind: 'error',
loc: propertyLoad.loc,
message: null,
},
],
loc: propertyLoad.loc,
},
);
@@ -200,14 +194,7 @@ function matchOptionalTestBlock(
storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected storeLocal',
description: null,
details: [
{
kind: 'error',
loc: propertyLoad.loc,
message: null,
},
],
loc: propertyLoad.loc,
},
);
if (
@@ -224,14 +211,7 @@ function matchOptionalTestBlock(
alternate.instructions[1].value.kind === 'StoreLocal',
{
reason: 'Unexpected alternate structure',
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
},
);
@@ -267,14 +247,7 @@ function traverseOptionalBlock(
if (maybeTest.terminal.kind === 'branch') {
CompilerError.invariant(optional.terminal.optional, {
reason: '[OptionalChainDeps] Expect base case to be always optional',
description: null,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
});
/**
* Optional base expressions are currently within value blocks which cannot
@@ -312,14 +285,7 @@ function traverseOptionalBlock(
maybeTest.instructions.at(-1)!.lvalue.identifier.id,
{
reason: '[OptionalChainDeps] Unexpected test expression',
description: null,
details: [
{
kind: 'error',
loc: maybeTest.terminal.loc,
message: null,
},
],
loc: maybeTest.terminal.loc,
},
);
baseObject = {
@@ -408,14 +374,7 @@ function traverseOptionalBlock(
reason:
'[OptionalChainDeps] Unexpected instructions an inner optional block. ' +
'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains',
description: null,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
});
}
const matchConsequentResult = matchOptionalTestBlock(test, context.blocks);
@@ -428,13 +387,7 @@ function traverseOptionalBlock(
{
reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough',
description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`,
details: [
{
kind: 'error',
loc: optional.terminal.loc,
message: null,
},
],
loc: optional.terminal.loc,
},
);
const load = {

View File

@@ -24,14 +24,7 @@ export function computeUnconditionalBlocks(fn: HIRFunction): Set<BlockId> {
CompilerError.invariant(!unconditionalBlocks.has(current), {
reason:
'Internal error: non-terminating loop in ComputeUnconditionalBlocks',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
unconditionalBlocks.add(current);

View File

@@ -1,109 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Effect, ValueKind} from '..';
import {TypeConfig} from './TypeSchema';
/**
* Libraries developed before we officially documented the [Rules of React](https://react.dev/reference/rules)
* implement APIs which cannot be memoized safely, either via manual or automatic memoization.
*
* Any non-hook API that is designed to be called during render (not events/effects) should be safe to memoize:
*
* ```js
* function Component() {
* const {someFunction} = useLibrary();
* // it should always be safe to memoize functions like this
* const result = useMemo(() => someFunction(), [someFunction]);
* }
* ```
*
* However, some APIs implement "interior mutability" — mutating values rather than copying into a new value
* and setting state with the new value. Such functions (`someFunction()` in the example) could return different
* values even though the function itself is the same object. This breaks memoization, since React relies on
* the outer object (or function) changing if part of its value has changed.
*
* Given that we didn't have the Rules of React precisely documented prior to the introduction of React compiler,
* it's understandable that some libraries accidentally shipped APIs that break this rule. However, developers
* can easily run into pitfalls with these APIs. They may manually memoize them, which can break their app. Or
* they may try using React Compiler, and think that the compiler has broken their code.
*
* To help ensure that developers can successfully use the compiler with existing code, this file teaches the
* compiler about specific APIs that are known to be incompatible with memoization. We've tried to be as precise
* as possible.
*
* The React team is open to collaborating with library authors to help develop compatible versions of these APIs,
* and we have already reached out to the teams who own any API listed here to ensure they are aware of the issue.
*/
export function defaultModuleTypeProvider(
moduleName: string,
): TypeConfig | null {
switch (moduleName) {
case 'react-hook-form': {
return {
kind: 'object',
properties: {
useForm: {
kind: 'hook',
returnType: {
kind: 'object',
properties: {
// Only the `watch()` function returned by react-hook-form's `useForm()` API is incompatible
watch: {
kind: 'function',
positionalParams: [],
restParam: Effect.Read,
calleeEffect: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
returnValueKind: ValueKind.Mutable,
knownIncompatible: `React Hook Form's \`useForm()\` API returns a \`watch()\` function which cannot be memoized safely.`,
},
},
},
},
},
};
}
case '@tanstack/react-table': {
return {
kind: 'object',
properties: {
/*
* Many of the properties of `useReactTable()`'s return value are incompatible, so we mark the entire hook
* as incompatible
*/
useReactTable: {
kind: 'hook',
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `TanStack Table's \`useReactTable()\` API returns functions that cannot be memoized safely`,
},
},
};
}
case '@tanstack/react-virtual': {
return {
kind: 'object',
properties: {
/*
* Many of the properties of `useVirtualizer()`'s return value are incompatible, so we mark the entire hook
* as incompatible
*/
useVirtualizer: {
kind: 'hook',
positionalParams: [],
restParam: Effect.Read,
returnType: {kind: 'type', name: 'Any'},
knownIncompatible: `TanStack Virtual's \`useVirtualizer()\` API returns functions that cannot be memoized safely`,
},
},
};
}
}
return null;
}

View File

@@ -54,14 +54,7 @@ export class ReactiveScopeDependencyTreeHIR {
prevAccessType == null || prevAccessType === accessType,
{
reason: 'Conflicting access types',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
let nextNode = currNode.properties.get(path[i].property);
@@ -97,13 +90,7 @@ export class ReactiveScopeDependencyTreeHIR {
CompilerError.invariant(reactive === rootNode.reactive, {
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
description: `Identifier ${printIdentifier(identifier)}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
}
return rootNode;

View File

@@ -89,13 +89,7 @@ export class Dominator<T> {
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return dominator === id ? null : dominator;
@@ -136,13 +130,7 @@ export class PostDominator<T> {
CompilerError.invariant(dominator !== undefined, {
reason: 'Unknown node',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return dominator === id ? null : dominator;
@@ -187,13 +175,7 @@ function computeImmediateDominators<T>(graph: Graph<T>): Map<T, T> {
CompilerError.invariant(newIdom !== null, {
reason: `At least one predecessor must have been visited for block ${id}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});

View File

@@ -6,10 +6,10 @@
*/
import * as t from '@babel/types';
import {ZodError, z} from 'zod/v4';
import {fromZodError} from 'zod-validation-error/v4';
import {ZodError, z} from 'zod';
import {fromZodError} from 'zod-validation-error';
import {CompilerError} from '../CompilerError';
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
import {Logger, ProgramContext} from '../Entrypoint';
import {Err, Ok, Result} from '../Utils/Result';
import {
DEFAULT_GLOBALS,
@@ -49,9 +49,6 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
import {assertExhaustive} from '../Utils/utils';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -84,11 +81,21 @@ export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
export const USE_FIRE_FUNCTION_NAME = 'useFire';
export const EMIT_FREEZE_GLOBAL_GATING = '__DEV__';
export const MacroSchema = z.string();
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 CompilerMode = 'all_features' | 'no_inferred_memo';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
const HookSchema = z.object({
/*
@@ -150,7 +157,7 @@ export const EnvironmentConfigSchema = z.object({
* A function that, given the name of a module, can optionally return a description
* of that module's type signature.
*/
moduleTypeProvider: z.nullable(z.any()).default(null),
moduleTypeProvider: z.nullable(z.function().args(z.string())).default(null),
/**
* A list of functions which the application compiles as macros, where
@@ -201,7 +208,7 @@ export const EnvironmentConfigSchema = z.object({
* that if a useEffect or useCallback references a function value, that function value will be
* considered frozen, and in turn all of its referenced variables will be considered frozen as well.
*/
enablePreserveExistingMemoizationGuarantees: z.boolean().default(true),
enablePreserveExistingMemoizationGuarantees: z.boolean().default(false),
/**
* Validates that all useMemo/useCallback values are also memoized by Forget. This mode can be
@@ -218,11 +225,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validatePreserveExistingMemoizationGuarantees: z.boolean().default(true),
/**
* Validate that dependencies supplied to manual memoization calls are exhaustive.
*/
validateExhaustiveMemoizationDependencies: z.boolean().default(false),
/**
* When this is true, rather than pruning existing manual memoization but ensuring or validating
* that the memoized values remain memoized, the compiler will simply not prune existing calls to
@@ -242,10 +244,9 @@ export const EnvironmentConfigSchema = z.object({
enableUseTypeAnnotations: z.boolean().default(false),
/**
* Allows specifying a function that can populate HIR with type information from
* Flow
* Enable a new model for mutability and aliasing inference
*/
flowTypeProvider: z.nullable(z.any()).default(null),
enableNewMutationAliasingModel: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
@@ -257,8 +258,6 @@ export const EnvironmentConfigSchema = z.object({
enableFire: z.boolean().default(false),
enableNameAnonymousFunctions: z.boolean().default(false),
/**
* Enables inference and auto-insertion of effect dependencies. Takes in an array of
* configurable module and import pairs to allow for user-land experimentation. For example,
@@ -324,18 +323,6 @@ export const EnvironmentConfigSchema = z.object({
*/
validateNoSetStateInEffects: z.boolean().default(false),
/**
* Validates that effects are not used to calculate derived data which could instead be computed
* during render.
*/
validateNoDerivedComputationsInEffects: z.boolean().default(false),
/**
* Experimental: Validates that effects are not used to calculate derived data which could instead be computed
* during render. Generates a custom error message for each type of violation.
*/
validateNoDerivedComputationsInEffects_exp: z.boolean().default(false),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -370,13 +357,6 @@ export const EnvironmentConfigSchema = z.object({
validateNoCapitalizedCalls: z.nullable(z.array(z.string())).default(null),
validateBlocklistedImports: z.nullable(z.array(z.string())).default(null),
/**
* Validates that AST nodes generated during codegen have proper source locations.
* This is useful for debugging issues with source maps and Istanbul coverage.
* When enabled, the compiler will error if important source locations are missing in the generated AST.
*/
validateSourceLocations: z.boolean().default(false),
/**
* Validate against impure functions called during render
*/
@@ -630,13 +610,6 @@ export const EnvironmentConfigSchema = z.object({
*/
enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
/**
* Treat identifiers as SetState type if both
* - they are named with a "set-" prefix
* - they are called somewhere
*/
enableTreatSetIdentifiersAsStateSetters: z.boolean().default(false),
/*
* If specified a value, the compiler lowers any calls to `useContext` to use
* this value as the callee.
@@ -668,35 +641,7 @@ export const EnvironmentConfigSchema = z.object({
* Invalid:
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(true),
/**
* Validates that Components/Hooks are always defined at module level. This prevents scope
* reference errors that occur when the compiler attempts to optimize the nested component/hook
* while its parent function remains uncompiled.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
/**
* When enabled, allows setState calls in effects based on valid patterns involving refs:
* - Allow setState where the value being set is derived from a ref. This is useful where
* state needs to take into account layer information, and a layout effect reads layout
* data from a ref and sets state.
* - Allow conditionally calling setState after manually comparing previous/new values
* for changes via a ref. Relying on effect deps is insufficient for non-primitive values,
* so a ref is generally required to manually track previous values and compare prev/next
* for meaningful changes before setting state.
*/
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
/**
* Enables inference of event handler types for JSX props on built-in DOM elements.
* When enabled, functions passed to event handler props (props starting with "on")
* on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
* allows ref access within those functions since DOM event handlers are guaranteed
* by React to only execute in response to events, not during render.
*/
enableInferEventHandlers: z.boolean().default(false),
validateNoVoidUseMemo: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -736,7 +681,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
outputMode: CompilerOutputMode;
compilerMode: CompilerMode;
programContext: ProgramContext;
hasFireRewrite: boolean;
hasInferredEffect: boolean;
@@ -746,12 +691,10 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
outputMode: CompilerOutputMode,
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -762,7 +705,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.outputMode = outputMode;
this.compilerMode = compilerMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -789,13 +732,7 @@ export class Environment {
CompilerError.invariant(!this.#globals.has(hookName), {
reason: `[Globals] Found existing definition in global registry for custom hook ${hookName}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
this.#globals.set(
@@ -822,101 +759,10 @@ export class Environment {
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
if (config.flowTypeProvider != null) {
this.#flowTypeEnvironment = new FlowTypeEnv();
CompilerError.invariant(code != null, {
reason:
'Expected Environment to be initialized with source code when a Flow type provider is specified',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
return this.#flowTypeEnvironment;
}
get enableDropManualMemoization(): boolean {
switch (this.outputMode) {
case 'lint': {
// linting drops to be more compatible with compiler analysis
return true;
}
case 'client':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableMemoization(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint': {
// linting also enables memoization so that we can check if manual memoization is preserved
return true;
}
case 'ssr':
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
}
get enableValidations(): boolean {
switch (this.outputMode) {
case 'client':
case 'lint':
case 'ssr': {
return true;
}
case 'client-no-memo': {
return false;
}
default: {
assertExhaustive(
this.outputMode,
`Unexpected output mode '${this.outputMode}'`,
);
}
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
}
get nextIdentifierId(): IdentifierId {
@@ -977,22 +823,10 @@ export class Environment {
#resolveModuleType(moduleName: string, loc: SourceLocation): Global | null {
let moduleType = this.#moduleTypes.get(moduleName);
if (moduleType === undefined) {
/*
* NOTE: Zod doesn't work when specifying a function as a default, so we have to
* fallback to the default value here
*/
const moduleTypeProvider =
this.config.moduleTypeProvider ?? defaultModuleTypeProvider;
if (moduleTypeProvider == null) {
if (this.config.moduleTypeProvider == null) {
return null;
}
if (typeof moduleTypeProvider !== 'function') {
CompilerError.throwInvalidConfig({
reason: `Expected a function for \`moduleTypeProvider\``,
loc,
});
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {
@@ -1166,13 +1000,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return shape.properties.get('*') ?? null;
@@ -1197,13 +1025,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (typeof property === 'string') {
@@ -1228,13 +1050,7 @@ export class Environment {
CompilerError.invariant(shape !== undefined, {
reason: `[HIR] Forget internal error: cannot resolve shape ${shapeId}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return shape.functionType;

View File

@@ -184,13 +184,7 @@ function handleAssignment(
CompilerError.invariant(valuePath.isLVal(), {
reason: `[FindContextIdentifiers] Expected object property value to be an LVal, got: ${valuePath.type}`,
description: null,
details: [
{
kind: 'error',
loc: valuePath.node.loc ?? GeneratedSource,
message: null,
},
],
loc: valuePath.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, valuePath);
@@ -198,13 +192,7 @@ function handleAssignment(
CompilerError.invariant(property.isRestElement(), {
reason: `[FindContextIdentifiers] Invalid assumptions for babel types.`,
description: null,
details: [
{
kind: 'error',
loc: property.node.loc ?? GeneratedSource,
message: null,
},
],
loc: property.node.loc ?? GeneratedSource,
suggestions: null,
});
handleAssignment(currentFn, identifiers, property);

View File

@@ -23,14 +23,13 @@ import {
BuiltInUseInsertionEffectHookId,
BuiltInUseLayoutEffectHookId,
BuiltInUseOperatorId,
BuiltInUseOptimisticId,
BuiltInUseReducerId,
BuiltInUseRefId,
BuiltInUseStateId,
BuiltInUseTransitionId,
BuiltInWeakMapId,
BuiltInWeakSetId,
BuiltInEffectEventId,
BuiltinEffectEventId,
ReanimatedSharedValueId,
ShapeRegistry,
addFunction,
@@ -115,99 +114,6 @@ const TYPED_GLOBALS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Mutable,
}),
],
[
'entries',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'keys',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Read],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Only keys are captured, and keys are immutable
{
kind: 'ImmutableCapture',
from: '@object',
into: '@returns',
},
],
},
}),
],
[
'values',
addFunction(DEFAULT_SHAPES, [], {
positionalParams: [Effect.Capture],
restParam: null,
returnType: {kind: 'Object', shapeId: BuiltInArrayId},
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Mutable,
aliasing: {
receiver: '@receiver',
params: ['@object'],
rest: null,
returns: '@returns',
temporaries: [],
effects: [
{
kind: 'Create',
into: '@returns',
reason: ValueReason.KnownReturnSignature,
value: ValueKind.Mutable,
},
// Object values are captured into the return
{
kind: 'Capture',
from: '@object',
into: '@returns',
},
],
},
}),
],
]),
],
[
@@ -819,18 +725,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnValueKind: ValueKind.Frozen,
}),
],
[
'useOptimistic',
addHook(DEFAULT_SHAPES, {
positionalParams: [],
restParam: Effect.Freeze,
returnType: {kind: 'Object', shapeId: BuiltInUseOptimisticId},
calleeEffect: Effect.Read,
hookKind: 'useOptimistic',
returnValueKind: ValueKind.Frozen,
returnValueReason: ValueReason.State,
}),
],
[
'use',
addFunction(
@@ -876,7 +770,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
returnType: {
kind: 'Function',
return: {kind: 'Poly'},
shapeId: BuiltInEffectEventId,
shapeId: BuiltinEffectEventId,
isConstructor: false,
},
calleeEffect: Effect.Read,
@@ -1014,7 +908,6 @@ export function installTypeConfig(
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -1033,7 +926,6 @@ export function installTypeConfig(
returnValueKind: typeConfig.returnValueKind ?? ValueKind.Frozen,
noAlias: typeConfig.noAlias === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'object': {

View File

@@ -7,19 +7,13 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
} from '../CompilerError';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {Environment, ReactFunctionType} from './Environment';
import type {HookKind} from './ObjectShape';
import {Type, makeType} from './Types';
import {z} from 'zod/v4';
import {z} from 'zod';
import type {AliasingEffect} from '../Inference/AliasingEffects';
import {isReservedWord} from '../Utils/Keyword';
import {Err, Ok, Result} from '../Utils/Result';
/*
* *******************************************************************************************
@@ -58,8 +52,7 @@ export type SourceLocation = t.SourceLocation | typeof GeneratedSource;
*/
export type ReactiveFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
params: Array<Place | SpreadPattern>;
generator: boolean;
async: boolean;
@@ -281,21 +274,37 @@ export type ReactiveTryTerminal = {
// A function lowered to HIR form, ie where its body is lowered to an HIR control-flow graph
export type HIRFunction = {
loc: SourceLocation;
id: ValidIdentifierName | null;
nameHint: string | null;
id: string | null;
fnType: ReactFunctionType;
env: Environment;
params: Array<Place | SpreadPattern>;
returnTypeAnnotation: t.FlowType | t.TSType | null;
returns: Place;
context: Array<Place>;
effects: Array<FunctionEffect> | null;
body: HIR;
generator: boolean;
async: boolean;
directives: Array<string>;
aliasingEffects: Array<AliasingEffect> | null;
aliasingEffects?: Array<AliasingEffect> | null;
};
export type FunctionEffect =
| {
kind: 'GlobalMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ReactMutation';
error: CompilerErrorDetailOptions;
}
| {
kind: 'ContextMutation';
places: ReadonlySet<Place>;
effect: Effect;
loc: SourceLocation;
};
/*
* Each reactive scope may have its own control-flow, so the instructions form
* a control-flow graph. The graph comprises a set of basic blocks which reference
@@ -817,11 +826,6 @@ export type StartMemoize = {
* (e.g. useMemo without a second arg)
*/
deps: Array<ManualMemoDependency> | null;
/**
* The source location of the dependencies argument. Used for
* emitting diagnostics with a suggested replacement
*/
depsLoc: SourceLocation | null;
loc: SourceLocation;
};
export type FinishMemoize = {
@@ -1135,8 +1139,7 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: ValidIdentifierName | null;
nameHint: string | null;
name: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1311,52 +1314,22 @@ export function forkTemporaryIdentifier(
};
}
export function validateIdentifierName(
name: string,
): Result<ValidatedIdentifier, CompilerError> {
if (isReservedWord(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: 'Expected a non-reserved identifier name',
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
return Err(error);
} else if (!t.isValidIdentifier(name)) {
const error = new CompilerError();
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Syntax,
reason: `Expected a valid identifier name`,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
}).withDetails({
kind: 'error',
loc: GeneratedSource,
message: 'reserved word',
}),
);
}
return Ok({
kind: 'named',
value: name as ValidIdentifierName,
});
}
/**
* 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
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
return validateIdentifierName(name).unwrap();
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
return {
kind: 'named',
value: name as ValidIdentifierName,
};
}
/**
@@ -1368,14 +1341,8 @@ export function makeIdentifierName(name: string): ValidatedIdentifier {
export function promoteTemporary(identifier: Identifier): void {
CompilerError.invariant(identifier.name === null, {
reason: `Expected a temporary (unnamed) identifier`,
loc: GeneratedSource,
description: `Identifier already has a name, \`${identifier.name}\``,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
identifier.name = {
@@ -1398,14 +1365,8 @@ export function isPromotedTemporary(name: string): boolean {
export function promoteTemporaryJsxTag(identifier: Identifier): void {
CompilerError.invariant(identifier.name === null, {
reason: `Expected a temporary (unnamed) identifier`,
loc: GeneratedSource,
description: `Identifier already has a name, \`${identifier.name}\``,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
identifier.name = {
@@ -1573,13 +1534,7 @@ export function isMutableEffect(
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: location,
message: null,
},
],
loc: location,
suggestions: null,
});
}
@@ -1685,28 +1640,6 @@ export function areEqualPaths(a: DependencyPath, b: DependencyPath): boolean {
)
);
}
export function isSubPath(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every(
(item, ix) =>
item.property === path[ix].property &&
item.optional === path[ix].optional,
)
);
}
export function isSubPathIgnoringOptionals(
subpath: DependencyPath,
path: DependencyPath,
): boolean {
return (
subpath.length <= path.length &&
subpath.every((item, ix) => item.property === path[ix].property)
);
}
export function getPlaceScope(
id: InstructionId,
@@ -1734,13 +1667,7 @@ export function makeBlockId(id: number): BlockId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected block id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as BlockId;
@@ -1757,13 +1684,7 @@ export function makeScopeId(id: number): ScopeId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected block id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as ScopeId;
@@ -1780,13 +1701,7 @@ export function makeIdentifierId(id: number): IdentifierId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected identifier id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as IdentifierId;
@@ -1803,13 +1718,7 @@ 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,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as DeclarationId;
@@ -1826,13 +1735,7 @@ export function makeInstructionId(id: number): InstructionId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected instruction id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as InstructionId;
@@ -1850,10 +1753,6 @@ export function isPrimitiveType(id: Identifier): boolean {
return id.type.kind === 'Primitive';
}
export function isPlainObjectType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInObject';
}
export function isArrayType(id: Identifier): boolean {
return id.type.kind === 'Object' && id.type.shapeId === 'BuiltInArray';
}
@@ -1918,18 +1817,6 @@ export function isStartTransitionType(id: Identifier): boolean {
);
}
export function isUseOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Object' && id.type.shapeId === 'BuiltInUseOptimistic'
);
}
export function isSetOptimisticType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetOptimistic'
);
}
export function isSetActionStateType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInSetActionState'
@@ -1963,8 +1850,7 @@ export function isStableType(id: Identifier): boolean {
isSetActionStateType(id) ||
isDispatcherType(id) ||
isUseRefType(id) ||
isStartTransitionType(id) ||
isSetOptimisticType(id)
isStartTransitionType(id)
);
}
@@ -1975,9 +1861,8 @@ export function isStableTypeContainer(id: Identifier): boolean {
}
return (
isUseStateType(id) || // setState
isUseActionStateType(id) || // setActionState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
isUseOptimisticType(id) || // setOptimistic
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
@@ -1997,7 +1882,6 @@ export function evaluatesToStableTypeOrContainer(
case 'useActionState':
case 'useRef':
case 'useTransition':
case 'useOptimistic':
return true;
}
}

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -309,8 +309,8 @@ export default class HIRBuilder {
resolveBinding(node: t.Identifier): Identifier {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
category: ErrorCategory.Todo,
reason: 'Support local variables named `fbt`',
severity: ErrorSeverity.Todo,
category: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [
@@ -322,21 +322,6 @@ export default class HIRBuilder {
],
});
}
if (node.name === 'this') {
CompilerError.throwDiagnostic({
category: ErrorCategory.UnsupportedSyntax,
reason: '`this` is not supported syntax',
description:
'React Compiler does not support compiling functions that use `this`',
details: [
{
kind: 'error',
message: '`this` was used here',
loc: node.loc ?? GeneratedSource,
},
],
});
}
const originalName = node.name;
let name = originalName;
let index = 0;
@@ -507,13 +492,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched label',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -536,13 +515,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched label',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -578,13 +551,7 @@ export default class HIRBuilder {
{
reason: 'Mismatched loops',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
},
);
@@ -609,13 +576,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Expected a loop or switch to be in scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -636,13 +597,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Continue may only refer to a labeled loop',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -650,13 +605,7 @@ export default class HIRBuilder {
CompilerError.invariant(false, {
reason: 'Expected a loop to be in scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
@@ -679,13 +628,7 @@ function _shrink(func: HIR): void {
CompilerError.invariant(block != null, {
reason: `expected block ${blockId} to exist`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
target = getTargetIfIndirection(block);
@@ -817,13 +760,7 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
CompilerError.invariant(block != null, {
reason: '[HIRBuilder] Unexpected null block',
description: `expected block ${blockId} to exist`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);
@@ -879,13 +816,7 @@ export function markInstructionIds(func: HIR): void {
CompilerError.invariant(!visited.has(instr), {
reason: `${printInstruction(instr)} already visited!`,
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
loc: instr.loc,
suggestions: null,
});
visited.add(instr);
@@ -908,13 +839,7 @@ export function markPredecessors(func: HIR): void {
CompilerError.invariant(block != null, {
reason: 'unexpected missing block',
description: `block ${blockId}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
if (prevBlock) {
block.preds.add(prevBlock.id);

View File

@@ -61,13 +61,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
CompilerError.invariant(predecessor !== undefined, {
reason: `Expected predecessor ${predecessorId} to exist`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (predecessor.terminal.kind !== 'goto' || predecessor.kind !== 'block') {
@@ -83,13 +77,7 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
CompilerError.invariant(phi.operands.size === 1, {
reason: `Found a block with a single predecessor but where a phi has multiple (${phi.operands.size}) operands`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
const operand = Array.from(phi.operands.values())[0]!;

View File

@@ -119,13 +119,7 @@ function parseAliasingSignatureConfig(
CompilerError.invariant(!lifetimes.has(temp), {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature to have unique names for receiver, params, rest, returns, and temporaries in module '${moduleName}'`,
details: [
{
kind: 'error',
loc,
message: null,
},
],
loc,
});
const place = signatureArgument(lifetimes.size);
lifetimes.set(temp, place);
@@ -136,13 +130,7 @@ function parseAliasingSignatureConfig(
CompilerError.invariant(place != null, {
reason: `Invalid type configuration for module`,
description: `Expected aliasing signature effects to reference known names from receiver/params/rest/returns/temporaries, but '${temp}' is not a known name in '${moduleName}'`,
details: [
{
kind: 'error',
loc,
message: null,
},
],
loc,
});
return place;
}
@@ -154,7 +142,6 @@ function parseAliasingSignatureConfig(
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'ImmutableCapture':
case 'CreateFrom':
case 'Capture':
case 'Alias':
@@ -277,13 +264,7 @@ function addShape(
CompilerError.invariant(!registry.has(id), {
reason: `[ObjectShape] Could not add shape to registry: name ${id} already exists.`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
registry.set(id, shape);
@@ -304,7 +285,6 @@ export type HookKind =
| 'useTransition'
| 'useImperativeHandle'
| 'useEffectEvent'
| 'useOptimistic'
| 'Custom';
/*
@@ -351,7 +331,6 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
knownIncompatible?: string | null | undefined;
canonicalName?: string;
@@ -400,15 +379,12 @@ export const BuiltInUseReducerId = 'BuiltInUseReducer';
export const BuiltInDispatchId = 'BuiltInDispatch';
export const BuiltInUseContextHookId = 'BuiltInUseContextHook';
export const BuiltInUseTransitionId = 'BuiltInUseTransition';
export const BuiltInUseOptimisticId = 'BuiltInUseOptimistic';
export const BuiltInSetOptimisticId = 'BuiltInSetOptimistic';
export const BuiltInStartTransitionId = 'BuiltInStartTransition';
export const BuiltInFireId = 'BuiltInFire';
export const BuiltInFireFunctionId = 'BuiltInFireFunction';
export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
// See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1189,25 +1165,6 @@ addObject(BUILTIN_SHAPES, BuiltInUseTransitionId, [
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseOptimisticId, [
['0', {kind: 'Poly'}],
[
'1',
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.Freeze,
returnType: PRIMITIVE_TYPE,
calleeEffect: Effect.Read,
returnValueKind: ValueKind.Primitive,
},
BuiltInSetOptimisticId,
),
],
]);
addObject(BUILTIN_SHAPES, BuiltInUseActionStateId, [
['0', {kind: 'Poly'}],
[
@@ -1266,20 +1223,7 @@ addFunction(
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEffectEventId,
);
addFunction(
BUILTIN_SHAPES,
[],
{
positionalParams: [],
restParam: Effect.ConditionallyMutate,
returnType: {kind: 'Poly'},
calleeEffect: Effect.ConditionallyMutate,
returnValueKind: ValueKind.Mutable,
},
BuiltInEventHandlerId,
BuiltinEffectEventId,
);
/**

View File

@@ -56,9 +56,6 @@ export function printFunction(fn: HIRFunction): string {
} else {
definition += '<<anonymous>>';
}
if (fn.nameHint != null) {
definition += ` ${fn.nameHint}`;
}
if (fn.params.length !== 0) {
definition +=
'(' +
@@ -557,11 +554,23 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
const context = instrValue.loweredFunc.func.context
.map(dep => printPlace(dep))
.join(',');
const effects =
instrValue.loweredFunc.func.effects
?.map(effect => {
if (effect.kind === 'ContextMutation') {
return `ContextMutation places=[${[...effect.places]
.map(place => printPlace(place))
.join(', ')}] effect=${effect.effect}`;
} else {
return `GlobalMutation`;
}
})
.join(', ') ?? '';
const aliasingEffects =
instrValue.loweredFunc.func.aliasingEffects
?.map(printAliasingEffect)
?.join(', ') ?? '';
value = `${kind} ${name} @context[${context}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
break;
}
case 'TaggedTemplateExpression': {
@@ -599,13 +608,7 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
{
reason: 'Bad assumption about quasi length.',
description: null,
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
suggestions: null,
},
);
@@ -874,15 +877,8 @@ export function printManualMemoDependency(
} else {
CompilerError.invariant(val.root.value.identifier.name?.kind === 'named', {
reason: 'DepsValidation: expected named local variable in depslist',
description: null,
suggestions: null,
details: [
{
kind: 'error',
loc: val.root.value.loc,
message: null,
},
],
loc: val.root.value.loc,
});
rootStr = nameOnly
? val.root.value.identifier.name.value
@@ -896,8 +892,7 @@ export function printType(type: Type): string {
if (type.kind === 'Object' && type.shapeId != null) {
return `:T${type.kind}<${type.shapeId}>`;
} else if (type.kind === 'Function' && type.shapeId != null) {
const returnType = printType(type.return);
return `:T${type.kind}<${type.shapeId}>()${returnType !== '' ? `: ${returnType}` : ''}`;
return `:T${type.kind}<${type.shapeId}>`;
} else {
return `:T${type.kind}`;
}
@@ -1000,16 +995,16 @@ export function printAliasingEffect(effect: AliasingEffect): string {
case 'MutateConditionally':
case 'MutateTransitive':
case 'MutateTransitiveConditionally': {
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}${effect.kind === 'Mutate' && effect.reason?.kind === 'AssignCurrentProperty' ? ' (assign `.current`)' : ''}`;
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
}
case 'MutateFrozen': {
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'MutateGlobal': {
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'Impure': {
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.category)}`;
}
case 'Render': {
return `Render ${printPlaceForAliasEffect(effect.place)}`;

View File

@@ -86,14 +86,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const hoistables = hoistablePropertyLoads.get(scope.id);
CompilerError.invariant(hoistables != null, {
reason: '[PropagateScopeDependencies] Scope not found in tracked blocks',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
});
/**
* Step 2: Calculate hoistable dependencies.
@@ -435,14 +428,7 @@ export class DependencyCollectionContext {
const scopedDependencies = this.#dependencies.value;
CompilerError.invariant(scopedDependencies != null, {
reason: '[PropagateScopeDeps]: Unexpected scope mismatch',
description: null,
details: [
{
kind: 'error',
loc: scope.loc,
message: null,
},
],
loc: scope.loc,
});
// Restore context of previous scope

View File

@@ -53,14 +53,7 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
next.phis.size === 0 && fallthrough.phis.size === 0,
{
reason: 'Unexpected phis when merging label blocks',
description: null,
details: [
{
kind: 'error',
loc: label.terminal.loc,
message: null,
},
],
loc: label.terminal.loc,
},
);
@@ -71,14 +64,7 @@ export function pruneUnusedLabelsHIR(fn: HIRFunction): void {
fallthrough.preds.has(nextId),
{
reason: 'Unexpected block predecessors when merging label blocks',
description: null,
details: [
{
kind: 'error',
loc: label.terminal.loc,
message: null,
},
],
loc: label.terminal.loc,
},
);

View File

@@ -202,14 +202,8 @@ function writeOptionalDependency(
CompilerError.invariant(firstOptional !== -1, {
reason:
'[ScopeDependencyUtils] Internal invariant broken: expected optional path',
loc: dep.identifier.loc,
description: null,
details: [
{
kind: 'error',
loc: dep.identifier.loc,
message: null,
},
],
suggestions: null,
});
if (firstOptional === dep.path.length - 1) {
@@ -245,13 +239,7 @@ function writeOptionalDependency(
CompilerError.invariant(testIdentifier !== null, {
reason: 'Satisfy type checker',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});

View File

@@ -6,7 +6,7 @@
*/
import {isValidIdentifier} from '@babel/types';
import {z} from 'zod/v4';
import {z} from 'zod';
import {Effect, ValueKind} from '..';
import {
EffectSchema,
@@ -111,19 +111,6 @@ export const AliasEffectSchema: z.ZodType<AliasEffectConfig> = z.object({
into: LifetimeIdSchema,
});
export type ImmutableCaptureEffectConfig = {
kind: 'ImmutableCapture';
from: string;
into: string;
};
export const ImmutableCaptureEffectSchema: z.ZodType<ImmutableCaptureEffectConfig> =
z.object({
kind: z.literal('ImmutableCapture'),
from: LifetimeIdSchema,
into: LifetimeIdSchema,
});
export type CaptureEffectConfig = {
kind: 'Capture';
from: string;
@@ -200,7 +187,6 @@ export type AliasingEffectConfig =
| AssignEffectConfig
| AliasEffectConfig
| CaptureEffectConfig
| ImmutableCaptureEffectConfig
| ImpureEffectConfig
| MutateEffectConfig
| MutateTransitiveConditionallyConfig
@@ -213,7 +199,6 @@ export const AliasingEffectSchema: z.ZodType<AliasingEffectConfig> = z.union([
AssignEffectSchema,
AliasEffectSchema,
CaptureEffectSchema,
ImmutableCaptureEffectSchema,
ImpureEffectSchema,
MutateEffectSchema,
MutateTransitiveConditionallySchema,
@@ -251,7 +236,6 @@ export type FunctionTypeConfig = {
impure?: boolean | null | undefined;
canonicalName?: string | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
knownIncompatible?: string | null | undefined;
};
export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
kind: z.literal('function'),
@@ -265,7 +249,6 @@ export const FunctionTypeSchema: z.ZodType<FunctionTypeConfig> = z.object({
impure: z.boolean().nullable().optional(),
canonicalName: z.string().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
knownIncompatible: z.string().nullable().optional(),
});
export type HookTypeConfig = {
@@ -276,7 +259,6 @@ export type HookTypeConfig = {
returnValueKind?: ValueKind | null | undefined;
noAlias?: boolean | null | undefined;
aliasing?: AliasingSignatureConfig | null | undefined;
knownIncompatible?: string | null | undefined;
};
export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
kind: z.literal('hook'),
@@ -286,7 +268,6 @@ export const HookTypeSchema: z.ZodType<HookTypeConfig> = z.object({
returnValueKind: ValueKindSchema.nullable().optional(),
noAlias: z.boolean().nullable().optional(),
aliasing: AliasingSignatureSchema.nullable().optional(),
knownIncompatible: z.string().nullable().optional(),
});
export type BuiltInTypeConfig =

View File

@@ -87,13 +87,7 @@ export function makeTypeId(id: number): TypeId {
CompilerError.invariant(id >= 0 && Number.isInteger(id), {
reason: 'Expected instruction id to be a non-negative integer',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return id as TypeId;

View File

@@ -11,7 +11,6 @@ import {
BasicBlock,
BlockId,
Instruction,
InstructionKind,
InstructionValue,
makeInstructionId,
Pattern,
@@ -33,32 +32,6 @@ export function* eachInstructionLValue(
yield* eachInstructionValueLValue(instr.value);
}
export function* eachInstructionLValueWithKind(
instr: ReactiveInstruction,
): Iterable<[Place, InstructionKind]> {
switch (instr.value.kind) {
case 'DeclareContext':
case 'StoreContext':
case 'DeclareLocal':
case 'StoreLocal': {
yield [instr.value.lvalue.place, instr.value.lvalue.kind];
break;
}
case 'Destructure': {
const kind = instr.value.lvalue.kind;
for (const place of eachPatternOperand(instr.value.lvalue.pattern)) {
yield [place, kind];
}
break;
}
case 'PostfixUpdate':
case 'PrefixUpdate': {
yield [instr.value.lvalue, InstructionKind.Reassign];
break;
}
}
}
export function* eachInstructionValueLValue(
value: ReactiveValue,
): Iterable<Place> {
@@ -1260,14 +1233,7 @@ export class ScopeBlockTraversal {
CompilerError.invariant(blockInfo.scope.id === top, {
reason:
'Expected traversed block fallthrough to match top-most active scope',
description: null,
details: [
{
kind: 'error',
loc: block.instructions[0]?.loc ?? block.terminal.id,
message: null,
},
],
loc: block.instructions[0]?.loc ?? block.terminal.id,
});
this.#activeScopes.pop();
}
@@ -1281,14 +1247,7 @@ export class ScopeBlockTraversal {
!this.blockInfos.has(block.terminal.fallthrough),
{
reason: 'Expected unique scope blocks and fallthroughs',
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
loc: block.terminal.loc,
},
);
this.blockInfos.set(block.terminal.block, {

View File

@@ -50,7 +50,7 @@ export type AliasingEffect =
/**
* Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
*/
| {kind: 'Mutate'; value: Place; reason?: MutationReason | null}
| {kind: 'Mutate'; value: Place}
/**
* Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
* This should be rare.
@@ -174,8 +174,6 @@ export type AliasingEffect =
place: Place;
};
export type MutationReason = {kind: 'AssignCurrentProperty'};
export function hashEffect(effect: AliasingEffect): string {
switch (effect.kind) {
case 'Apply': {
@@ -231,7 +229,7 @@ export function hashEffect(effect: AliasingEffect): string {
effect.kind,
effect.place.identifier.id,
effect.error.severity,
effect.error.reason,
effect.error.category,
effect.error.description,
printSourceLocation(effect.error.primaryLocation() ?? GeneratedSource),
].join(':');

View File

@@ -6,10 +6,20 @@
*/
import {CompilerError} from '../CompilerError';
import {Effect, HIRFunction, IdentifierId, makeInstructionId} from '../HIR';
import {
Effect,
HIRFunction,
Identifier,
IdentifierId,
LoweredFunction,
isRefOrRefValue,
makeInstructionId,
} from '../HIR';
import {deadCodeElimination} from '../Optimization';
import {inferReactiveScopeVariables} from '../ReactiveScopes';
import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
import {inferMutableRanges} from './InferMutableRanges';
import inferReferenceEffects from './InferReferenceEffects';
import {assertExhaustive} from '../Utils/utils';
import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
@@ -20,7 +30,12 @@ export default function analyseFunctions(func: HIRFunction): void {
switch (instr.value.kind) {
case 'ObjectMethod':
case 'FunctionExpression': {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
if (!func.env.config.enableNewMutationAliasingModel) {
lower(instr.value.loweredFunc.func);
infer(instr.value.loweredFunc);
} else {
lowerWithMutationAliasing(instr.value.loweredFunc.func);
}
/**
* Reset mutable range for outer inferReferenceEffects
@@ -78,14 +93,7 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
case 'Apply': {
CompilerError.invariant(false, {
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
description: null,
details: [
{
kind: 'error',
loc: effect.function.loc,
message: null,
},
],
loc: effect.function.loc,
});
}
case 'Mutate':
@@ -132,3 +140,58 @@ function lowerWithMutationAliasing(fn: HIRFunction): void {
value: fn,
});
}
function lower(func: HIRFunction): void {
analyseFunctions(func);
inferReferenceEffects(func, {isFunctionExpression: true});
deadCodeElimination(func);
inferMutableRanges(func);
rewriteInstructionKindsBasedOnReassignment(func);
inferReactiveScopeVariables(func);
func.env.logger?.debugLogIRs?.({
kind: 'hir',
name: 'AnalyseFunction (inner)',
value: func,
});
}
function infer(loweredFunc: LoweredFunction): void {
for (const operand of loweredFunc.func.context) {
const identifier = operand.identifier;
CompilerError.invariant(operand.effect === Effect.Unknown, {
reason:
'[AnalyseFunctions] Expected Function context effects to not have been set',
loc: operand.loc,
});
if (isRefOrRefValue(identifier)) {
/*
* TODO: this is a hack to ensure we treat functions which reference refs
* as having a capture and therefore being considered mutable. this ensures
* the function gets a mutable range which accounts for anywhere that it
* could be called, and allows us to help ensure it isn't called during
* render
*/
operand.effect = Effect.Capture;
} else if (isMutatedOrReassigned(identifier)) {
/**
* Reflects direct reassignments, PropertyStores, and ConditionallyMutate
* (directly or through maybe-aliases)
*/
operand.effect = Effect.Capture;
} else {
operand.effect = Effect.Read;
}
}
}
function isMutatedOrReassigned(id: Identifier): boolean {
/*
* This check checks for mutation and reassingnment, so the usual check for
* mutation (ie, `mutableRange.end - mutableRange.start > 1`) isn't quite
* enough.
*
* We need to track re-assignments in context refs as we need to reflect the
* re-assignment back to the captured refs.
*/
return id.mutableRange.end > id.mutableRange.start;
}

View File

@@ -1,114 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {BlockId, computePostDominatorTree, HIRFunction, Place} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
export type ControlDominators = (id: BlockId) => boolean;
/**
* Returns an object that lazily calculates whether particular blocks are controlled
* by values of interest. Which values matter are up to the caller.
*/
export function createControlDominators(
fn: HIRFunction,
isControlVariable: (place: Place) => boolean,
): ControlDominators {
const postDominators = computePostDominatorTree(fn, {
includeThrowsAsExitNode: false,
});
const postDominatorFrontierCache = new Map<BlockId, Set<BlockId>>();
function isControlledBlock(id: BlockId): boolean {
let controlBlocks = postDominatorFrontierCache.get(id);
if (controlBlocks === undefined) {
controlBlocks = postDominatorFrontier(fn, postDominators, id);
postDominatorFrontierCache.set(id, controlBlocks);
}
for (const blockId of controlBlocks) {
const controlBlock = fn.body.blocks.get(blockId)!;
switch (controlBlock.terminal.kind) {
case 'if':
case 'branch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
}
break;
}
case 'switch': {
if (isControlVariable(controlBlock.terminal.test)) {
return true;
}
for (const case_ of controlBlock.terminal.cases) {
if (case_.test !== null && isControlVariable(case_.test)) {
return true;
}
}
break;
}
}
}
return false;
}
return isControlledBlock;
}
/*
* Computes the post-dominator frontier of @param block. These are immediate successors of nodes that
* post-dominate @param targetId and from which execution may not reach @param block. Intuitively, these
* are the earliest blocks from which execution branches such that it may or may not reach the target block.
*/
function postDominatorFrontier(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const visited = new Set<BlockId>();
const frontier = new Set<BlockId>();
const targetPostDominators = postDominatorsOf(fn, postDominators, targetId);
for (const blockId of [...targetPostDominators, targetId]) {
if (visited.has(blockId)) {
continue;
}
visited.add(blockId);
const block = fn.body.blocks.get(blockId)!;
for (const pred of block.preds) {
if (!targetPostDominators.has(pred)) {
// The predecessor does not always reach this block, we found an item on the frontier!
frontier.add(pred);
}
}
}
return frontier;
}
function postDominatorsOf(
fn: HIRFunction,
postDominators: PostDominator<BlockId>,
targetId: BlockId,
): Set<BlockId> {
const result = new Set<BlockId>();
const visited = new Set<BlockId>();
const queue = [targetId];
while (queue.length) {
const currentId = queue.shift()!;
if (visited.has(currentId)) {
continue;
}
visited.add(currentId);
const current = fn.body.blocks.get(currentId)!;
for (const pred of current.preds) {
const predPostDominator = postDominators.get(pred) ?? pred;
if (predPostDominator === targetId || result.has(predPostDominator)) {
result.add(pred);
}
queue.push(pred);
}
}
return result;
}

View File

@@ -5,8 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerDiagnostic, CompilerError, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
CompilerDiagnostic,
CompilerError,
ErrorSeverity,
SourceLocation,
} from '..';
import {
CallExpression,
Effect,
@@ -42,7 +46,7 @@ type IdentifierSidemap = {
functions: Map<IdentifierId, TInstruction<FunctionExpression>>;
manualMemos: Map<IdentifierId, ManualMemoCallee>;
react: Set<IdentifierId>;
maybeDepsLists: Map<IdentifierId, {loc: SourceLocation; deps: Array<Place>}>;
maybeDepsLists: Map<IdentifierId, Array<Place>>;
maybeDeps: Map<IdentifierId, ManualMemoDependency>;
optionals: Set<IdentifierId>;
};
@@ -159,10 +163,10 @@ function collectTemporaries(
}
case 'ArrayExpression': {
if (value.elements.every(e => e.kind === 'Identifier')) {
sidemap.maybeDepsLists.set(instr.lvalue.identifier.id, {
loc: value.loc,
deps: value.elements as Array<Place>,
});
sidemap.maybeDepsLists.set(
instr.lvalue.identifier.id,
value.elements as Array<Place>,
);
}
break;
}
@@ -182,7 +186,6 @@ function makeManualMemoizationMarkers(
fnExpr: Place,
env: Environment,
depsList: Array<ManualMemoDependency> | null,
depsLoc: SourceLocation | null,
memoDecl: Place,
manualMemoId: number,
): [TInstruction<StartMemoize>, TInstruction<FinishMemoize>] {
@@ -198,7 +201,6 @@ function makeManualMemoizationMarkers(
* as dependencies
*/
deps: depsList,
depsLoc,
loc: fnExpr.loc,
},
effects: null,
@@ -289,85 +291,86 @@ function extractManualMemoizationArgs(
sidemap: IdentifierSidemap,
errors: CompilerError,
): {
fnPlace: Place;
fnPlace: Place | null;
depsList: Array<ManualMemoDependency> | null;
depsLoc: SourceLocation | null;
} | null {
} {
const [fnPlace, depsListPlace] = instr.value.args as Array<
Place | SpreadPattern | undefined
>;
if (fnPlace == null || fnPlace.kind !== 'Identifier') {
if (fnPlace == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
description:
kind === 'useCallback'
? 'The first argument to useCallback() must be a function to cache'
: 'The first argument to useMemo() must be a function that calculates a result to cache',
severity: ErrorSeverity.InvalidReact,
category: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetails({
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message:
kind === 'useCallback'
? `Expected a callback function`
: `Expected a memoization function`,
message: `Expected a callback function to be passed to ${kind}`,
}),
);
return null;
return {fnPlace: null, depsList: null};
}
if (depsListPlace == null) {
return {
fnPlace,
depsList: null,
depsLoc: null,
};
}
const maybeDepsList =
depsListPlace.kind === 'Identifier'
? sidemap.maybeDepsLists.get(depsListPlace.identifier.id)
: null;
if (maybeDepsList == null) {
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
severity: ErrorSeverity.InvalidReact,
category: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetails({
}).withDetail({
kind: 'error',
loc:
depsListPlace?.kind === 'Identifier' ? depsListPlace.loc : instr.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
}),
);
return null;
return {fnPlace: null, depsList: null};
}
const depsList: Array<ManualMemoDependency> = [];
for (const dep of maybeDepsList.deps) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
let depsList: Array<ManualMemoDependency> | null = null;
if (depsListPlace != null) {
const maybeDepsList = sidemap.maybeDepsLists.get(
depsListPlace.identifier.id,
);
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
severity: ErrorSeverity.InvalidReact,
category: `Expected the dependency list for ${kind} to be an array literal`,
description: `Expected the dependency list for ${kind} to be an array literal`,
suggestions: null,
}).withDetails({
}).withDetail({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
loc: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
}),
);
} else {
depsList.push(maybeDep);
return {fnPlace, depsList: null};
}
depsList = [];
for (const dep of maybeDepsList) {
const maybeDep = sidemap.maybeDeps.get(dep.identifier.id);
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: dep.loc,
message: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
}),
);
} else {
depsList.push(maybeDep);
}
}
}
return {
fnPlace,
depsList,
depsLoc: maybeDepsList.loc,
};
}
@@ -428,17 +431,50 @@ export function dropManualMemoization(
const manualMemo = sidemap.manualMemos.get(id);
if (manualMemo != null) {
const memoDetails = extractManualMemoizationArgs(
const {fnPlace, depsList} = extractManualMemoizationArgs(
instr as TInstruction<CallExpression> | TInstruction<MethodCall>,
manualMemo.kind,
sidemap,
errors,
);
if (memoDetails == null) {
if (fnPlace == null) {
continue;
}
const {fnPlace, depsList, depsLoc} = memoDetails;
/**
* Bailout on void return useMemos. This is an anti-pattern where code might be using
* useMemo like useEffect: running arbirtary side-effects synced to changes in specific
* values.
*/
if (
func.env.config.validateNoVoidUseMemo &&
manualMemo.kind === 'useMemo'
) {
const funcToCheck = sidemap.functions.get(
fnPlace.identifier.id,
)?.value;
if (funcToCheck !== undefined && funcToCheck.loweredFunc.func) {
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
: 'useMemo'
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
suggestions: null,
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
}),
);
}
}
}
instr.value = getManualMemoizationReplacement(
fnPlace,
@@ -462,11 +498,11 @@ export function dropManualMemoization(
if (!sidemap.functions.has(fnPlace.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the first argument to be an inline function expression`,
severity: ErrorSeverity.InvalidReact,
category: `Expected the first argument to be an inline function expression`,
description: `Expected the first argument to be an inline function expression`,
suggestions: [],
}).withDetails({
}).withDetail({
kind: 'error',
loc: fnPlace.loc,
message: `Expected the first argument to be an inline function expression`,
@@ -489,7 +525,6 @@ export function dropManualMemoization(
fnPlace,
func.env,
depsList,
depsLoc,
memoDecl,
nextManualMemoId++,
);
@@ -582,14 +617,7 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
default: {
CompilerError.invariant(false, {
reason: `Unexpected terminal in optional`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: `Unexpected ${terminal.kind} in optional`,
},
],
loc: terminal.loc,
});
}
}
@@ -598,3 +626,17 @@ function findOptionalPlaces(fn: HIRFunction): Set<IdentifierId> {
}
return optionals;
}
function hasNonVoidReturn(func: HIRFunction): boolean {
for (const [, block] of func.body.blocks) {
if (block.terminal.kind === 'return') {
if (
block.terminal.returnVariant === 'Explicit' ||
block.terminal.returnVariant === 'Implicit'
) {
return true;
}
}
}
return false;
}

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
Effect,
HIRFunction,
Identifier,
isMutableEffect,
isRefOrRefLikeMutableType,
makeInstructionId,
} from '../HIR/HIR';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import DisjointSet from '../Utils/DisjointSet';
/**
* If a function captures a mutable value but never gets called, we don't infer a
* mutable range for that function. This means that we also don't alias the function
* with its mutable captures.
*
* This case is tricky, because we don't generally know for sure what is a mutation
* and what may just be a normal function call. For example:
*
* ```
* hook useFoo() {
* const x = makeObject();
* return () => {
* return readObject(x); // could be a mutation!
* }
* }
* ```
*
* If we pessimistically assume that all such cases are mutations, we'd have to group
* lots of memo scopes together unnecessarily. However, if there is definitely a mutation:
*
* ```
* hook useFoo(createEntryForKey) {
* const cache = new WeakMap();
* return (key) => {
* let entry = cache.get(key);
* if (entry == null) {
* entry = createEntryForKey(key);
* cache.set(key, entry); // known mutation!
* }
* return entry;
* }
* }
* ```
*
* Then we have to ensure that the function and its mutable captures alias together and
* end up in the same scope. However, aliasing together isn't enough if the function
* and operands all have empty mutable ranges (end = start + 1).
*
* This pass finds function expressions and object methods that have an empty mutable range
* and known-mutable operands which also don't have a mutable range, and ensures that the
* function and those operands are aliased together *and* that their ranges are updated to
* end after the function expression. This is sufficient to ensure that a reactive scope is
* created for the alias set.
*/
export function inferAliasForUncalledFunctions(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const block of fn.body.blocks.values()) {
instrs: for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (
value.kind !== 'ObjectMethod' &&
value.kind !== 'FunctionExpression'
) {
continue;
}
/*
* If the function is known to be mutated, we will have
* already aliased any mutable operands with it
*/
const range = lvalue.identifier.mutableRange;
if (range.end > range.start + 1) {
continue;
}
/*
* If the function already has operands with an active mutable range,
* then we don't need to do anything — the function will have already
* been visited and included in some mutable alias set. This case can
* also occur due to visiting the same function in an earlier iteration
* of the outer fixpoint loop.
*/
for (const operand of eachInstructionValueOperand(value)) {
if (isMutable(instr, operand)) {
continue instrs;
}
}
const operands: Set<Identifier> = new Set();
for (const effect of value.loweredFunc.func.effects ?? []) {
if (effect.kind !== 'ContextMutation') {
continue;
}
/*
* We're looking for known-mutations only, so we look at the effects
* rather than function context
*/
if (effect.effect === Effect.Store || effect.effect === Effect.Mutate) {
for (const operand of effect.places) {
/*
* It's possible that function effect analysis thinks there was a context mutation,
* but then InferReferenceEffects figures out some operands are globals and therefore
* creates a non-mutable effect for those operands.
* We should change InferReferenceEffects to swap the ContextMutation for a global
* mutation in that case, but for now we just filter them out here
*/
if (
isMutableEffect(operand.effect, operand.loc) &&
!isRefOrRefLikeMutableType(operand.identifier.type)
) {
operands.add(operand.identifier);
}
}
}
}
if (operands.size !== 0) {
operands.add(lvalue.identifier);
aliases.union([...operands]);
// Update mutable ranges, if the ranges are empty then a reactive scope isn't created
for (const operand of operands) {
operand.mutableRange.end = makeInstructionId(instr.id + 1);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* 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,
Identifier,
Instruction,
isPrimitiveType,
Place,
} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export type AliasSet = Set<Identifier>;
export function inferAliases(func: HIRFunction): DisjointSet<Identifier> {
const aliases = new DisjointSet<Identifier>();
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
inferInstr(instr, aliases);
}
}
return aliases;
}
function inferInstr(
instr: Instruction,
aliases: DisjointSet<Identifier>,
): void {
const {lvalue, value: instrValue} = instr;
let alias: Place | null = null;
switch (instrValue.kind) {
case 'LoadLocal':
case 'LoadContext': {
if (isPrimitiveType(instrValue.place.identifier)) {
return;
}
alias = instrValue.place;
break;
}
case 'StoreLocal':
case 'StoreContext': {
alias = instrValue.value;
break;
}
case 'Destructure': {
alias = instrValue.value;
break;
}
case 'ComputedLoad':
case 'PropertyLoad': {
alias = instrValue.object;
break;
}
case 'TypeCastExpression': {
alias = instrValue.value;
break;
}
default:
return;
}
aliases.union([lvalue.identifier, alias.identifier]);
}

View File

@@ -0,0 +1,27 @@
/**
* 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, Identifier} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForPhis(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (isPhiMutatedAfterCreation) {
for (const [, operand] of phi.operands) {
aliases.union([phi.place.identifier, operand.identifier]);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
Effect,
HIRFunction,
Identifier,
InstructionId,
Place,
} from '../HIR/HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
export function inferAliasForStores(
func: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
for (const [_, block] of func.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
const isStore =
lvalue.effect === Effect.Store ||
/*
* Some typed functions annotate callees or arguments
* as Effect.Store.
*/
![...eachInstructionValueOperand(value)].every(
operand => operand.effect !== Effect.Store,
);
if (!isStore) {
continue;
}
for (const operand of eachInstructionLValue(instr)) {
maybeAlias(aliases, lvalue, operand, instr.id);
}
for (const operand of eachInstructionValueOperand(value)) {
if (
operand.effect === Effect.Capture ||
operand.effect === Effect.Store
) {
maybeAlias(aliases, lvalue, operand, instr.id);
}
}
}
}
}
function maybeAlias(
aliases: DisjointSet<Identifier>,
lvalue: Place,
rvalue: Place,
id: InstructionId,
): void {
if (
lvalue.identifier.mutableRange.end > id + 1 ||
rvalue.identifier.mutableRange.end > id
) {
aliases.union([lvalue.identifier, rvalue.identifier]);
}
}

View File

@@ -438,14 +438,7 @@ function rewriteSplices(
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected block instructions to be sorted',
description: null,
details: [
{
kind: 'error',
loc: originalInstrs[cursor].loc,
message: null,
},
],
loc: originalInstrs[cursor].loc,
},
);
currBlock.instructions.push(originalInstrs[cursor]);
@@ -454,14 +447,7 @@ function rewriteSplices(
CompilerError.invariant(originalInstrs[cursor].id === rewrite.location, {
reason:
'[InferEffectDependencies] Internal invariant broken: splice location not found',
description: null,
details: [
{
kind: 'error',
loc: originalInstrs[cursor].loc,
message: null,
},
],
loc: originalInstrs[cursor].loc,
});
if (rewrite.kind === 'instr') {
@@ -481,14 +467,7 @@ function rewriteSplices(
{
reason:
'[InferEffectDependencies] Internal invariant broken: expected entry block to have a fallthrough',
description: null,
details: [
{
kind: 'error',
loc: entryBlock.terminal.loc,
message: null,
},
],
loc: entryBlock.terminal.loc,
},
);
const originalTerminal = currBlock.terminal;
@@ -587,14 +566,7 @@ function inferMinimalDependencies(
CompilerError.invariant(hoistableToFnEntry != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing entry block',
description: null,
details: [
{
kind: 'error',
loc: fnInstr.loc,
message: null,
},
],
loc: fnInstr.loc,
});
const dependencies = inferDependencies(
@@ -650,14 +622,7 @@ function inferDependencies(
CompilerError.invariant(resultUnfiltered != null, {
reason:
'[InferEffectDependencies] Internal invariant broken: missing scope dependencies',
description: null,
details: [
{
kind: 'error',
loc: fn.loc,
message: null,
},
],
loc: fn.loc,
});
const fnContext = new Set(fn.context.map(dep => dep.identifier.id));

View File

@@ -0,0 +1,351 @@
/**
* 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,
CompilerErrorDetailOptions,
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) {
CompilerError.invariant(value.context.size > 0, {
reason:
"[InferFunctionEffects] Expected Context-kind value's capture list to be non-empty.",
loc: place.loc,
});
return {
kind: 'ContextMutation',
loc: place.loc,
effect: place.effect,
places: 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 transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
return eff.error;
}
case 'ContextMutation': {
return {
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';
}
export function getWriteErrorReason(abstractValue: AbstractValue): string {
if (abstractValue.reason.has(ValueReason.Global)) {
return 'Modifying a variable defined outside a component or hook is not allowed. Consider using an effect';
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
return 'Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX';
} else if (abstractValue.reason.has(ValueReason.Context)) {
return `Modifying a value returned from 'useContext()' is not allowed.`;
} else if (abstractValue.reason.has(ValueReason.KnownReturnSignature)) {
return 'Modifying a value returned from a function whose return value should not be mutated';
} else if (abstractValue.reason.has(ValueReason.ReactiveFunctionArgument)) {
return 'Modifying component props or hook arguments is not allowed. Consider using a local variable instead';
} else if (abstractValue.reason.has(ValueReason.State)) {
return "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead";
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
return "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead";
} else if (abstractValue.reason.has(ValueReason.Effect)) {
return 'Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()';
} else if (abstractValue.reason.has(ValueReason.HookCaptured)) {
return 'Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook';
} else if (abstractValue.reason.has(ValueReason.HookReturn)) {
return 'Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed';
} else {
return 'This modifies a variable that React considers immutable';
}
}

View File

@@ -0,0 +1,218 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
Effect,
HIRFunction,
Identifier,
InstructionId,
InstructionKind,
isArrayType,
isMapType,
isRefOrRefValue,
isSetType,
makeInstructionId,
Place,
} from '../HIR/HIR';
import {printPlace} from '../HIR/PrintHIR';
import {
eachInstructionLValue,
eachInstructionOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {assertExhaustive} from '../Utils/utils';
/*
* For each usage of a value in the given function, determines if the usage
* may be succeeded by a mutable usage of that same value and if so updates
* the usage to be mutable.
*
* Stated differently, this inference ensures that inferred capabilities of
* each reference are as follows:
* - freeze: the value is frozen at this point
* - readonly: the value is not modified at this point *or any subsequent
* point*
* - mutable: the value is modified at this point *or some subsequent point*.
*
* Note that this refines the capabilities inferered by InferReferenceCapability,
* which looks at individual references and not the lifetime of a value's mutability.
*
* == Algorithm
*
* TODO:
* 1. Forward data-flow analysis to determine aliasing. Unlike InferReferenceCapability
* which only tracks aliasing of top-level variables (`y = x`), this analysis needs
* to know if a value is aliased anywhere (`y.x = x`). The forward data flow tracks
* all possible locations which may have aliased a value. The concrete result is
* a mapping of each Place to the set of possibly-mutable values it may alias.
*
* ```
* const x = []; // {x: v0; v0: mutable []}
* const y = {}; // {x: v0, y: v1; v0: mutable [], v1: mutable []}
* y.x = x; // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
* read(x); // {x: v0, y: v1; v0: mutable [v1], v1: mutable [v0]}
* mutate(y); // can infer that y mutates v0 and v1
* ```
*
* DONE:
* 2. Forward data-flow analysis to compute mutability liveness. Walk forwards over
* the CFG and track which values are mutated in a successor.
*
* ```
* mutate(y); // mutable y => v0, v1 mutated
* read(x); // x maps to v0, v1, those are in the mutated-later set, so x is mutable here
* ...
* ```
*/
function infer(place: Place, instrId: InstructionId): void {
if (!isRefOrRefValue(place.identifier)) {
place.identifier.mutableRange.end = makeInstructionId(instrId + 1);
}
}
function inferPlace(
place: Place,
instrId: InstructionId,
inferMutableRangeForStores: boolean,
): void {
switch (place.effect) {
case Effect.Unknown: {
throw new Error(`Found an unknown place ${printPlace(place)}}!`);
}
case Effect.Capture:
case Effect.Read:
case Effect.Freeze:
return;
case Effect.Store:
if (inferMutableRangeForStores) {
infer(place, instrId);
}
return;
case Effect.ConditionallyMutateIterator: {
const identifier = place.identifier;
if (
!isArrayType(identifier) &&
!isSetType(identifier) &&
!isMapType(identifier)
) {
infer(place, instrId);
}
return;
}
case Effect.ConditionallyMutate:
case Effect.Mutate: {
infer(place, instrId);
return;
}
default:
assertExhaustive(place.effect, `Unexpected ${printPlace(place)} effect`);
}
}
export function inferMutableLifetimes(
func: HIRFunction,
inferMutableRangeForStores: boolean,
): void {
/*
* Context variables only appear to mutate where they are assigned, but we need
* to force their range to start at their declaration. Track the declaring instruction
* id so that the ranges can be extended if/when they are reassigned
*/
const contextVariableDeclarationInstructions = new Map<
Identifier,
InstructionId
>();
for (const [_, block] of func.body.blocks) {
for (const phi of block.phis) {
const isPhiMutatedAfterCreation: boolean =
phi.place.identifier.mutableRange.end >
(block.instructions.at(0)?.id ?? block.terminal.id);
if (
inferMutableRangeForStores &&
isPhiMutatedAfterCreation &&
phi.place.identifier.mutableRange.start === 0
) {
for (const [, operand] of phi.operands) {
if (phi.place.identifier.mutableRange.start === 0) {
phi.place.identifier.mutableRange.start =
operand.identifier.mutableRange.start;
} else {
phi.place.identifier.mutableRange.start = makeInstructionId(
Math.min(
phi.place.identifier.mutableRange.start,
operand.identifier.mutableRange.start,
),
);
}
}
}
}
for (const instr of block.instructions) {
for (const operand of eachInstructionLValue(instr)) {
const lvalueId = operand.identifier;
/*
* lvalue start being mutable when they're initially assigned a
* value.
*/
lvalueId.mutableRange.start = instr.id;
/*
* Let's be optimistic and assume this lvalue is not mutable by
* default.
*/
lvalueId.mutableRange.end = makeInstructionId(instr.id + 1);
}
for (const operand of eachInstructionOperand(instr)) {
inferPlace(operand, instr.id, inferMutableRangeForStores);
}
if (
instr.value.kind === 'DeclareContext' ||
(instr.value.kind === 'StoreContext' &&
instr.value.lvalue.kind !== InstructionKind.Reassign &&
!contextVariableDeclarationInstructions.has(
instr.value.lvalue.place.identifier,
))
) {
/**
* Save declarations of context variables if they hasn't already been
* declared (due to hoisted declarations).
*/
contextVariableDeclarationInstructions.set(
instr.value.lvalue.place.identifier,
instr.id,
);
} else if (instr.value.kind === 'StoreContext') {
/*
* Else this is a reassignment, extend the range from the declaration (if present).
* Note that declarations may not be present for context variables that are reassigned
* within a function expression before (or without) a read of the same variable
*/
const declaration = contextVariableDeclarationInstructions.get(
instr.value.lvalue.place.identifier,
);
if (
declaration != null &&
!isRefOrRefValue(instr.value.lvalue.place.identifier)
) {
const range = instr.value.lvalue.place.identifier.mutableRange;
if (range.start === 0) {
range.start = declaration;
} else {
range.start = makeInstructionId(Math.min(range.start, declaration));
}
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
inferPlace(operand, block.terminal.id, inferMutableRangeForStores);
}
}
}

View File

@@ -0,0 +1,102 @@
/**
* 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, Identifier} from '../HIR/HIR';
import {inferAliasForUncalledFunctions} from './InerAliasForUncalledFunctions';
import {inferAliases} from './InferAlias';
import {inferAliasForPhis} from './InferAliasForPhis';
import {inferAliasForStores} from './InferAliasForStores';
import {inferMutableLifetimes} from './InferMutableLifetimes';
import {inferMutableRangesForAlias} from './InferMutableRangesForAlias';
import {inferTryCatchAliases} from './InferTryCatchAliases';
export function inferMutableRanges(ir: HIRFunction): void {
// Infer mutable ranges for non fields
inferMutableLifetimes(ir, false);
// Calculate aliases
const aliases = inferAliases(ir);
/*
* Calculate aliases for try/catch, where any value created
* in the try block could be aliased to the catch param
*/
inferTryCatchAliases(ir, aliases);
/*
* Eagerly canonicalize so that if nothing changes we can bail out
* after a single iteration
*/
let prevAliases: Map<Identifier, Identifier> = aliases.canonicalize();
while (true) {
// Infer mutable ranges for aliases that are not fields
inferMutableRangesForAlias(ir, aliases);
// Update aliasing information of fields
inferAliasForStores(ir, aliases);
// Update aliasing information of phis
inferAliasForPhis(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
// 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);
inferAliasForUncalledFunctions(ir, aliases);
const nextAliases = aliases.canonicalize();
if (areEqualMaps(prevAliases, nextAliases)) {
break;
}
prevAliases = nextAliases;
}
}
function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
if (a.size !== b.size) {
return false;
}
for (const [key, value] of a) {
if (!b.has(key)) {
return false;
}
if (b.get(key) !== value) {
return false;
}
}
return true;
}

Some files were not shown because too many files have changed in this diff Show More