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
840 changed files with 10809 additions and 44604 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',
},
},
@@ -547,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',
@@ -565,7 +567,6 @@ module.exports = {
BigInt: 'readonly',
BigInt64Array: 'readonly',
BigUint64Array: 'readonly',
CacheType: 'readonly',
Class: 'readonly',
ClientRect: 'readonly',
CopyInspectedElementPath: 'readonly',
@@ -577,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',
@@ -621,6 +619,7 @@ module.exports = {
PropertyIndexedKeyframes: 'readonly',
KeyframeAnimationOptions: 'readonly',
GetAnimationsOptions: 'readonly',
Animatable: 'readonly',
ScrollTimeline: 'readonly',
EventListenerOptionsOrUseCapture: 'readonly',
FocusOptions: 'readonly',

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

@@ -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
@@ -811,18 +811,9 @@ 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

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

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,183 +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, useCallback} from 'react';
import {Resizable} from 're-resizable';
import {useSnackbar} from 'notistack';
import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
import {
ConfigError,
generateOverridePragmaFromConfig,
updateSourceWithOverridePragma,
} from '../../lib/configUtils';
// @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(): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
const store = useStore();
const dispatchStore = useStoreDispatch();
const {enqueueSnackbar} = useSnackbar();
const toggleExpanded = useCallback(() => {
setIsExpanded(prev => !prev);
}, []);
const handleApplyConfig: () => Promise<void> = async () => {
try {
const config = store.config || '';
if (!config.trim()) {
enqueueSnackbar(
'Config is empty. Please add configuration options first.',
{
variant: 'warning',
},
);
return;
}
const newPragma = await generateOverridePragmaFromConfig(config);
const updatedSource = updateSourceWithOverridePragma(
store.source,
newPragma,
);
dispatchStore({
type: 'updateFile',
payload: {
source: updatedSource,
config: config,
},
});
} catch (error) {
console.error('Failed to apply config:', error);
if (error instanceof ConfigError && error.message.trim()) {
enqueueSnackbar(error.message, {
variant: 'error',
});
} else {
enqueueSnackbar('Unexpected error: failed to apply config.', {
variant: 'error',
});
}
}
};
const handleChange: (value: string | undefined) => void = value => {
if (value === undefined) return;
// Only update the config
dispatchStore({
type: 'updateFile',
payload: {
source: store.source,
config: value,
},
});
};
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,
});
const uri = monaco.Uri.parse(`file:///config.ts`);
const model = monaco.editor.getModel(uri);
if (model) {
model.updateOptions({tabSize: 2});
}
};
return (
<div className="flex flex-row relative">
{isExpanded ? (
<>
<Resizable
className="border-r"
minWidth={300}
maxWidth={600}
defaultSize={{width: 350, height: 'auto'}}
enable={{right: true}}>
<h2
title="Minimize config editor"
aria-label="Minimize config editor"
onClick={toggleExpanded}
className="p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 font-light text-secondary hover:text-link">
- Config Overrides
</h2>
<div className="h-[calc(100vh_-_3.5rem_-_4rem)]">
<MonacoEditor
path={'config.ts'}
language={'typescript'}
value={store.config}
onMount={handleMount}
onChange={handleChange}
options={{
...monacoOptions,
lineNumbers: 'off',
folding: false,
renderLineHighlight: 'none',
scrollBeyondLastLine: false,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
overviewRulerLanes: 0,
fontSize: 12,
}}
/>
</div>
</Resizable>
<button
onClick={handleApplyConfig}
title="Apply config overrides to input"
aria-label="Apply config overrides to input"
className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-1/2 z-10 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full border-2 border-white shadow-lg flex items-center justify-center text-sm font-medium transition-colors duration-150">
</button>
</>
) : (
<div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
<button
title="Expand config editor"
aria-label="Expand config editor"
style={{
transform: 'rotate(90deg) translate(-50%)',
whiteSpace: 'nowrap',
}}
onClick={toggleExpanded}
className="flex-grow-0 w-5 transition-colors duration-150 ease-in font-light text-secondary hover:text-link">
Config Overrides
</button>
</div>
)}
</div>
);
}

View File

@@ -13,7 +13,7 @@ import BabelPluginReactCompiler, {
CompilerErrorDetail,
CompilerDiagnostic,
Effect,
ErrorCategory,
ErrorSeverity,
parseConfigPragmaForTests,
ValueKind,
type Hook,
@@ -37,7 +37,6 @@ import {
type Store,
} from '../../lib/stores';
import {useStore, useStoreDispatch} from '../StoreContext';
import ConfigEditor from './ConfigEditor';
import Input from './Input';
import {
CompilerOutput,
@@ -47,7 +46,6 @@ import {
} from './Output';
import {transformFromAstSync} from '@babel/core';
import {LoggerEvent} from 'babel-plugin-react-compiler/dist/Entrypoint';
import {useSearchParams} from 'next/navigation';
function parseInput(
input: string,
@@ -258,7 +256,7 @@ function compile(
console.error(err);
error.details.push(
new CompilerErrorDetail({
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
reason: `Unexpected failure when transforming input! ${err}`,
loc: null,
suggestions: null,
@@ -293,13 +291,7 @@ export default function Editor(): JSX.Element {
[deferredStore.source],
);
// TODO: Remove this once the config editor is more stable
const searchParams = useSearchParams();
const search = searchParams.get('showConfig');
const shouldShowConfig = search === 'true';
useMountEffect(() => {
// Initialize store
let mountStore: Store;
try {
mountStore = initStoreFromUrlOrLocalStorage();
@@ -315,12 +307,9 @@ export default function Editor(): JSX.Element {
});
mountStore = defaultStore;
}
dispatchStore({
type: 'setStore',
payload: {
store: mountStore,
},
payload: {store: mountStore},
});
});
@@ -339,7 +328,6 @@ export default function Editor(): JSX.Element {
return (
<>
<div className="relative flex basis top-14">
{shouldShowConfig && <ConfigEditor />}
<div className={clsx('relative sm:basis-1/4')}>
<Input language={language} errors={errors} />
</div>

View File

@@ -17,7 +17,6 @@ import {useStore, useStoreDispatch} from '../StoreContext';
import {monacoOptions} from './monacoOptions';
// @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
import React$Types from '../../node_modules/@types/react/index.d.ts';
import {parseAndFormatConfig} from '../../lib/configUtils.ts';
loader.config({monaco});
@@ -80,17 +79,13 @@ 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;
// Parse and format the config
const config = await parseAndFormatConfig(value);
dispatchStore({
type: 'updateFile',
payload: {
source: value,
config,
},
});
};

View File

@@ -64,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 && passName !== 'Output' && passName !== 'SourceMap') {
continue;
}
for (const result of results) {
switch (result.kind) {
case 'hir': {
@@ -229,10 +225,10 @@ function Output({store, compilerOutput}: Props): JSX.Element {
}
useEffect(() => {
tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
tabify(store.source, compilerOutput).then(tabs => {
setTabs(tabs);
});
}, [store.source, compilerOutput, store.showInternals]);
}, [store.source, compilerOutput]);
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
let lastResult: string = '';
@@ -252,7 +248,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
return (
<>
<TabbedWindow
defaultTab={store.showInternals ? 'HIR' : 'Output'}
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}

View File

@@ -14,11 +14,10 @@ import {useState} from 'react';
import {defaultStore} from '../lib/defaultStore';
import {IconGitHub} from './Icons/IconGitHub';
import Logo from './Logo';
import {useStore, useStoreDispatch} from './StoreContext';
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();
@@ -57,27 +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="relative inline-block w-[34px] h-5">
<input
type="checkbox"
checked={store.showInternals}
onChange={() => 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-blue-500 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

@@ -56,11 +56,7 @@ type ReducerAction =
type: 'updateFile';
payload: {
source: string;
config: string;
};
}
| {
type: 'toggleInternals';
};
function storeReducer(store: Store, action: ReducerAction): Store {
@@ -70,18 +66,10 @@ function storeReducer(store: Store, action: ReducerAction): Store {
return newStore;
}
case 'updateFile': {
const {source, config} = action.payload;
const {source} = action.payload;
const newStore = {
...store,
source,
config,
};
return newStore;
}
case 'toggleInternals': {
const newStore = {
...store,
showInternals: !store.showInternals,
};
return newStore;
}

View File

@@ -1,120 +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 parserBabel from 'prettier/plugins/babel';
import prettierPluginEstree from 'prettier/plugins/estree';
import * as prettier from 'prettier/standalone';
import {parsePluginOptions} from 'babel-plugin-react-compiler';
import {parseConfigPragmaAsString} from '../../../packages/babel-plugin-react-compiler/src/Utils/TestUtils';
export class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigError';
}
}
/**
* Parse config from pragma and format it with prettier
*/
export async function parseAndFormatConfig(source: string): Promise<string> {
const pragma = source.substring(0, source.indexOf('\n'));
let configString = parseConfigPragmaAsString(pragma);
if (configString !== '') {
configString = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
(${configString} satisfies Partial<PluginOptions>)`;
}
try {
const formatted = await prettier.format(configString, {
semi: true,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return formatted;
} catch (error) {
console.error('Error formatting config:', error);
return ''; // Return empty string if not valid for now
}
}
function extractCurlyBracesContent(input: string): string {
const startIndex = input.indexOf('({') + 1;
const endIndex = input.lastIndexOf('}');
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
throw new Error('No outer curly braces found in input.');
}
return input.slice(startIndex, endIndex + 1);
}
function cleanContent(content: string): string {
return content
.replace(/[\r\n]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Validate that a config string can be parsed as a valid PluginOptions object
* Throws an error if validation fails.
*/
function validateConfigAsPluginOptions(configString: string): void {
// Validate that config can be parse as JS obj
let parsedConfig: unknown;
try {
parsedConfig = new Function(`return (${configString})`)();
} catch (_) {
throw new ConfigError('Config has invalid syntax.');
}
// Validate against PluginOptions schema
try {
parsePluginOptions(parsedConfig);
} catch (_) {
throw new ConfigError('Config does not match the expected schema.');
}
}
/**
* Generate a the override pragma comment from a formatted config object string
*/
export async function generateOverridePragmaFromConfig(
formattedConfigString: string,
): Promise<string> {
const content = extractCurlyBracesContent(formattedConfigString);
const cleanConfig = cleanContent(content);
validateConfigAsPluginOptions(cleanConfig);
// Format the config to ensure it's valid
await prettier.format(`(${cleanConfig})`, {
semi: false,
parser: 'babel-ts',
plugins: [parserBabel, prettierPluginEstree],
});
return `// @OVERRIDE:${cleanConfig}`;
}
/**
* Update the override pragma comment in source code.
*/
export function updateSourceWithOverridePragma(
source: string,
newPragma: string,
): string {
const firstLineEnd = source.indexOf('\n');
const firstLine = source.substring(0, firstLineEnd);
const pragmaRegex = /^\/\/\s*@/;
if (firstLineEnd !== -1 && pragmaRegex.test(firstLine.trim())) {
return newPragma + source.substring(firstLineEnd);
} else {
return newPragma + '\n' + source;
}
}

View File

@@ -13,36 +13,10 @@ export default function MyApp() {
}
`;
export const defaultConfig = `\
import type { PluginOptions } from 'babel-plugin-react-compiler/dist';
({
compilationMode: 'infer',
panicThreshold: 'none',
environment: {},
logger: null,
gating: null,
noEmit: false,
dynamicGating: null,
eslintSuppressionRules: null,
flowSuppressions: true,
ignoreUseNoForget: false,
sources: filename => {
return filename.indexOf('node_modules') === -1;
},
enableReanimatedCheck: true,
customOptOutDirectives: null,
target: '19',
} satisfies Partial<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 : defaultConfig,
showInternals: 'showInternals' in raw ? raw.showInternals : false,
};
return raw;
}

View File

@@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.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

@@ -34,30 +34,26 @@
"invariant": "^2.2.4",
"lz-string": "^1.5.0",
"monaco-editor": "^0.52.0",
"next": "15.5.2",
"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.1.1",
"react-dom": "19.1.1"
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "18.11.9",
"@types/react": "19.1.12",
"@types/react-dom": "19.1.9",
"@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.1.12",
"@types/react-dom": "19.1.9"
}
}

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

@@ -7,37 +7,49 @@
import * as t from '@babel/types';
import {codeFrameColumns} from '@babel/code-frame';
import {type SourceLocation} from './HIR';
import type {SourceLocation} from './HIR';
import {Err, Ok, Result} from './Utils/Result';
import {assertExhaustive} from './Utils/utils';
import invariant from 'invariant';
export enum ErrorSeverity {
/**
* An actionable error that the developer can fix. For example, product code errors should be
* reported as such.
* Invalid JS syntax, or valid syntax that is semantically invalid which may indicate some
* misunderstanding on the users part.
*/
Error = 'Error',
InvalidJS = 'InvalidJS',
/**
* An error that the developer may not necessarily be able to fix. For example, syntax not
* supported by the compiler does not indicate any fault in the product code.
* JS syntax that is not supported and which we do not plan to support. Developers should
* rewrite to use supported forms.
*/
Warning = 'Warning',
UnsupportedJS = 'UnsupportedJS',
/**
* Not an error. These will not be surfaced in ESLint, but may be surfaced in other ways
* (eg Forgive) where informational hints can be shown.
* Code that breaks the rules of React.
*/
Hint = 'Hint',
InvalidReact = 'InvalidReact',
/**
* These errors will not be reported anywhere. Useful for work in progress validations.
* Incorrect configuration of the compiler.
*/
Off = 'Off',
InvalidConfig = 'InvalidConfig',
/**
* Code that can reasonably occur and that doesn't break any rules, but is unsafe to preserve
* memoization.
*/
CannotPreserveMemoization = 'CannotPreserveMemoization',
/**
* Unhandled syntax that we don't support yet.
*/
Todo = 'Todo',
/**
* An unexpected internal error in the compiler that indicates critical issues that can panic
* the compiler.
*/
Invariant = 'Invariant',
}
export type CompilerDiagnosticOptions = {
category: ErrorCategory;
reason: string;
description: string | null;
severity: ErrorSeverity;
category: string;
description: string;
details: Array<CompilerDiagnosticDetail>;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
@@ -46,15 +58,11 @@ export type CompilerDiagnosticDetail =
/**
* A/the source of the error
*/
| {
kind: 'error';
loc: SourceLocation | null;
message: string | null;
}
| {
kind: 'hint';
message: string;
};
{
kind: 'error';
loc: SourceLocation | null;
message: string;
};
export enum CompilerSuggestionOperation {
InsertBefore,
@@ -78,13 +86,10 @@ export type CompilerSuggestion =
description: string;
};
/**
* @deprecated use {@link CompilerDiagnosticOptions} instead
*/
export type CompilerErrorDetailOptions = {
category: ErrorCategory;
reason: string;
description?: string | null | undefined;
severity: ErrorSeverity;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
@@ -110,41 +115,34 @@ export class CompilerDiagnostic {
return new CompilerDiagnostic({...options, details: []});
}
get reason(): CompilerDiagnosticOptions['reason'] {
return this.options.reason;
get category(): CompilerDiagnosticOptions['category'] {
return this.options.category;
}
get description(): CompilerDiagnosticOptions['description'] {
return this.options.description;
}
get severity(): ErrorSeverity {
return getRuleForCategory(this.category).severity;
get severity(): CompilerDiagnosticOptions['severity'] {
return this.options.severity;
}
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
}
withDetails(...details: Array<CompilerDiagnosticDetail>): CompilerDiagnostic {
this.options.details.push(...details);
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
this.options.details.push(detail);
return this;
}
primaryLocation(): SourceLocation | null {
const firstErrorDetail = this.options.details.filter(
d => d.kind === 'error',
)[0];
return firstErrorDetail != null && firstErrorDetail.kind === 'error'
? firstErrorDetail.loc
: null;
return this.options.details.filter(d => d.kind === 'error')[0]?.loc ?? null;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [printErrorSummary(this.category, this.reason)];
if (this.description != null) {
buffer.push('\n\n', `${this.description}.`);
}
const buffer = [
printErrorSummary(this.severity, this.category),
'\n\n',
this.description,
];
for (const detail of this.options.details) {
switch (detail.kind) {
case 'error': {
@@ -154,9 +152,9 @@ export class CompilerDiagnostic {
}
let codeFrame: string;
try {
codeFrame = printCodeFrame(source, loc, detail.message ?? '');
codeFrame = printCodeFrame(source, loc, detail.message);
} catch (e) {
codeFrame = detail.message ?? '';
codeFrame = detail.message;
}
buffer.push('\n\n');
if (loc.filename != null) {
@@ -169,14 +167,9 @@ export class CompilerDiagnostic {
buffer.push(codeFrame);
break;
}
case 'hint': {
buffer.push('\n\n');
buffer.push(detail.message);
break;
}
default: {
assertExhaustive(
detail,
detail.kind,
`Unexpected detail kind ${(detail as any).kind}`,
);
}
@@ -186,7 +179,7 @@ export class CompilerDiagnostic {
}
toString(): string {
const buffer = [printErrorSummary(this.category, this.reason)];
const buffer = [printErrorSummary(this.severity, this.category)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
@@ -198,11 +191,9 @@ export class CompilerDiagnostic {
}
}
/**
/*
* Each bailout or invariant in HIR lowering creates an {@link CompilerErrorDetail}, which is then
* aggregated into a single {@link CompilerError} later.
*
* @deprecated use {@link CompilerDiagnostic} instead
*/
export class CompilerErrorDetail {
options: CompilerErrorDetailOptions;
@@ -217,8 +208,8 @@ export class CompilerErrorDetail {
get description(): CompilerErrorDetailOptions['description'] {
return this.options.description;
}
get severity(): ErrorSeverity {
return getRuleForCategory(this.category).severity;
get severity(): CompilerErrorDetailOptions['severity'] {
return this.options.severity;
}
get loc(): CompilerErrorDetailOptions['loc'] {
return this.options.loc;
@@ -226,16 +217,13 @@ export class CompilerErrorDetail {
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
}
primaryLocation(): SourceLocation | null {
return this.loc;
}
printErrorMessage(source: string, options: PrintErrorMessageOptions): string {
const buffer = [printErrorSummary(this.category, this.reason)];
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`\n\n${this.description}.`);
}
@@ -260,7 +248,7 @@ export class CompilerErrorDetail {
}
toString(): string {
const buffer = [printErrorSummary(this.category, this.reason)];
const buffer = [printErrorSummary(this.severity, this.reason)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
@@ -272,28 +260,21 @@ export class CompilerErrorDetail {
}
}
/**
* An aggregate of {@link CompilerDiagnostic}. This allows us to aggregate all issues found by the
* compiler into a single error before we throw. Where possible, prefer to push diagnostics into
* the error aggregate instead of throwing immediately.
*/
export class CompilerError extends Error {
details: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
disabledDetails: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
printedMessage: string | null = null;
static invariant(
condition: unknown,
options: Omit<CompilerDiagnosticOptions, 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
reason: options.reason,
description: options.description,
category: ErrorCategory.Invariant,
}).withDetails(...options.details),
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.Invariant,
}),
);
throw errors;
}
@@ -306,45 +287,49 @@ export class CompilerError extends Error {
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
category: ErrorCategory.Todo,
}),
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
);
throw errors;
}
static throwInvalidJS(
options: Omit<CompilerErrorDetailOptions, 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
category: ErrorCategory.Syntax,
severity: ErrorSeverity.InvalidJS,
}),
);
throw errors;
}
static throwInvalidReact(options: CompilerErrorDetailOptions): never {
const errors = new CompilerError();
errors.pushErrorDetail(new CompilerErrorDetail(options));
throw errors;
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'category'>,
static throwInvalidReact(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
category: ErrorCategory.Config,
severity: ErrorSeverity.InvalidReact,
}),
);
throw errors;
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidConfig,
}),
);
throw errors;
@@ -360,7 +345,6 @@ export class CompilerError extends Error {
super(...args);
this.name = 'ReactCompilerError';
this.details = [];
this.disabledDetails = [];
}
override get message(): string {
@@ -401,93 +385,60 @@ export class CompilerError extends Error {
merge(other: CompilerError): void {
this.details.push(...other.details);
this.disabledDetails.push(...other.disabledDetails);
}
pushDiagnostic(diagnostic: CompilerDiagnostic): void {
if (diagnostic.severity === ErrorSeverity.Off) {
this.disabledDetails.push(diagnostic);
} else {
this.details.push(diagnostic);
}
this.details.push(diagnostic);
}
/**
* @deprecated use {@link pushDiagnostic} instead
*/
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail({
category: options.category,
reason: options.reason,
description: options.description ?? null,
severity: options.severity,
suggestions: options.suggestions,
loc: typeof options.loc === 'symbol' ? null : options.loc,
});
return this.pushErrorDetail(detail);
}
/**
* @deprecated use {@link pushDiagnostic} instead
*/
pushErrorDetail(detail: CompilerErrorDetail): CompilerErrorDetail {
if (detail.severity === ErrorSeverity.Off) {
this.disabledDetails.push(detail);
} else {
this.details.push(detail);
}
this.details.push(detail);
return detail;
}
hasAnyErrors(): boolean {
hasErrors(): boolean {
return this.details.length > 0;
}
asResult(): Result<void, CompilerError> {
return this.hasAnyErrors() ? Err(this) : Ok(undefined);
return this.hasErrors() ? Err(this) : Ok(undefined);
}
/**
* Returns true if any of the error details are of severity Error.
/*
* An error is critical if it means the compiler has entered into a broken state and cannot
* continue safely. Other expected errors such as Todos mean that we can skip over that component
* but otherwise continue compiling the rest of the app.
*/
hasErrors(): boolean {
for (const detail of this.details) {
if (detail.severity === ErrorSeverity.Error) {
return true;
isCritical(): boolean {
return this.details.some(detail => {
switch (detail.severity) {
case ErrorSeverity.Invariant:
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.UnsupportedJS: {
return true;
}
case ErrorSeverity.CannotPreserveMemoization:
case ErrorSeverity.Todo: {
return false;
}
default: {
assertExhaustive(detail.severity, 'Unhandled error severity');
}
}
}
return false;
}
/**
* Returns true if there are no Errors and there is at least one Warning.
*/
hasWarning(): boolean {
let res = false;
for (const detail of this.details) {
if (detail.severity === ErrorSeverity.Error) {
return false;
}
if (detail.severity === ErrorSeverity.Warning) {
res = true;
}
}
return res;
}
hasHints(): boolean {
let res = false;
for (const detail of this.details) {
if (detail.severity === ErrorSeverity.Error) {
return false;
}
if (detail.severity === ErrorSeverity.Warning) {
return false;
}
if (detail.severity === ErrorSeverity.Hint) {
res = true;
}
}
return res;
});
}
}
@@ -514,471 +465,31 @@ function printCodeFrame(
);
}
function printErrorSummary(category: ErrorCategory, message: string): string {
let heading: string;
switch (category) {
case ErrorCategory.AutomaticEffectDependencies:
case ErrorCategory.CapitalizedCalls:
case ErrorCategory.Config:
case ErrorCategory.EffectDerivationsOfState:
case ErrorCategory.EffectSetState:
case ErrorCategory.ErrorBoundaries:
case ErrorCategory.Factories:
case ErrorCategory.FBT:
case ErrorCategory.Fire:
case ErrorCategory.Gating:
case ErrorCategory.Globals:
case ErrorCategory.Hooks:
case ErrorCategory.Immutability:
case ErrorCategory.Purity:
case ErrorCategory.Refs:
case ErrorCategory.RenderSetState:
case ErrorCategory.StaticComponents:
case ErrorCategory.Suppression:
case ErrorCategory.Syntax:
case ErrorCategory.UseMemo: {
heading = 'Error';
function printErrorSummary(severity: ErrorSeverity, message: string): string {
let severityCategory: string;
switch (severity) {
case ErrorSeverity.InvalidConfig:
case ErrorSeverity.InvalidJS:
case ErrorSeverity.InvalidReact:
case ErrorSeverity.UnsupportedJS: {
severityCategory = 'Error';
break;
}
case ErrorCategory.EffectDependencies:
case ErrorCategory.IncompatibleLibrary:
case ErrorCategory.PreserveManualMemo:
case ErrorCategory.UnsupportedSyntax: {
heading = 'Compilation Skipped';
case ErrorSeverity.CannotPreserveMemoization: {
severityCategory = 'Memoization';
break;
}
case ErrorCategory.Invariant: {
heading = 'Invariant';
case ErrorSeverity.Invariant: {
severityCategory = 'Invariant';
break;
}
case ErrorCategory.Todo: {
heading = 'Todo';
case ErrorSeverity.Todo: {
severityCategory = 'Todo';
break;
}
default: {
assertExhaustive(category, `Unhandled category '${category}'`);
assertExhaustive(severity, `Unexpected severity '${severity}'`);
}
}
return `${heading}: ${message}`;
return `${severityCategory}: ${message}`;
}
/**
* See getRuleForCategory() for how these map to ESLint rules
*/
export enum ErrorCategory {
/**
* Checking for valid hooks usage (non conditional, non-first class, non reactive, etc)
*/
Hooks = 'Hooks',
/**
* Checking for no capitalized calls (not definitively an error, hence separating)
*/
CapitalizedCalls = 'CapitalizedCalls',
/**
* Checking for static components
*/
StaticComponents = 'StaticComponents',
/**
* Checking for valid usage of manual memoization
*/
UseMemo = 'UseMemo',
/**
* Checking for higher order functions acting as factories for components/hooks
*/
Factories = 'Factories',
/**
* Checks that manual memoization is preserved
*/
PreserveManualMemo = 'PreserveManualMemo',
/**
* Checks for known incompatible libraries
*/
IncompatibleLibrary = 'IncompatibleLibrary',
/**
* Checking for no mutations of props, hook arguments, hook return values
*/
Immutability = 'Immutability',
/**
* Checking for assignments to globals
*/
Globals = 'Globals',
/**
* Checking for valid usage of refs, ie no access during render
*/
Refs = 'Refs',
/**
* Checks for memoized effect deps
*/
EffectDependencies = 'EffectDependencies',
/**
* Checks for no setState in effect bodies
*/
EffectSetState = 'EffectSetState',
EffectDerivationsOfState = 'EffectDerivationsOfState',
/**
* Validates against try/catch in place of error boundaries
*/
ErrorBoundaries = 'ErrorBoundaries',
/**
* Checking for pure functions
*/
Purity = 'Purity',
/**
* Validates against setState in render
*/
RenderSetState = 'RenderSetState',
/**
* Internal invariants
*/
Invariant = 'Invariant',
/**
* Todos
*/
Todo = 'Todo',
/**
* Syntax errors
*/
Syntax = 'Syntax',
/**
* Checks for use of unsupported syntax
*/
UnsupportedSyntax = 'UnsupportedSyntax',
/**
* Config errors
*/
Config = 'Config',
/**
* Gating error
*/
Gating = 'Gating',
/**
* Suppressions
*/
Suppression = 'Suppression',
/**
* Issues with auto deps
*/
AutomaticEffectDependencies = 'AutomaticEffectDependencies',
/**
* Issues with `fire`
*/
Fire = 'Fire',
/**
* fbt-specific issues
*/
FBT = 'FBT',
}
export type LintRule = {
// Stores the category the rule corresponds to, used to filter errors when reporting
category: ErrorCategory;
// Stores the severity of the error, which is used to map to lint levels such as error/warning.
severity: ErrorSeverity;
/**
* The "name" of the rule as it will be used by developers to enable/disable, eg
* "eslint-disable-nest line <name>"
*/
name: string;
/**
* A description of the rule that appears somewhere in ESLint. This does not affect
* how error messages are formatted
*/
description: string;
/**
* If true, this rule will automatically appear in the default, "recommended" ESLint
* rule set. Otherwise it will be part of an `allRules` export that developers can
* use to opt-in to showing output of all possible rules.
*
* NOTE: not all validations are enabled by default! Setting this flag only affects
* whether a given rule is part of the recommended set. The corresponding validation
* also should be enabled by default if you want the error to actually show up!
*/
recommended: boolean;
};
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
export function getRuleForCategory(category: ErrorCategory): LintRule {
const rule = getRuleForCategoryImpl(category);
invariant(
RULE_NAME_PATTERN.test(rule.name),
`Invalid rule name, got '${rule.name}' but rules must match ${RULE_NAME_PATTERN.toString()}`,
);
return rule;
}
function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
switch (category) {
case ErrorCategory.AutomaticEffectDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
recommended: false,
};
}
case ErrorCategory.CapitalizedCalls: {
return {
category,
severity: ErrorSeverity.Error,
name: 'capitalized-calls',
description:
'Validates against calling capitalized functions/methods instead of using JSX',
recommended: false,
};
}
case ErrorCategory.Config: {
return {
category,
severity: ErrorSeverity.Error,
name: 'config',
description: 'Validates the compiler configuration options',
recommended: true,
};
}
case ErrorCategory.EffectDependencies: {
return {
category,
severity: ErrorSeverity.Error,
name: 'memoized-effect-dependencies',
description: 'Validates that effect dependencies are memoized',
recommended: false,
};
}
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
severity: ErrorSeverity.Error,
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
recommended: false,
};
}
case ErrorCategory.EffectSetState: {
return {
category,
severity: ErrorSeverity.Error,
name: 'set-state-in-effect',
description:
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
recommended: true,
};
}
case ErrorCategory.ErrorBoundaries: {
return {
category,
severity: ErrorSeverity.Error,
name: 'error-boundaries',
description:
'Validates usage of error boundaries instead of try/catch for errors in child components',
recommended: true,
};
}
case ErrorCategory.Factories: {
return {
category,
severity: ErrorSeverity.Error,
name: 'component-hook-factories',
description:
'Validates against higher order functions defining nested components or hooks. ' +
'Components and hooks should be defined at the module level',
recommended: true,
};
}
case ErrorCategory.FBT: {
return {
category,
severity: ErrorSeverity.Error,
name: 'fbt',
description: 'Validates usage of fbt',
recommended: false,
};
}
case ErrorCategory.Fire: {
return {
category,
severity: ErrorSeverity.Error,
name: 'fire',
description: 'Validates usage of `fire`',
recommended: false,
};
}
case ErrorCategory.Gating: {
return {
category,
severity: ErrorSeverity.Error,
name: 'gating',
description:
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
recommended: true,
};
}
case ErrorCategory.Globals: {
return {
category,
severity: ErrorSeverity.Error,
name: 'globals',
description:
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
recommended: true,
};
}
case ErrorCategory.Hooks: {
return {
category,
severity: ErrorSeverity.Error,
name: 'hooks',
description: 'Validates the rules of hooks',
/**
* TODO: the "Hooks" rule largely reimplements the "rules-of-hooks" non-compiler rule.
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
* this rule.
*/
recommended: false,
};
}
case ErrorCategory.Immutability: {
return {
category,
severity: ErrorSeverity.Error,
name: 'immutability',
description:
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
recommended: true,
};
}
case ErrorCategory.Invariant: {
return {
category,
severity: ErrorSeverity.Error,
name: 'invariant',
description: 'Internal invariants',
recommended: false,
};
}
case ErrorCategory.PreserveManualMemo: {
return {
category,
severity: ErrorSeverity.Error,
name: 'preserve-manual-memoization',
description:
'Validates that existing manual memoized is preserved by the compiler. ' +
'React Compiler will only compile components and hooks if its inference ' +
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
recommended: true,
};
}
case ErrorCategory.Purity: {
return {
category,
severity: ErrorSeverity.Error,
name: 'purity',
description:
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
recommended: true,
};
}
case ErrorCategory.Refs: {
return {
category,
severity: ErrorSeverity.Error,
name: 'refs',
description:
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
recommended: true,
};
}
case ErrorCategory.RenderSetState: {
return {
category,
severity: ErrorSeverity.Error,
name: 'set-state-in-render',
description:
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
recommended: true,
};
}
case ErrorCategory.StaticComponents: {
return {
category,
severity: ErrorSeverity.Error,
name: 'static-components',
description:
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
recommended: true,
};
}
case ErrorCategory.Suppression: {
return {
category,
severity: ErrorSeverity.Error,
name: 'rule-suppression',
description: 'Validates against suppression of other rules',
recommended: false,
};
}
case ErrorCategory.Syntax: {
return {
category,
severity: ErrorSeverity.Error,
name: 'syntax',
description: 'Validates against invalid syntax',
recommended: false,
};
}
case ErrorCategory.Todo: {
return {
category,
severity: ErrorSeverity.Hint,
name: 'todo',
description: 'Unimplemented features',
recommended: false,
};
}
case ErrorCategory.UnsupportedSyntax: {
return {
category,
severity: ErrorSeverity.Warning,
name: 'unsupported-syntax',
description:
'Validates against syntax that we do not plan to support in React Compiler',
recommended: true,
};
}
case ErrorCategory.UseMemo: {
return {
category,
severity: ErrorSeverity.Error,
name: 'use-memo',
description:
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
recommended: true,
};
}
case ErrorCategory.IncompatibleLibrary: {
return {
category,
severity: ErrorSeverity.Warning,
name: 'incompatible-library',
description:
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
recommended: true,
};
}
default: {
assertExhaustive(category, `Unsupported category ${category}`);
}
}
}
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
category => getRuleForCategory(category as any),
);

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,
@@ -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;
@@ -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,
@@ -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,
},
);
}

View File

@@ -135,12 +135,7 @@ export type PluginOptions = {
*/
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.
*/

View File

@@ -33,7 +33,9 @@ import {findContextIdentifiers} from '../HIR/FindContextIdentifiers';
import {
analyseFunctions,
dropManualMemoization,
inferMutableRanges,
inferReactivePlaces,
inferReferenceEffects,
inlineImmediatelyInvokedFunctionExpressions,
inferEffectDependencies,
} from '../Inference';
@@ -90,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';
@@ -98,12 +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 {nameAnonymousFunctions} from '../Transform/NameAnonymousFunctions';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -227,12 +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.isInferredMemoEnabled) {
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.config.enableNewMutationAliasingModel) {
validateLocalsNotReassignedAfterRender(hir);
}
// Note: Has to come after infer reference effects because "dead" code may still affect inference
@@ -247,15 +263,20 @@ 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.isInferredMemoEnabled) {
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.isInferredMemoEnabled) {
@@ -271,10 +292,6 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
@@ -287,7 +304,12 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}
inferReactivePlaces(hir);
@@ -300,6 +322,13 @@ function runWithEnvironment(
value: hir,
});
propagatePhiTypes(hir);
log({
kind: 'hir',
name: 'PropagatePhiTypes',
value: hir,
});
if (env.isInferredMemoEnabled) {
if (env.config.validateStaticComponents) {
env.logErrors(validateStaticComponents(hir));
@@ -325,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});

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';
@@ -104,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();
@@ -120,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,
});
@@ -138,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;
}
@@ -209,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;
@@ -310,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;
}
@@ -425,14 +422,7 @@ export function compileProgram(
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,
@@ -465,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,
}),
);
@@ -500,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;
}
@@ -833,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,
}),
);
@@ -857,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,
@@ -961,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: {
@@ -1420,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

@@ -11,7 +11,7 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
import {GeneratedSource} from '../HIR';
@@ -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.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,
});
@@ -1576,7 +1519,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,
});
@@ -1601,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,
});
@@ -1658,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,
});
@@ -1684,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,
});
@@ -1705,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,
});
@@ -1738,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,
});
@@ -1758,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,
});
@@ -1784,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,
});
@@ -1818,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,
});
@@ -1830,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,
});
@@ -1859,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,
});
@@ -2071,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,
});
@@ -2099,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,
});
@@ -2198,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,
});
@@ -2237,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,
});
@@ -2250,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,
});
@@ -2259,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;
@@ -2286,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,
});
@@ -2296,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,
});
@@ -2319,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,
});
}
@@ -2359,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 {
@@ -2420,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,
});
@@ -2430,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;
@@ -2444,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,
});
@@ -2466,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,
});
@@ -2476,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,
});
@@ -2518,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: [
{
@@ -2533,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: [
{
@@ -2654,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,
});
@@ -2662,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,
});
@@ -2679,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,
});
@@ -2691,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,
});
@@ -2746,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,
});
@@ -2755,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,
});
@@ -2834,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,
});
@@ -3058,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,
});
@@ -3254,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,
});
@@ -3289,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,
});
@@ -3310,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,
});
@@ -3369,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,
});
@@ -3383,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,
});
@@ -3408,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,
});
@@ -3456,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);
@@ -3493,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,
});
@@ -3575,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,
@@ -3677,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,
});
@@ -3733,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,
});
@@ -3745,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
@@ -3802,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,
});
@@ -3816,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,
});
@@ -3860,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>;
@@ -3893,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,
});
@@ -3905,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,
});
@@ -3970,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,
@@ -4009,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,
@@ -4082,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,
});
@@ -4113,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,
@@ -4130,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,
});
@@ -4139,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,
});
@@ -4153,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,
});
@@ -4175,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,
@@ -4324,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;
@@ -505,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(
@@ -584,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;
@@ -623,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,91 +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`,
},
},
};
}
}
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

@@ -49,8 +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';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -246,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.function().args(z.string())).default(null),
enableNewMutationAliasingModel: z.boolean().default(true),
/**
* Enables inference of optional dependency chains. Without this flag
@@ -261,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,
@@ -328,12 +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),
/**
* Validates against creating JSX within a try block and recommends using an error boundary
* instead.
@@ -653,13 +642,6 @@ export const EnvironmentConfigSchema = z.object({
* useMemo(() => { ... }, [...]);
*/
validateNoVoidUseMemo: z.boolean().default(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.
*/
validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false),
});
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -709,8 +691,6 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -752,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(
@@ -785,40 +759,6 @@ 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 isInferredMemoEnabled(): boolean {
@@ -883,16 +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;
}
const unparsedModuleConfig = moduleTypeProvider(moduleName);
const unparsedModuleConfig = this.config.moduleTypeProvider(moduleName);
if (unparsedModuleConfig != null) {
const parsedModuleConfig = TypeSchema.safeParse(unparsedModuleConfig);
if (!parsedModuleConfig.success) {
@@ -1066,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;
@@ -1097,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') {
@@ -1128,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

@@ -114,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',
},
],
},
}),
],
]),
],
[
@@ -1001,7 +908,6 @@ export function installTypeConfig(
mutableOnlyIfOperandsAreMutable:
typeConfig.mutableOnlyIfOperandsAreMutable === true,
aliasing: typeConfig.aliasing,
knownIncompatible: typeConfig.knownIncompatible ?? null,
});
}
case 'hook': {
@@ -1020,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';
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
@@ -1130,8 +1139,7 @@ export type JsxAttribute =
export type FunctionExpression = {
kind: 'FunctionExpression';
name: ValidIdentifierName | null;
nameHint: string | null;
name: string | null;
loweredFunc: LoweredFunction;
type:
| 'ArrowFunctionExpression'
@@ -1306,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,
};
}
/**
@@ -1363,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 = {
@@ -1393,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 = {
@@ -1568,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,
});
}
@@ -1707,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;
@@ -1730,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;
@@ -1753,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;
@@ -1776,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;
@@ -1799,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;

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);
@@ -350,7 +331,6 @@ export type FunctionSignature = {
mutableOnlyIfOperandsAreMutable?: boolean;
impure?: boolean;
knownIncompatible?: string | null | undefined;
canonicalName?: string;

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

@@ -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

@@ -1233,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();
}
@@ -1254,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

@@ -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,
@@ -296,11 +300,11 @@ function extractManualMemoizationArgs(
if (fnPlace == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected a callback function to be passed to ${kind}`,
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: `Expected a callback function to be passed to ${kind}`,
@@ -311,11 +315,11 @@ function extractManualMemoizationArgs(
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Unexpected spread argument to ${kind}`,
severity: ErrorSeverity.InvalidReact,
category: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetails({
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: `Unexpected spread argument to ${kind}`,
@@ -331,11 +335,11 @@ function extractManualMemoizationArgs(
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: `Expected the dependency list for ${kind} to be an array literal`,
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: depsListPlace.loc,
message: `Expected the dependency list for ${kind} to be an array literal`,
@@ -349,11 +353,11 @@ function extractManualMemoizationArgs(
if (maybeDep == 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\`)`,
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,
}).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\`)`,
@@ -454,15 +458,15 @@ export function dropManualMemoization(
if (!hasNonVoidReturn(funcToCheck.loweredFunc.func)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks must return a value',
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`,
} callback doesn't return a value. useMemo is for computing and caching values, not for arbitrary side effects.`,
suggestions: null,
}).withDetails({
}).withDetail({
kind: 'error',
loc: instr.value.loc,
message: 'useMemo() callbacks must return a value',
@@ -494,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`,
@@ -613,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,
});
}
}

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;
}

View File

@@ -0,0 +1,54 @@
/**
* 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,
InstructionId,
isRefOrRefValue,
} from '../HIR/HIR';
import DisjointSet from '../Utils/DisjointSet';
export function inferMutableRangesForAlias(
_fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
const aliasSets = aliases.buildSets();
for (const aliasSet of aliasSets) {
/*
* Update mutableRange.end only if the identifiers have actually been
* mutated.
*/
const mutatingIdentifiers = [...aliasSet].filter(
id =>
id.mutableRange.end - id.mutableRange.start > 1 && !isRefOrRefValue(id),
);
if (mutatingIdentifiers.length > 0) {
// Find final instruction which mutates this alias set.
let lastMutatingInstructionId = 0;
for (const id of mutatingIdentifiers) {
if (id.mutableRange.end > lastMutatingInstructionId) {
lastMutatingInstructionId = id.mutableRange.end;
}
}
/*
* Update mutableRange.end for all aliases in this set ending before the
* last mutation.
*/
for (const alias of aliasSet) {
if (
alias.mutableRange.end < lastMutatingInstructionId &&
!isRefOrRefValue(alias)
) {
alias.mutableRange.end = lastMutatingInstructionId as InstructionId;
}
}
}
}
}

View File

@@ -9,6 +9,7 @@ import {
CompilerDiagnostic,
CompilerError,
Effect,
ErrorSeverity,
SourceLocation,
ValueKind,
} from '..';
@@ -18,7 +19,6 @@ import {
DeclarationId,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
Hole,
IdentifierId,
@@ -26,7 +26,6 @@ import {
InstructionKind,
InstructionValue,
isArrayType,
isJsxType,
isMapType,
isPrimitiveType,
isRefOrRefValue,
@@ -35,7 +34,6 @@ import {
Phi,
Place,
SpreadPattern,
Type,
ValueReason,
} from '../HIR';
import {
@@ -45,6 +43,12 @@ import {
eachTerminalSuccessor,
} from '../HIR/visitors';
import {Ok, Result} from '../Utils/Result';
import {
getArgumentEffect,
getFunctionCallSignature,
isKnownMutableEffect,
mergeValueKinds,
} from './InferReferenceEffects';
import {
assertExhaustive,
getOrInsertDefault,
@@ -58,17 +62,13 @@ import {
printInstruction,
printInstructionValue,
printPlace,
printSourceLocation,
} from '../HIR/PrintHIR';
import {FunctionSignature} from '../HIR/ObjectShape';
import {getWriteErrorReason} from './InferFunctionEffects';
import prettyFormat from 'pretty-format';
import {createTemporaryPlace} from '../HIR/HIRBuilder';
import {
AliasingEffect,
AliasingSignature,
hashEffect,
MutationReason,
} from './AliasingEffects';
import {ErrorCategory} from '../CompilerError';
import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
const DEBUG = false;
@@ -134,13 +134,7 @@ export function inferMutationAliasingEffects(
reason:
'Expected React component to have not more than two parameters: one for props and for ref',
description: null,
details: [
{
kind: 'error',
loc: fn.loc,
message: null,
},
],
loc: fn.loc,
suggestions: null,
});
const [props, ref] = fn.params;
@@ -207,13 +201,7 @@ export function inferMutationAliasingEffects(
CompilerError.invariant(false, {
reason: `[InferMutationAliasingEffects] Potential infinite loop`,
description: `A value, temporary place, or effect was not cached properly`,
details: [
{
kind: 'error',
loc: fn.loc,
message: null,
},
],
loc: fn.loc,
});
}
for (const [blockId, block] of fn.body.blocks) {
@@ -368,14 +356,7 @@ function inferBlock(
CompilerError.invariant(state.kind(handlerParam) != null, {
reason:
'Expected catch binding to be intialized with a DeclareLocal Catch instruction',
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
const effects: Array<AliasingEffect> = [];
for (const instr of block.instructions) {
@@ -464,34 +445,25 @@ function applySignature(
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'This value cannot be modified',
description: reason,
}).withDetails({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetails({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
effects.push({
kind: 'MutateFrozen',
place: effect.value,
error: diagnostic,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
}),
});
}
}
@@ -525,14 +497,7 @@ function applySignature(
) {
CompilerError.invariant(false, {
reason: `Expected instruction lvalue to be initialized`,
description: null,
details: [
{
kind: 'error',
loc: instruction.loc,
message: null,
},
],
loc: instruction.loc,
});
}
return effects.length !== 0 ? effects : null;
@@ -561,13 +526,7 @@ function applyEffect(
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
details: [
{
kind: 'error',
loc: effect.into.loc,
message: null,
},
],
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
@@ -606,13 +565,7 @@ function applyEffect(
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
details: [
{
kind: 'error',
loc: effect.into.loc,
message: null,
},
],
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
@@ -672,13 +625,7 @@ function applyEffect(
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
details: [
{
kind: 'error',
loc: effect.into.loc,
message: null,
},
],
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
@@ -752,13 +699,7 @@ function applyEffect(
{
reason: `Expected destination value to already be initialized within this instruction for Alias effect`,
description: `Destination ${printPlace(effect.into)} is not initialized in this instruction`,
details: [
{
kind: 'error',
loc: effect.into.loc,
message: null,
},
],
loc: effect.into.loc,
},
);
/*
@@ -817,13 +758,7 @@ function applyEffect(
CompilerError.invariant(!initialized.has(effect.into.identifier.id), {
reason: `Cannot re-initialize variable within an instruction`,
description: `Re-initialized ${printPlace(effect.into)} in ${printAliasingEffect(effect)}`,
details: [
{
kind: 'error',
loc: effect.into.loc,
message: null,
},
],
loc: effect.into.loc,
});
initialized.add(effect.into.identifier.id);
@@ -1092,18 +1027,18 @@ function applyEffect(
effect.value.identifier.declarationId,
);
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'Cannot access variable before it is declared',
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time`,
severity: ErrorSeverity.InvalidReact,
category: 'Cannot access variable before it is declared',
description: `${variable ?? 'This variable'} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time.`,
});
if (hoistedAccess != null && hoistedAccess.loc != effect.value.loc) {
diagnostic.withDetails({
diagnostic.withDetail({
kind: 'error',
loc: hoistedAccess.loc,
message: `${variable ?? 'variable'} accessed before it is declared`,
});
}
diagnostic.withDetails({
diagnostic.withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable ?? 'variable'} is declared here`,
@@ -1124,30 +1059,13 @@ function applyEffect(
const reason = getWriteErrorReason({
kind: value.kind,
reason: value.reason,
context: new Set(),
});
const variable =
effect.value.identifier.name !== null &&
effect.value.identifier.name.kind === 'named'
? `\`${effect.value.identifier.name.value}\``
: 'value';
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
reason: 'This value cannot be modified',
description: reason,
}).withDetails({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetails({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
applyEffect(
context,
state,
@@ -1157,7 +1075,15 @@ function applyEffect(
? 'MutateFrozen'
: 'MutateGlobal',
place: effect.value,
error: diagnostic,
error: CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
}),
},
initialized,
effects,
@@ -1224,13 +1150,7 @@ class InferenceState {
reason:
'[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
description: null,
details: [
{
kind: 'error',
loc: value.loc,
message: null,
},
],
loc: value.loc,
suggestions: null,
});
this.#values.set(value, kind);
@@ -1241,13 +1161,7 @@ class InferenceState {
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
description: `${printPlace(place)}`,
details: [
{
kind: 'error',
loc: place.loc,
message: 'this is uninitialized',
},
],
loc: place.loc,
suggestions: null,
});
return Array.from(values);
@@ -1259,13 +1173,7 @@ class InferenceState {
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
description: `${printPlace(place)}`,
details: [
{
kind: 'error',
loc: place.loc,
message: 'this is uninitialized',
},
],
loc: place.loc,
suggestions: null,
});
let mergedKind: AbstractValue | null = null;
@@ -1277,13 +1185,7 @@ class InferenceState {
CompilerError.invariant(mergedKind !== null, {
reason: `[InferMutationAliasingEffects] Expected at least one value`,
description: `No value found at \`${printPlace(place)}\``,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
loc: place.loc,
suggestions: null,
});
return mergedKind;
@@ -1295,13 +1197,7 @@ class InferenceState {
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
description: `${printIdentifier(value.identifier)}`,
details: [
{
kind: 'error',
loc: value.loc,
message: 'Expected value for identifier to be initialized',
},
],
loc: value.loc,
suggestions: null,
});
this.#variables.set(place.identifier.id, new Set(values));
@@ -1312,13 +1208,7 @@ class InferenceState {
CompilerError.invariant(values != null, {
reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
description: `${printIdentifier(value.identifier)}`,
details: [
{
kind: 'error',
loc: value.loc,
message: 'Expected value for identifier to be initialized',
},
],
loc: value.loc,
suggestions: null,
});
const prevValues = this.values(place);
@@ -1331,15 +1221,11 @@ class InferenceState {
// Defines (initializing or updating) a variable with a specific kind of value.
define(place: Place, value: InstructionValue): void {
CompilerError.invariant(this.#values.has(value), {
reason: `[InferMutationAliasingEffects] Expected value to be initialized`,
reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
value.loc,
)}'`,
description: printInstructionValue(value),
details: [
{
kind: 'error',
loc: value.loc,
message: 'Expected value for identifier to be initialized',
},
],
loc: value.loc,
suggestions: null,
});
this.#variables.set(place.identifier.id, new Set([value]));
@@ -1794,15 +1680,7 @@ function computeSignatureForInstruction(
}
case 'PropertyStore':
case 'ComputedStore': {
const mutationReason: MutationReason | null =
value.kind === 'PropertyStore' && value.property === 'current'
? {kind: 'AssignCurrentProperty'}
: null;
effects.push({
kind: 'Mutate',
value: value.object,
reason: mutationReason,
});
effects.push({kind: 'Mutate', value: value.object});
effects.push({
kind: 'Capture',
from: value.value,
@@ -1963,23 +1841,6 @@ function computeSignatureForInstruction(
});
}
}
for (const prop of value.props) {
if (
prop.kind === 'JsxAttribute' &&
prop.place.identifier.type.kind === 'Function' &&
(isJsxType(prop.place.identifier.type.return) ||
(prop.place.identifier.type.return.kind === 'Phi' &&
prop.place.identifier.type.return.operands.some(operand =>
isJsxType(operand),
)))
) {
// Any props which return jsx are assumed to be called during render
effects.push({
kind: 'Render',
place: prop.place,
});
}
}
}
break;
}
@@ -2146,11 +2007,11 @@ function computeSignatureForInstruction(
kind: 'MutateGlobal',
place: value.value,
error: CompilerDiagnostic.create({
category: ErrorCategory.Globals,
reason:
severity: ErrorSeverity.InvalidReact,
category:
'Cannot reassign variables declared outside of the component/hook',
description: `Variable ${variable} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)`,
}).withDetails({
}).withDetail({
kind: 'error',
loc: instr.loc,
message: `${variable} cannot be reassigned`,
@@ -2179,7 +2040,7 @@ function computeSignatureForInstruction(
effects.push({
kind: 'Freeze',
value: operand,
reason: ValueReason.HookCaptured,
reason: ValueReason.Other,
});
}
}
@@ -2245,40 +2106,20 @@ function computeEffectsForLegacySignature(
kind: 'Impure',
place: receiver,
error: CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
severity: ErrorSeverity.InvalidReact,
category: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
: '') +
'Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
}).withDetails({
}).withDetail({
kind: 'error',
loc,
message: 'Cannot call impure function',
}),
});
}
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
const errors = new CompilerError();
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.IncompatibleLibrary,
reason: 'Use of incompatible library',
description: [
'This API returns functions which cannot be memoized without leading to stale UI. ' +
'To prevent this, by default React Compiler will skip memoizing this component/hook. ' +
'However, you may see issues if values from this API are passed to other components/hooks that are ' +
'memoized',
].join(''),
}).withDetails({
kind: 'error',
loc: receiver.loc,
message: signature.knownIncompatible,
}),
);
throw errors;
}
const stores: Array<Place> = [];
const captures: Array<Place> = [];
function visit(place: Place, effect: Effect): void {
@@ -2693,208 +2534,3 @@ export type AbstractValue = {
kind: ValueKind;
reason: ReadonlySet<ValueReason>;
};
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';
}
}
function getArgumentEffect(
signatureEffect: Effect | null,
arg: Place | SpreadPattern,
): Effect {
if (signatureEffect != null) {
if (arg.kind === 'Identifier') {
return signatureEffect;
} else if (
signatureEffect === Effect.Mutate ||
signatureEffect === Effect.ConditionallyMutate
) {
return signatureEffect;
} else {
// see call-spread-argument-mutable-iterator test fixture
if (signatureEffect === Effect.Freeze) {
CompilerError.throwTodo({
reason: 'Support spread syntax for hook arguments',
loc: arg.place.loc,
});
}
// effects[i] is Effect.Capture | Effect.Read | Effect.Store
return Effect.ConditionallyMutateIterator;
}
} else {
return Effect.ConditionallyMutate;
}
}
export function getFunctionCallSignature(
env: Environment,
type: Type,
): FunctionSignature | null {
if (type.kind !== 'Function') {
return null;
}
return env.getFunctionSignature(type);
}
export function isKnownMutableEffect(effect: Effect): boolean {
switch (effect) {
case Effect.Store:
case Effect.ConditionallyMutate:
case Effect.ConditionallyMutateIterator:
case Effect.Mutate: {
return true;
}
case Effect.Unknown: {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
suggestions: null,
});
}
case Effect.Read:
case Effect.Capture:
case Effect.Freeze: {
return false;
}
default: {
assertExhaustive(effect, `Unexpected effect \`${effect}\``);
}
}
}
/**
* Joins two values using the following rules:
* == Effect Transitions ==
*
* Freezing an immutable value has not effect:
* ┌───────────────┐
* │ │
* ▼ │ Freeze
* ┌──────────────────────────┐ │
* │ Immutable │──┘
* └──────────────────────────┘
*
* Freezing a mutable or maybe-frozen value makes it frozen. Freezing a frozen
* value has no effect:
* ┌───────────────┐
* ┌─────────────────────────┐ Freeze │ │
* │ MaybeFrozen │────┐ ▼ │ Freeze
* └─────────────────────────┘ │ ┌──────────────────────────┐ │
* ├────▶│ Frozen │──┘
* │ └──────────────────────────┘
* ┌─────────────────────────┐ │
* │ Mutable │────┘
* └─────────────────────────┘
*
* == Join Lattice ==
* - immutable | mutable => mutable
* The justification is that immutable and mutable values are different types,
* and functions can introspect them to tell the difference (if the argument
* is null return early, else if its an object mutate it).
* - frozen | mutable => maybe-frozen
* Frozen values are indistinguishable from mutable values at runtime, so callers
* cannot dynamically avoid mutation of "frozen" values. If a value could be
* frozen we have to distinguish it from a mutable value. But it also isn't known
* frozen yet, so we distinguish as maybe-frozen.
* - immutable | frozen => frozen
* This is subtle and falls out of the above rules. If a value could be any of
* immutable, mutable, or frozen, then at runtime it could either be a primitive
* or a reference type, and callers can't distinguish frozen or not for reference
* types. To ensure that any sequence of joins btw those three states yields the
* correct maybe-frozen, these two have to produce a frozen value.
* - <any> | maybe-frozen => maybe-frozen
* - immutable | context => context
* - mutable | context => context
* - frozen | context => maybe-frozen
*
* ┌──────────────────────────┐
* │ Immutable │───┐
* └──────────────────────────┘ │
* │ ┌─────────────────────────┐
* ├───▶│ Frozen │──┐
* ┌──────────────────────────┐ │ └─────────────────────────┘ │
* │ Frozen │───┤ │ ┌─────────────────────────┐
* └──────────────────────────┘ │ ├─▶│ MaybeFrozen │
* │ ┌─────────────────────────┐ │ └─────────────────────────┘
* ├───▶│ MaybeFrozen │──┘
* ┌──────────────────────────┐ │ └─────────────────────────┘
* │ Mutable │───┘
* └──────────────────────────┘
*/
function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
if (a === b) {
return a;
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
return ValueKind.MaybeFrozen;
// after this a and b differ and neither are MaybeFrozen
} else if (a === ValueKind.Mutable || b === ValueKind.Mutable) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
// frozen | mutable
return ValueKind.MaybeFrozen;
} else if (a === ValueKind.Context || b === ValueKind.Context) {
// context | mutable
return ValueKind.Context;
} else {
// mutable | immutable
return ValueKind.Mutable;
}
} else if (a === ValueKind.Context || b === ValueKind.Context) {
if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
// frozen | context
return ValueKind.MaybeFrozen;
} else {
// context | immutable
return ValueKind.Context;
}
} else if (a === ValueKind.Frozen || b === ValueKind.Frozen) {
return ValueKind.Frozen;
} else if (a === ValueKind.Global || b === ValueKind.Global) {
return ValueKind.Global;
} else {
CompilerError.invariant(
a === ValueKind.Primitive && b == ValueKind.Primitive,
{
reason: `Unexpected value kind in mergeValues()`,
description: `Found kinds ${a} and ${b}`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
},
);
return ValueKind.Primitive;
}
}

View File

@@ -27,7 +27,7 @@ import {
} from '../HIR/visitors';
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
import {Err, Ok, Result} from '../Utils/Result';
import {AliasingEffect, MutationReason} from './AliasingEffects';
import {AliasingEffect} from './AliasingEffects';
/**
* This pass builds an abstract model of the heap and interprets the effects of the
@@ -101,7 +101,6 @@ export function inferMutationAliasingRanges(
transitive: boolean;
kind: MutationKind;
place: Place;
reason: MutationReason | null;
}> = [];
const renders: Array<{index: number; place: Place}> = [];
@@ -177,7 +176,6 @@ export function inferMutationAliasingRanges(
effect.kind === 'MutateTransitive'
? MutationKind.Definite
: MutationKind.Conditional,
reason: null,
place: effect.value,
});
} else if (
@@ -192,7 +190,6 @@ export function inferMutationAliasingRanges(
effect.kind === 'Mutate'
? MutationKind.Definite
: MutationKind.Conditional,
reason: effect.kind === 'Mutate' ? (effect.reason ?? null) : null,
place: effect.value,
});
} else if (
@@ -229,14 +226,7 @@ export function inferMutationAliasingRanges(
} else {
CompilerError.invariant(effect.kind === 'Freeze', {
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
description: null,
details: [
{
kind: 'error',
loc: block.terminal.loc,
message: null,
},
],
loc: block.terminal.loc,
});
}
}
@@ -251,7 +241,6 @@ export function inferMutationAliasingRanges(
mutation.transitive,
mutation.kind,
mutation.place.loc,
mutation.reason,
errors,
);
}
@@ -278,7 +267,6 @@ export function inferMutationAliasingRanges(
functionEffects.push({
kind: 'Mutate',
value: {...place, loc: node.local.loc},
reason: node.mutationReason,
});
}
}
@@ -385,14 +373,7 @@ export function inferMutationAliasingRanges(
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 'MutateTransitive':
@@ -526,7 +507,6 @@ export function inferMutationAliasingRanges(
true,
MutationKind.Conditional,
into.loc,
null,
ignoredErrors,
);
for (const from of tracked) {
@@ -539,14 +519,7 @@ export function inferMutationAliasingRanges(
const fromNode = state.nodes.get(from.identifier);
CompilerError.invariant(fromNode != null, {
reason: `Expected a node to exist for all parameters and context variables`,
description: null,
details: [
{
kind: 'error',
loc: into.loc,
message: null,
},
],
loc: into.loc,
});
if (fromNode.lastMutated === mutationIndex) {
if (into.identifier.id === fn.returns.identifier.id) {
@@ -568,7 +541,7 @@ export function inferMutationAliasingRanges(
}
}
if (errors.hasAnyErrors() && !isFunctionExpression) {
if (errors.hasErrors() && !isFunctionExpression) {
return Err(errors);
}
return Ok(functionEffects);
@@ -607,7 +580,6 @@ type Node = {
transitive: {kind: MutationKind; loc: SourceLocation} | null;
local: {kind: MutationKind; loc: SourceLocation} | null;
lastMutated: number;
mutationReason: MutationReason | null;
value:
| {kind: 'Object'}
| {kind: 'Phi'}
@@ -627,7 +599,6 @@ class AliasingState {
transitive: null,
local: null,
lastMutated: 0,
mutationReason: null,
value,
});
}
@@ -726,7 +697,6 @@ class AliasingState {
transitive: boolean,
startKind: MutationKind,
loc: SourceLocation,
reason: MutationReason | null,
errors: CompilerError,
): void {
const seen = new Map<Identifier, MutationKind>();
@@ -747,7 +717,6 @@ class AliasingState {
if (node == null) {
continue;
}
node.mutationReason ??= reason;
node.lastMutated = Math.max(node.lastMutated, index);
if (end != null) {
node.id.mutableRange.end = makeInstructionId(

View File

@@ -349,13 +349,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
CompilerError.invariant(false, {
reason: 'Unexpected unknown effect',
description: null,
details: [
{
kind: 'error',
loc: operand.loc,
message: null,
},
],
loc: operand.loc,
suggestions: null,
});
}

View File

@@ -0,0 +1,49 @@
/**
* 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, HIRFunction, Identifier} from '../HIR';
import DisjointSet from '../Utils/DisjointSet';
/*
* Any values created within a try/catch block could be aliased to the try handler.
* Our lowering ensures that every instruction within a try block will be lowered into a
* basic block ending in a maybe-throw terminal that points to its catch block, so we can
* iterate such blocks and alias their instruction lvalues to the handler's param (if present).
*/
export function inferTryCatchAliases(
fn: HIRFunction,
aliases: DisjointSet<Identifier>,
): void {
const handlerParams: Map<BlockId, Identifier> = new Map();
for (const [_, block] of fn.body.blocks) {
if (
block.terminal.kind === 'try' &&
block.terminal.handlerBinding !== null
) {
handlerParams.set(
block.terminal.handler,
block.terminal.handlerBinding.identifier,
);
} else if (block.terminal.kind === 'maybe-throw') {
const handlerParam = handlerParams.get(block.terminal.handler);
if (handlerParam === undefined) {
/*
* There's no catch clause param, nothing to alias to so
* skip this block
*/
continue;
}
/*
* Otherwise alias all values created in this block to the
* catch clause param
*/
for (const instr of block.instructions) {
aliases.union([handlerParam, instr.lvalue.identifier]);
}
}
}
}

View File

@@ -7,6 +7,8 @@
export {default as analyseFunctions} from './AnalyseFunctions';
export {dropManualMemoization} from './DropManualMemoization';
export {inferMutableRanges} from './InferMutableRanges';
export {inferReactivePlaces} from './InferReactivePlaces';
export {default as inferReferenceEffects} from './InferReferenceEffects';
export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions';
export {inferEffectDependencies} from './InferEffectDependencies';

View File

@@ -191,14 +191,7 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
case 'Primitive': {
CompilerError.invariant(value.kind === 'Primitive', {
reason: 'value kind expected to be Primitive',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
@@ -211,14 +204,7 @@ function evaluatePhi(phi: Phi, constants: Constants): Constant | null {
case 'LoadGlobal': {
CompilerError.invariant(value.kind === 'LoadGlobal', {
reason: 'value kind expected to be LoadGlobal',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});

View File

@@ -42,7 +42,6 @@ import {
mapInstructionValueOperands,
mapTerminalOperands,
} from '../HIR/visitors';
import {ErrorCategory} from '../CompilerError';
type InlinedJsxDeclarationMap = Map<
DeclarationId,
@@ -84,7 +83,6 @@ export function inlineJsxTransform(
kind: 'CompileDiagnostic',
fnLoc: null,
detail: {
category: ErrorCategory.Todo,
reason: 'JSX Inlining is not supported on value blocks',
loc: instr.loc,
},
@@ -709,14 +707,7 @@ function createPropsProperties(
const spreadProp = jsxSpreadAttributes[0];
CompilerError.invariant(spreadProp.kind === 'JsxSpreadAttribute', {
reason: 'Spread prop attribute must be of kind JSXSpreadAttribute',
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
loc: instr.loc,
});
propsProperty = {
kind: 'ObjectProperty',

View File

@@ -78,17 +78,10 @@ export function instructionReordering(fn: HIRFunction): void {
}
CompilerError.invariant(shared.size === 0, {
reason: `InstructionReordering: expected all reorderable nodes to have been emitted`,
description: null,
details: [
{
kind: 'error',
loc:
[...shared.values()]
.map(node => node.instruction?.loc)
.filter(loc => loc != null)[0] ?? GeneratedSource,
message: null,
},
],
loc:
[...shared.values()]
.map(node => node.instruction?.loc)
.filter(loc => loc != null)[0] ?? GeneratedSource,
});
markInstructionIds(fn.body);
}
@@ -309,13 +302,7 @@ function reorderBlock(
node.reorderability === Reorderability.Reorderable,
{
reason: `Expected all remaining instructions to be reorderable`,
details: [
{
kind: 'error',
loc: node.instruction?.loc ?? block.terminal.loc,
message: null,
},
],
loc: node.instruction?.loc ?? block.terminal.loc,
description:
node.instruction != null
? `Instruction [${node.instruction.id}] was not emitted yet but is not reorderable`

View File

@@ -249,13 +249,13 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
const fn: HIRFunction = {
loc: GeneratedSource,
id: null,
nameHint: null,
fnType: 'Other',
env,
params: [obj],
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -263,7 +263,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
reversePostorderBlocks(fn.body);
@@ -276,7 +275,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
value: {
kind: 'FunctionExpression',
name: null,
nameHint: null,
loweredFunc: {
func: fn,
},

View File

@@ -31,9 +31,7 @@ export function outlineFunctions(
) {
const loweredFunc = value.loweredFunc.func;
const id = fn.env.generateGloballyUniqueIdentifierName(
loweredFunc.id ?? loweredFunc.nameHint,
);
const id = fn.env.generateGloballyUniqueIdentifierName(loweredFunc.id);
loweredFunc.id = id.value;
fn.env.outlineFunction(loweredFunc, null);

View File

@@ -364,13 +364,13 @@ function emitOutlinedFn(
const fn: HIRFunction = {
loc: GeneratedSource,
id: null,
nameHint: null,
fnType: 'Other',
env,
params: [propsObj],
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -378,7 +378,6 @@ function emitOutlinedFn(
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
return fn;
}

View File

@@ -52,13 +52,7 @@ export function pruneMaybeThrows(fn: HIRFunction): void {
const mappedTerminal = terminalMapping.get(predecessor);
CompilerError.invariant(mappedTerminal != null, {
reason: `Expected non-existing phi operand's predecessor to have been mapped to a new terminal`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
description: `Could not find mapping for predecessor bb${predecessor} in block bb${
block.id
} for phi ${printPlace(phi.place)}`,

View File

@@ -41,15 +41,8 @@ function findScopesToMerge(fn: HIRFunction): DisjointSet<ReactiveScope> {
{
reason:
'Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.',
description: null,
suggestions: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
},
);
mergeScopesBuilder.union([operandScope, lvalueScope]);

View File

@@ -170,53 +170,11 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
CompilerError.invariant(!valueBlockNodes.has(fallthrough), {
reason: 'Expect hir blocks to have unique fallthroughs',
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
if (node != null) {
valueBlockNodes.set(fallthrough, node);
}
} else if (terminal.kind === 'goto') {
/**
* If we encounter a goto that is not to the natural fallthrough of the current
* block (not the topmost fallthrough on the stack), then this is a goto to a
* label. Any scopes that extend beyond the goto must be extended to include
* the labeled range, so that the break statement doesn't accidentally jump
* out of the scope. We do this by extending the start and end of the scope's
* range to the label and its fallthrough respectively.
*/
const start = activeBlockFallthroughRanges.find(
range => range.fallthrough === terminal.block,
);
if (start != null && start !== activeBlockFallthroughRanges.at(-1)) {
const fallthroughBlock = fn.body.blocks.get(start.fallthrough)!;
const firstId =
fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
for (const scope of activeScopes) {
/**
* activeScopes is only filtered at block start points, so some of the
* scopes may not actually be active anymore, ie we've past their end
* instruction. Only extend ranges for scopes that are actually active.
*
* TODO: consider pruning activeScopes per instruction
*/
if (scope.range.end <= terminal.id) {
continue;
}
scope.range.start = makeInstructionId(
Math.min(start.range.start, scope.range.start),
);
scope.range.end = makeInstructionId(
Math.max(firstId, scope.range.end),
);
}
}
}
/*
@@ -259,14 +217,7 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void {
// Transition from block->value block, derive the outer block range
CompilerError.invariant(fallthrough !== null, {
reason: `Expected a fallthrough for value block`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
const fallthroughBlock = fn.body.blocks.get(fallthrough)!;
const nextId =

View File

@@ -81,16 +81,10 @@ class CheckInstructionsAgainstScopesVisitor extends ReactiveFunctionVisitor<
!this.activeScopes.has(scope.id)
) {
CompilerError.invariant(false, {
description: `Instruction [${id}] is part of scope @${scope.id}, but that scope has already completed.`,
loc: place.loc,
reason:
'Encountered an instruction that should be part of a scope, but where that scope has already completed',
description: `Instruction [${id}] is part of scope @${scope.id}, but that scope has already completed`,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
suggestions: null,
});
}

View File

@@ -28,14 +28,7 @@ class Visitor extends ReactiveFunctionVisitor<Set<BlockId>> {
if (terminal.kind === 'break' || terminal.kind === 'continue') {
CompilerError.invariant(seenLabels.has(terminal.target), {
reason: 'Unexpected break to invalid label',
description: null,
details: [
{
kind: 'error',
loc: stmt.terminal.loc,
message: null,
},
],
loc: stmt.terminal.loc,
});
}
}

View File

@@ -44,7 +44,6 @@ export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
return {
loc: fn.loc,
id: fn.id,
nameHint: fn.nameHint,
params: fn.params,
generator: fn.generator,
async: fn.async,
@@ -71,13 +70,7 @@ class Driver {
CompilerError.invariant(!this.cx.emitted.has(block.id), {
reason: `Cannot emit the same block twice: bb${block.id}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
this.cx.emitted.add(block.id);
@@ -137,14 +130,7 @@ class Driver {
if (this.cx.isScheduled(terminal.consequent)) {
CompilerError.invariant(false, {
reason: `Unexpected 'if' where the consequent is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
} else {
consequent = this.traverseBlock(
@@ -157,14 +143,7 @@ class Driver {
if (this.cx.isScheduled(alternateId)) {
CompilerError.invariant(false, {
reason: `Unexpected 'if' where the alternate is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
} else {
alternate = this.traverseBlock(this.cx.ir.blocks.get(alternateId)!);
@@ -217,14 +196,7 @@ class Driver {
if (this.cx.isScheduled(case_.block)) {
CompilerError.invariant(case_.block === terminal.fallthrough, {
reason: `Unexpected 'switch' where a case is already scheduled and block is not the fallthrough`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
return;
} else {
@@ -283,14 +255,7 @@ class Driver {
} else {
CompilerError.invariant(false, {
reason: `Unexpected 'do-while' where the loop is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
}
@@ -351,14 +316,7 @@ class Driver {
} else {
CompilerError.invariant(false, {
reason: `Unexpected 'while' where the loop is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
}
@@ -444,14 +402,7 @@ class Driver {
} else {
CompilerError.invariant(false, {
reason: `Unexpected 'for' where the loop is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
}
@@ -549,14 +500,7 @@ class Driver {
} else {
CompilerError.invariant(false, {
reason: `Unexpected 'for-of' where the loop is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
}
@@ -628,14 +572,7 @@ class Driver {
} else {
CompilerError.invariant(false, {
reason: `Unexpected 'for-in' where the loop is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
}
@@ -678,14 +615,7 @@ class Driver {
if (this.cx.isScheduled(terminal.alternate)) {
CompilerError.invariant(false, {
reason: `Unexpected 'branch' where the alternate is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
} else {
alternate = this.traverseBlock(
@@ -723,14 +653,7 @@ class Driver {
if (this.cx.isScheduled(terminal.block)) {
CompilerError.invariant(false, {
reason: `Unexpected 'label' where the block is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
} else {
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
@@ -888,14 +811,7 @@ class Driver {
if (this.cx.isScheduled(terminal.block)) {
CompilerError.invariant(false, {
reason: `Unexpected 'scope' where the block is already scheduled`,
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
} else {
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
@@ -921,13 +837,7 @@ class Driver {
CompilerError.invariant(false, {
reason: 'Unexpected unsupported terminal',
description: null,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
suggestions: null,
});
}
@@ -964,13 +874,7 @@ class Driver {
reason:
'Expected branch block to end in an instruction that sets the test value',
description: null,
details: [
{
kind: 'error',
loc: instr.lvalue.loc,
message: null,
},
],
loc: instr.lvalue.loc,
suggestions: null,
},
);
@@ -1002,13 +906,7 @@ class Driver {
CompilerError.invariant(false, {
reason: 'Expected goto value block to have at least one instruction',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
} else if (defaultBlock.instructions.length === 1) {
@@ -1293,27 +1191,14 @@ class Driver {
CompilerError.invariant(false, {
reason: 'Expected a break target',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}
if (this.cx.scopeFallthroughs.has(target.block)) {
CompilerError.invariant(target.type === 'implicit', {
reason: 'Expected reactive scope to implicitly break to fallthrough',
description: null,
details: [
{
kind: 'error',
loc,
message: null,
},
],
loc,
});
return null;
}
@@ -1339,13 +1224,7 @@ class Driver {
CompilerError.invariant(target !== null, {
reason: `Expected continue target to be scheduled for bb${block}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
@@ -1420,13 +1299,7 @@ class Context {
CompilerError.invariant(!this.#scheduled.has(block), {
reason: `Break block is already scheduled: bb${block}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
this.#scheduled.add(block);
@@ -1445,13 +1318,7 @@ class Context {
CompilerError.invariant(!this.#scheduled.has(continueBlock), {
reason: `Continue block is already scheduled: bb${continueBlock}`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
this.#scheduled.add(continueBlock);
@@ -1479,13 +1346,7 @@ class Context {
CompilerError.invariant(last !== undefined && last.id === scheduleId, {
reason: 'Can only unschedule the last target',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (last.type !== 'loop' || last.ownsBlock !== null) {
@@ -1560,13 +1421,7 @@ class Context {
CompilerError.invariant(false, {
reason: 'Expected a break target',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}

View File

@@ -13,7 +13,7 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {CompilerError, ErrorCategory} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -61,7 +61,6 @@ export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
export type CodegenFunction = {
type: 'CodegenFunction';
id: t.Identifier | null;
nameHint: string | null;
params: t.FunctionDeclaration['params'];
body: t.BlockStatement;
generator: boolean;
@@ -297,14 +296,7 @@ export function codegenFunction(
CompilerError.invariant(globalGating != null, {
reason:
'Bad config not caught! Expected at least one of gating or globalGating',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
ifTest = globalGating;
@@ -373,7 +365,7 @@ function codegenReactiveFunction(
}
}
if (cx.errors.hasAnyErrors()) {
if (cx.errors.hasErrors()) {
return Err(cx.errors);
}
@@ -384,7 +376,6 @@ function codegenReactiveFunction(
type: 'CodegenFunction',
loc: fn.loc,
id: fn.id !== null ? t.identifier(fn.id) : null,
nameHint: fn.nameHint,
params,
body,
generator: fn.generator,
@@ -508,16 +499,10 @@ function codegenBlock(cx: Context, block: ReactiveBlock): t.BlockStatement {
continue;
}
CompilerError.invariant(temp.get(key)! === value, {
loc: null,
reason: 'Expected temporary value to be unchanged',
description: null,
suggestions: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
});
}
cx.temp = temp;
@@ -685,13 +670,7 @@ function codegenReactiveScope(
description: `Declaration \`${printIdentifier(
identifier,
)}\` is unnamed in scope @${scope.id}`,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
@@ -728,13 +707,7 @@ function codegenReactiveScope(
CompilerError.invariant(firstOutputIndex !== null, {
reason: `Expected scope to have at least one declaration`,
description: `Scope '@${scope.id}' has no declarations`,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
testCondition = t.binaryExpression(
@@ -757,13 +730,7 @@ function codegenReactiveScope(
{
reason: `Expected to not have both change detection enabled and memoization disabled`,
description: `Incompatible config options`,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
},
);
testCondition = t.logicalExpression(
@@ -947,14 +914,8 @@ function codegenReactiveScope(
earlyReturnValue.value.name.kind === 'named',
{
reason: `Expected early return value to be promoted to a named variable`,
loc: earlyReturnValue.loc,
description: null,
details: [
{
kind: 'error',
loc: earlyReturnValue.loc,
message: null,
},
],
suggestions: null,
},
);
@@ -984,8 +945,7 @@ function codegenTerminal(
if (terminal.targetKind === 'implicit') {
return null;
}
return createBreakStatement(
terminal.loc,
return t.breakStatement(
terminal.targetKind === 'labeled'
? t.identifier(codegenLabel(terminal.target))
: null,
@@ -995,16 +955,14 @@ function codegenTerminal(
if (terminal.targetKind === 'implicit') {
return null;
}
return createContinueStatement(
terminal.loc,
return t.continueStatement(
terminal.targetKind === 'labeled'
? t.identifier(codegenLabel(terminal.target))
: null,
);
}
case 'for': {
return createForStatement(
terminal.loc,
return t.forStatement(
codegenForInit(cx, terminal.init),
codegenInstructionValueToExpression(cx, terminal.test),
terminal.update !== null
@@ -1017,13 +975,7 @@ function codegenTerminal(
CompilerError.invariant(terminal.init.kind === 'SequenceExpression', {
reason: `Expected a sequence expression init for for..in`,
description: `Got \`${terminal.init.kind}\` expression instead`,
details: [
{
kind: 'error',
loc: terminal.init.loc,
message: null,
},
],
loc: terminal.init.loc,
suggestions: null,
});
if (terminal.init.instructions.length !== 2) {
@@ -1058,13 +1010,7 @@ function codegenTerminal(
CompilerError.invariant(false, {
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
description: `Found ${iterableItem.value.kind}`,
details: [
{
kind: 'error',
loc: iterableItem.value.loc,
message: null,
},
],
loc: iterableItem.value.loc,
suggestions: null,
});
}
@@ -1081,13 +1027,7 @@ function codegenTerminal(
reason:
'Destructure should never be Reassign as it would be an Object/ArrayPattern',
description: null,
details: [
{
kind: 'error',
loc: iterableItem.loc,
message: null,
},
],
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.Catch:
@@ -1098,13 +1038,7 @@ function codegenTerminal(
CompilerError.invariant(false, {
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..in collection`,
description: null,
details: [
{
kind: 'error',
loc: iterableItem.loc,
message: null,
},
],
loc: iterableItem.loc,
suggestions: null,
});
default:
@@ -1113,8 +1047,7 @@ function codegenTerminal(
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
);
}
return createForInStatement(
terminal.loc,
return t.forInStatement(
/*
* Special handling here since we only want the VariableDeclarators without any inits
* This needs to be updated when we handle non-trivial ForOf inits
@@ -1134,13 +1067,7 @@ function codegenTerminal(
{
reason: `Expected a single-expression sequence expression init for for..of`,
description: `Got \`${terminal.init.kind}\` expression instead`,
details: [
{
kind: 'error',
loc: terminal.init.loc,
message: null,
},
],
loc: terminal.init.loc,
suggestions: null,
},
);
@@ -1149,13 +1076,7 @@ function codegenTerminal(
CompilerError.invariant(terminal.test.kind === 'SequenceExpression', {
reason: `Expected a sequence expression test for for..of`,
description: `Got \`${terminal.init.kind}\` expression instead`,
details: [
{
kind: 'error',
loc: terminal.test.loc,
message: null,
},
],
loc: terminal.test.loc,
suggestions: null,
});
if (terminal.test.instructions.length !== 2) {
@@ -1189,13 +1110,7 @@ function codegenTerminal(
CompilerError.invariant(false, {
reason: `Expected a StoreLocal or Destructure to be assigned to the collection`,
description: `Found ${iterableItem.value.kind}`,
details: [
{
kind: 'error',
loc: iterableItem.value.loc,
message: null,
},
],
loc: iterableItem.value.loc,
suggestions: null,
});
}
@@ -1216,13 +1131,7 @@ function codegenTerminal(
CompilerError.invariant(false, {
reason: `Unexpected ${iterableItem.value.lvalue.kind} variable in for..of collection`,
description: null,
details: [
{
kind: 'error',
loc: iterableItem.loc,
message: null,
},
],
loc: iterableItem.loc,
suggestions: null,
});
default:
@@ -1231,8 +1140,7 @@ function codegenTerminal(
`Unhandled lvalue kind: ${iterableItem.value.lvalue.kind}`,
);
}
return createForOfStatement(
terminal.loc,
return t.forOfStatement(
/*
* Special handling here since we only want the VariableDeclarators without any inits
* This needs to be updated when we handle non-trivial ForOf inits
@@ -1254,7 +1162,7 @@ function codegenTerminal(
alternate = block;
}
}
return createIfStatement(terminal.loc, test, consequent, alternate);
return t.ifStatement(test, consequent, alternate);
}
case 'return': {
const value = codegenPlaceToExpression(cx, terminal.value);
@@ -1265,8 +1173,7 @@ function codegenTerminal(
return t.returnStatement(value);
}
case 'switch': {
return createSwitchStatement(
terminal.loc,
return t.switchStatement(
codegenPlaceToExpression(cx, terminal.test),
terminal.cases.map(case_ => {
const test =
@@ -1279,26 +1186,15 @@ function codegenTerminal(
);
}
case 'throw': {
return createThrowStatement(
terminal.loc,
codegenPlaceToExpression(cx, terminal.value),
);
return t.throwStatement(codegenPlaceToExpression(cx, terminal.value));
}
case 'do-while': {
const test = codegenInstructionValueToExpression(cx, terminal.test);
return createDoWhileStatement(
terminal.loc,
test,
codegenBlock(cx, terminal.loop),
);
return t.doWhileStatement(test, codegenBlock(cx, terminal.loop));
}
case 'while': {
const test = codegenInstructionValueToExpression(cx, terminal.test);
return createWhileStatement(
terminal.loc,
test,
codegenBlock(cx, terminal.loop),
);
return t.whileStatement(test, codegenBlock(cx, terminal.loop));
}
case 'label': {
return codegenBlock(cx, terminal.block);
@@ -1309,8 +1205,7 @@ function codegenTerminal(
catchParam = convertIdentifier(terminal.handlerBinding.identifier);
cx.temp.set(terminal.handlerBinding.identifier.declarationId, null);
}
return createTryStatement(
terminal.loc,
return t.tryStatement(
codegenBlock(cx, terminal.block),
t.catchClause(catchParam, codegenBlock(cx, terminal.handler)),
);
@@ -1377,13 +1272,7 @@ function codegenInstructionNullable(
reason:
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
loc: instr.loc,
suggestions: null,
});
} else if (hasReassign) {
@@ -1396,13 +1285,7 @@ function codegenInstructionNullable(
CompilerError.invariant(instr.lvalue === null, {
reason: `Const declaration cannot be referenced as an expression`,
description: null,
details: [
{
kind: 'error',
loc: instr.value.loc,
message: `this is ${kind}`,
},
],
loc: instr.value.loc,
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'const', [
@@ -1413,38 +1296,20 @@ function codegenInstructionNullable(
CompilerError.invariant(instr.lvalue === null, {
reason: `Function declaration cannot be referenced as an expression`,
description: null,
details: [
{
kind: 'error',
loc: instr.value.loc,
message: `this is ${kind}`,
},
],
loc: instr.value.loc,
suggestions: null,
});
const genLvalue = codegenLValue(cx, lvalue);
CompilerError.invariant(genLvalue.type === 'Identifier', {
reason: 'Expected an identifier as a function declaration lvalue',
description: null,
details: [
{
kind: 'error',
loc: instr.value.loc,
message: null,
},
],
loc: instr.value.loc,
suggestions: null,
});
CompilerError.invariant(value?.type === 'FunctionExpression', {
reason: 'Expected a function as a function declaration value',
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
details: [
{
kind: 'error',
loc: instr.value.loc,
message: null,
},
],
loc: instr.value.loc,
suggestions: null,
});
return createFunctionDeclaration(
@@ -1460,13 +1325,7 @@ function codegenInstructionNullable(
CompilerError.invariant(instr.lvalue === null, {
reason: `Const declaration cannot be referenced as an expression`,
description: null,
details: [
{
kind: 'error',
loc: instr.value.loc,
message: 'this is const',
},
],
loc: instr.value.loc,
suggestions: null,
});
return createVariableDeclaration(instr.loc, 'let', [
@@ -1477,13 +1336,7 @@ function codegenInstructionNullable(
CompilerError.invariant(value !== null, {
reason: 'Expected a value for reassignment',
description: null,
details: [
{
kind: 'error',
loc: instr.value.loc,
message: null,
},
],
loc: instr.value.loc,
suggestions: null,
});
const expr = t.assignmentExpression(
@@ -1516,13 +1369,7 @@ function codegenInstructionNullable(
CompilerError.invariant(false, {
reason: `Expected ${kind} to have been pruned in PruneHoistedContexts`,
description: null,
details: [
{
kind: 'error',
loc: instr.loc,
message: null,
},
],
loc: instr.loc,
suggestions: null,
});
}
@@ -1540,14 +1387,7 @@ function codegenInstructionNullable(
} else if (instr.value.kind === 'ObjectMethod') {
CompilerError.invariant(instr.lvalue, {
reason: 'Expected object methods to have a temp lvalue',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
cx.objectMethods.set(instr.lvalue.identifier.id, instr.value);
@@ -1594,13 +1434,7 @@ function codegenForInit(
(instr.kind === 'let' || instr.kind === 'const'),
{
reason: 'Expected a variable declaration',
details: [
{
kind: 'error',
loc: init.loc,
message: null,
},
],
loc: init.loc,
description: `Got ${instr.type}`,
suggestions: null,
},
@@ -1613,13 +1447,7 @@ function codegenForInit(
});
CompilerError.invariant(declarators.length > 0, {
reason: 'Expected a variable declaration',
details: [
{
kind: 'error',
loc: init.loc,
message: null,
},
],
loc: init.loc,
description: null,
suggestions: null,
});
@@ -1715,13 +1543,7 @@ const createExpressionStatement = withLoc(t.expressionStatement);
const _createLabelledStatement = withLoc(t.labeledStatement);
const createVariableDeclaration = withLoc(t.variableDeclaration);
const createFunctionDeclaration = withLoc(t.functionDeclaration);
const createWhileStatement = withLoc(t.whileStatement);
const createDoWhileStatement = withLoc(t.doWhileStatement);
const createSwitchStatement = withLoc(t.switchStatement);
const createIfStatement = withLoc(t.ifStatement);
const createForStatement = withLoc(t.forStatement);
const createForOfStatement = withLoc(t.forOfStatement);
const createForInStatement = withLoc(t.forInStatement);
const _createWhileStatement = withLoc(t.whileStatement);
const createTaggedTemplateExpression = withLoc(t.taggedTemplateExpression);
const createLogicalExpression = withLoc(t.logicalExpression);
const createSequenceExpression = withLoc(t.sequenceExpression);
@@ -1736,10 +1558,6 @@ const createJsxText = withLoc(t.jsxText);
const createJsxClosingElement = withLoc(t.jsxClosingElement);
const createJsxOpeningElement = withLoc(t.jsxOpeningElement);
const createStringLiteral = withLoc(t.stringLiteral);
const createThrowStatement = withLoc(t.throwStatement);
const createTryStatement = withLoc(t.tryStatement);
const createBreakStatement = withLoc(t.breakStatement);
const createContinueStatement = withLoc(t.continueStatement);
function createHookGuard(
guard: ExternalFunction,
@@ -1950,13 +1768,7 @@ function codegenInstructionValue(
CompilerError.invariant(t.isExpression(optionalValue.callee), {
reason: 'v8 intrinsics are validated during lowering',
description: null,
details: [
{
kind: 'error',
loc: optionalValue.callee.loc ?? null,
message: null,
},
],
loc: optionalValue.callee.loc ?? null,
suggestions: null,
});
value = t.optionalCallExpression(
@@ -1972,13 +1784,7 @@ function codegenInstructionValue(
CompilerError.invariant(t.isExpression(property), {
reason: 'Private names are validated during lowering',
description: null,
details: [
{
kind: 'error',
loc: property.loc ?? null,
message: null,
},
],
loc: property.loc ?? null,
suggestions: null,
});
value = t.optionalMemberExpression(
@@ -1994,13 +1800,7 @@ function codegenInstructionValue(
reason:
'Expected an optional value to resolve to a call expression or member expression',
description: `Got a \`${optionalValue.type}\``,
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
suggestions: null,
});
}
@@ -2016,15 +1816,10 @@ function codegenInstructionValue(
t.isOptionalMemberExpression(memberExpr),
{
reason:
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression',
'[Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. ' +
`Got a \`${memberExpr.type}\``,
description: null,
details: [
{
kind: 'error',
loc: memberExpr.loc ?? null,
message: `Got: '${memberExpr.type}'`,
},
],
loc: memberExpr.loc ?? null,
suggestions: null,
},
);
@@ -2038,13 +1833,7 @@ function codegenInstructionValue(
'[Codegen] Internal error: Forget should always generate MethodCall::property ' +
'as a MemberExpression of MethodCall::receiver',
description: null,
details: [
{
kind: 'error',
loc: memberExpr.loc ?? null,
message: null,
},
],
loc: memberExpr.loc ?? null,
suggestions: null,
},
);
@@ -2089,14 +1878,7 @@ function codegenInstructionValue(
const method = cx.objectMethods.get(property.place.identifier.id);
CompilerError.invariant(method, {
reason: 'Expected ObjectMethod instruction',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
const loweredFunc = method.loweredFunc;
@@ -2167,13 +1949,7 @@ function codegenInstructionValue(
CompilerError.invariant(tagValue.type === 'StringLiteral', {
reason: `Expected JSX tag to be an identifier or string, got \`${tagValue.type}\``,
description: null,
details: [
{
kind: 'error',
loc: tagValue.loc ?? null,
message: null,
},
],
loc: tagValue.loc ?? null,
suggestions: null,
});
if (tagValue.value.indexOf(':') >= 0) {
@@ -2193,13 +1969,7 @@ function codegenInstructionValue(
SINGLE_CHILD_FBT_TAGS.has(tagValue.value)
) {
CompilerError.invariant(instrValue.children != null, {
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
reason: 'Expected fbt element to have children',
suggestions: null,
description: null,
@@ -2328,7 +2098,6 @@ function codegenInstructionValue(
),
reactiveFunction,
).unwrap();
if (instrValue.type === 'ArrowFunctionExpression') {
let body: t.BlockStatement | t.Expression = fn.body;
if (body.body.length === 1 && loweredFunc.directives.length == 0) {
@@ -2340,26 +2109,14 @@ function codegenInstructionValue(
value = t.arrowFunctionExpression(fn.params, body, fn.async);
} else {
value = t.functionExpression(
instrValue.name != null ? t.identifier(instrValue.name) : null,
fn.id ??
(instrValue.name != null ? t.identifier(instrValue.name) : null),
fn.params,
fn.body,
fn.generator,
fn.async,
);
}
if (
cx.env.config.enableNameAnonymousFunctions &&
instrValue.name == null &&
instrValue.nameHint != null
) {
const name = instrValue.nameHint;
value = t.memberExpression(
t.objectExpression([t.objectProperty(t.stringLiteral(name), value)]),
t.stringLiteral(name),
true,
false,
);
}
break;
}
case 'TaggedTemplateExpression': {
@@ -2427,7 +2184,7 @@ function codegenInstructionValue(
reason: `(CodegenReactiveFunction::codegenInstructionValue) Cannot declare variables in a value block, tried to declare '${
(declarator.id as t.Identifier).name
}'`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
@@ -2435,7 +2192,7 @@ function codegenInstructionValue(
} else {
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
category: ErrorCategory.Todo,
severity: ErrorSeverity.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});
@@ -2514,13 +2271,7 @@ function codegenInstructionValue(
{
reason: `Unexpected StoreLocal in codegenInstructionValue`,
description: null,
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
suggestions: null,
},
);
@@ -2550,13 +2301,7 @@ function codegenInstructionValue(
CompilerError.invariant(false, {
reason: `Unexpected ${instrValue.kind} in codegenInstructionValue`,
description: null,
details: [
{
kind: 'error',
loc: instrValue.loc,
message: null,
},
],
loc: instrValue.loc,
suggestions: null,
});
}
@@ -2567,9 +2312,6 @@ function codegenInstructionValue(
);
}
}
if (instrValue.loc != null && instrValue.loc != GeneratedSource) {
value.loc = instrValue.loc;
}
return value;
}
@@ -2705,13 +2447,7 @@ function convertMemberExpressionToJsx(
CompilerError.invariant(expr.property.type === 'Identifier', {
reason: 'Expected JSX member expression property to be a string',
description: null,
details: [
{
kind: 'error',
loc: expr.loc ?? null,
message: null,
},
],
loc: expr.loc ?? null,
suggestions: null,
});
const property = t.jsxIdentifier(expr.property.name);
@@ -2722,13 +2458,7 @@ function convertMemberExpressionToJsx(
reason:
'Expected JSX member expression to be an identifier or nested member expression',
description: null,
details: [
{
kind: 'error',
loc: expr.object.loc ?? null,
message: null,
},
],
loc: expr.object.loc ?? null,
suggestions: null,
});
const object = convertMemberExpressionToJsx(expr.object);
@@ -2752,13 +2482,7 @@ function codegenObjectPropertyKey(
CompilerError.invariant(t.isExpression(expr), {
reason: 'Expected object property key to be an expression',
description: null,
details: [
{
kind: 'error',
loc: key.name.loc,
message: null,
},
],
loc: key.name.loc,
suggestions: null,
});
return expr;
@@ -2905,13 +2629,7 @@ function codegenPlace(cx: Context, place: Place): t.Expression | t.JSXText {
description: `Value for '${printPlace(
place,
)}' was not set in the codegen context`,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
loc: place.loc,
suggestions: null,
});
const identifier = convertIdentifier(place.identifier);
@@ -2924,13 +2642,7 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
identifier.name !== null && identifier.name.kind === 'named',
{
reason: `Expected temporaries to be promoted to named identifiers in an earlier pass`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
description: `identifier ${identifier.id} is unnamed`,
suggestions: null,
},
@@ -2946,14 +2658,7 @@ function compareScopeDependency(
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
{
reason: '[Codegen] Expected named identifier for dependency',
description: null,
details: [
{
kind: 'error',
loc: a.identifier.loc,
message: null,
},
],
loc: a.identifier.loc,
},
);
const aName = [
@@ -2977,14 +2682,7 @@ function compareScopeDeclaration(
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
{
reason: '[Codegen] Expected named identifier for declaration',
description: null,
details: [
{
kind: 'error',
loc: a.identifier.loc,
message: null,
},
],
loc: a.identifier.loc,
},
);
const aName = a.identifier.name.value;

View File

@@ -75,13 +75,7 @@ export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
CompilerError.invariant(terminal.kind === 'scope', {
reason: `Expected block to have a scope terminal`,
description: `Expected block bb${block.id} to end in a scope terminal`,
details: [
{
kind: 'error',
loc: terminal.loc,
message: null,
},
],
loc: terminal.loc,
});
const body = fn.body.blocks.get(terminal.block)!;
if (

View File

@@ -162,13 +162,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
});
CompilerError.invariant(false, {
reason: `Invalid mutable range for scope`,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
description: `Scope @${scope.id} has range [${scope.range.start}:${
scope.range.end
}] but the valid range is [1:${maxInstruction + 1}]`,

View File

@@ -119,7 +119,6 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
lastUsage: Map<DeclarationId, InstructionId>;
temporaries: Map<DeclarationId, DeclarationId> = new Map();
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
super();
@@ -159,17 +158,11 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
const merged: Array<MergedScope> = [];
function reset(): void {
CompilerError.invariant(current !== null, {
loc: null,
reason:
'MergeConsecutiveScopes: expected current scope to be non-null if reset()',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
suggestions: null,
description: null,
});
if (current.to > current.from + 1) {
merged.push(current);
@@ -222,12 +215,6 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
current.lvalues.add(
instr.instruction.lvalue.identifier.declarationId,
);
if (instr.instruction.value.kind === 'LoadLocal') {
this.temporaries.set(
instr.instruction.lvalue.identifier.declarationId,
instr.instruction.value.place.identifier.declarationId,
);
}
}
break;
}
@@ -249,13 +236,6 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
)) {
current.lvalues.add(lvalue.identifier.declarationId);
}
this.temporaries.set(
instr.instruction.value.lvalue.place.identifier
.declarationId,
this.temporaries.get(
instr.instruction.value.value.identifier.declarationId,
) ?? instr.instruction.value.value.identifier.declarationId,
);
} else {
log(
`Reset scope @${current.block.scope.id} from StoreLocal in [${instr.instruction.id}]`,
@@ -280,7 +260,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
case 'scope': {
if (
current !== null &&
canMergeScopes(current.block, instr, this.temporaries) &&
canMergeScopes(current.block, instr) &&
areLValuesLastUsedByScope(
instr.scope,
current.lvalues,
@@ -381,16 +361,10 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
}
const mergedScope = block[entry.from]!;
CompilerError.invariant(mergedScope.kind === 'scope', {
loc: null,
reason:
'MergeConsecutiveScopes: Expected scope starting index to be a scope',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
suggestions: null,
});
nextInstructions.push(mergedScope);
@@ -452,7 +426,6 @@ function areLValuesLastUsedByScope(
function canMergeScopes(
current: ReactiveScopeBlock,
next: ReactiveScopeBlock,
temporaries: Map<DeclarationId, DeclarationId>,
): boolean {
// Don't merge scopes with reassignments
if (
@@ -492,14 +465,11 @@ function canMergeScopes(
(next.scope.dependencies.size !== 0 &&
[...next.scope.dependencies].every(
dep =>
dep.path.length === 0 &&
isAlwaysInvalidatingType(dep.identifier.type) &&
Iterable_some(
current.scope.declarations.values(),
decl =>
decl.identifier.declarationId === dep.identifier.declarationId ||
decl.identifier.declarationId ===
temporaries.get(dep.identifier.declarationId),
decl.identifier.declarationId === dep.identifier.declarationId,
),
))
) {
@@ -507,16 +477,12 @@ function canMergeScopes(
return true;
}
log(` cannot merge scopes:`);
log(
` ${printReactiveScopeSummary(current.scope)} ${[...current.scope.declarations.values()].map(decl => decl.identifier.declarationId)}`,
);
log(
` ${printReactiveScopeSummary(next.scope)} ${[...next.scope.dependencies].map(dep => `${dep.identifier.declarationId} ${temporaries.get(dep.identifier.declarationId) ?? dep.identifier.declarationId}`)}`,
);
log(` ${printReactiveScopeSummary(current.scope)}`);
log(` ${printReactiveScopeSummary(next.scope)}`);
return false;
}
export function isAlwaysInvalidatingType(type: Type): boolean {
function isAlwaysInvalidatingType(type: Type): boolean {
switch (type.kind) {
case 'Object': {
switch (type.shapeId) {

View File

@@ -323,13 +323,7 @@ function writeTerminal(writer: Writer, terminal: ReactiveTerminal): void {
CompilerError.invariant(block != null, {
reason: 'Expected case to have a block',
description: null,
details: [
{
kind: 'error',
loc: case_.test?.loc ?? null,
message: null,
},
],
loc: case_.test?.loc ?? null,
suggestions: null,
});
writeReactiveInstructions(writer, block);

View File

@@ -290,14 +290,7 @@ class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
CompilerError.invariant(lval.identifier.name != null, {
reason:
'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
description: null,
details: [
{
kind: 'error',
loc: instruction.loc,
message: null,
},
],
loc: instruction.loc,
});
}
@@ -461,13 +454,7 @@ function promoteIdentifier(identifier: Identifier, state: State): void {
reason:
'promoteTemporary: Expected to be called only for temporary variables',
description: null,
details: [
{
kind: 'error',
loc: GeneratedSource,
message: null,
},
],
loc: GeneratedSource,
suggestions: null,
});
if (state.tags.has(identifier.declarationId)) {

View File

@@ -145,14 +145,7 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
if (maybeHoistedFn != null) {
CompilerError.invariant(maybeHoistedFn.kind === 'func', {
reason: '[PruneHoistedContexts] Unexpected hoisted function',
description: null,
details: [
{
kind: 'error',
loc: instruction.loc,
message: null,
},
],
loc: instruction.loc,
});
maybeHoistedFn.definition = instruction.value.lvalue.place;
/**

View File

@@ -196,14 +196,7 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
): void {
CompilerError.invariant(state !== 'Create', {
reason: "Visiting a terminal statement with state 'Create'",
description: null,
details: [
{
kind: 'error',
loc: stmt.terminal.loc,
message: null,
},
],
loc: stmt.terminal.loc,
});
super.visitTerminal(stmt, state);
}

View File

@@ -24,6 +24,7 @@ import {
getHookKind,
isMutableEffect,
} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {assertExhaustive, getOrInsertDefault} from '../Utils/utils';
import {getPlaceScope, ReactiveScope} from '../HIR/HIR';
import {
@@ -34,7 +35,6 @@ import {
visitReactiveFunction,
} from './visitors';
import {printPlace} from '../HIR/PrintHIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
/*
* This pass prunes reactive scopes that are not necessary to bound downstream computation.
@@ -264,13 +264,7 @@ class State {
CompilerError.invariant(identifierNode !== undefined, {
reason: 'Expected identifier to be initialized',
description: `[${id}] operand=${printPlace(place)} for identifier declaration ${identifier}`,
details: [
{
kind: 'error',
loc: place.loc,
message: null,
},
],
loc: place.loc,
suggestions: null,
});
identifierNode.scopes.add(scope.id);
@@ -292,13 +286,7 @@ function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
CompilerError.invariant(node !== undefined, {
reason: `Expected a node for all identifiers, none found for \`${id}\``,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (node.seen) {
@@ -340,13 +328,7 @@ function computeMemoizedIdentifiers(state: State): Set<DeclarationId> {
CompilerError.invariant(node !== undefined, {
reason: 'Expected a node for all scopes',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
if (node.seen) {
@@ -429,9 +411,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
this.state = state;
this.options = {
memoizeJsxElements: !this.env.config.enableForest,
forceMemoizePrimitives:
this.env.config.enableForest ||
this.env.config.enablePreserveExistingMemoizationGuarantees,
forceMemoizePrimitives: this.env.config.enableForest,
};
}
@@ -554,23 +534,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
case 'JSXText':
case 'BinaryExpression':
case 'UnaryExpression': {
if (options.forceMemoizePrimitives) {
/**
* Because these instructions produce primitives we usually don't consider
* them as escape points: they are known to copy, not return references.
* However if we're forcing memoization of primitives then we mark these
* instructions as needing memoization and walk their rvalues to ensure
* any scopes transitively reachable from the rvalues are considered for
* memoization. Note: we may still prune primitive-producing scopes if
* they don't ultimately escape at all.
*/
const level = MemoizationLevel.Conditional;
return {
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
rvalues: [...eachReactiveValueOperand(value)],
};
}
const level = MemoizationLevel.Never;
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Never;
return {
// All of these instructions return a primitive value and never need to be memoized
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
@@ -719,7 +685,9 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
}
case 'ComputedLoad':
case 'PropertyLoad': {
const level = MemoizationLevel.Conditional;
const level = options.forceMemoizePrimitives
? MemoizationLevel.Memoized
: MemoizationLevel.Conditional;
return {
// Indirection for the inner value, memoized if the value is
lvalues: lvalue !== null ? [{place: lvalue, level}] : [],
@@ -995,13 +963,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
CompilerError.invariant(identifierNode !== undefined, {
reason: 'Expected identifier to be initialized',
description: null,
details: [
{
kind: 'error',
loc: stmt.terminal.loc,
message: null,
},
],
loc: stmt.terminal.loc,
suggestions: null,
});
for (const scope of scopes) {
@@ -1026,13 +988,7 @@ class CollectDependenciesVisitor extends ReactiveFunctionVisitor<
CompilerError.invariant(identifierNode !== undefined, {
reason: 'Expected identifier to be initialized',
description: null,
details: [
{
kind: 'error',
loc: reassignment.loc,
message: null,
},
],
loc: reassignment.loc,
suggestions: null,
});
for (const scope of scopes) {

View File

@@ -186,13 +186,7 @@ class Scopes {
CompilerError.invariant(last === next, {
reason: 'Mismatch push/pop calls',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
}

View File

@@ -97,13 +97,7 @@ export function eliminateRedundantPhi(
CompilerError.invariant(same !== null, {
reason: 'Expected phis to be non-empty',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
rewrites.set(phi.place.identifier, same);
@@ -155,26 +149,12 @@ export function eliminateRedundantPhi(
for (const phi of block.phis) {
CompilerError.invariant(!rewrites.has(phi.place.identifier), {
reason: '[EliminateRedundantPhis]: rewrite not complete',
description: null,
details: [
{
kind: 'error',
loc: phi.place.loc,
message: null,
},
],
loc: phi.place.loc,
});
for (const [, operand] of phi.operands) {
CompilerError.invariant(!rewrites.has(operand.identifier), {
reason: '[EliminateRedundantPhis]: rewrite not complete',
description: null,
details: [
{
kind: 'error',
loc: phi.place.loc,
message: null,
},
],
loc: phi.place.loc,
});
}
}

View File

@@ -70,13 +70,7 @@ class SSABuilder {
CompilerError.invariant(this.#current !== null, {
reason: 'we need to be in a block to access state!',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
return this.#states.get(this.#current)!;
@@ -259,13 +253,7 @@ function enterSSAImpl(
CompilerError.invariant(!visitedBlocks.has(block), {
reason: `found a cycle! visiting bb${block.id} again`,
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
@@ -278,13 +266,7 @@ function enterSSAImpl(
CompilerError.invariant(func.context.length === 0, {
reason: `Expected function context to be empty for outer function declarations`,
description: null,
details: [
{
kind: 'error',
loc: func.loc,
message: null,
},
],
loc: func.loc,
suggestions: null,
});
func.params = func.params.map(param => {
@@ -313,13 +295,7 @@ function enterSSAImpl(
reason:
'Expected function expression entry block to have zero predecessors',
description: null,
details: [
{
kind: 'error',
loc: null,
message: null,
},
],
loc: null,
suggestions: null,
});
entry.preds.add(blockId);

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