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
400 changed files with 8505 additions and 17409 deletions

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,6 +546,7 @@ module.exports = {
},
globals: {
$Call: 'readonly',
$ElementType: 'readonly',
$Flow$ModuleRef: 'readonly',
$FlowFixMe: 'readonly',

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

@@ -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,10 +7,9 @@
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 {
/**
@@ -48,9 +47,8 @@ export enum ErrorSeverity {
}
export type CompilerDiagnosticOptions = {
category: ErrorCategory;
severity: ErrorSeverity;
reason: string;
category: string;
description: string;
details: Array<CompilerDiagnosticDetail>;
suggestions?: Array<CompilerSuggestion> | null | undefined;
@@ -60,15 +58,11 @@ export type CompilerDiagnosticDetail =
/**
* A/the source of the error
*/
| {
kind: 'error';
loc: SourceLocation | null;
message: string;
}
| {
kind: 'hint';
message: string;
};
{
kind: 'error';
loc: SourceLocation | null;
message: string;
};
export enum CompilerSuggestionOperation {
InsertBefore,
@@ -93,10 +87,9 @@ export type CompilerSuggestion =
};
export type CompilerErrorDetailOptions = {
category: ErrorCategory;
severity: ErrorSeverity;
reason: string;
description?: string | null | undefined;
severity: ErrorSeverity;
loc: SourceLocation | null;
suggestions?: Array<CompilerSuggestion> | null | undefined;
};
@@ -122,8 +115,8 @@ 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;
@@ -134,9 +127,6 @@ export class CompilerDiagnostic {
get suggestions(): CompilerDiagnosticOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
}
withDetail(detail: CompilerDiagnosticDetail): CompilerDiagnostic {
this.options.details.push(detail);
@@ -144,17 +134,12 @@ export class CompilerDiagnostic {
}
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.severity, this.reason),
printErrorSummary(this.severity, this.category),
'\n\n',
this.description,
];
@@ -182,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}`,
);
}
@@ -199,7 +179,7 @@ export class CompilerDiagnostic {
}
toString(): string {
const buffer = [printErrorSummary(this.severity, this.reason)];
const buffer = [printErrorSummary(this.severity, this.category)];
if (this.description != null) {
buffer.push(`. ${this.description}.`);
}
@@ -237,9 +217,6 @@ export class CompilerErrorDetail {
get suggestions(): CompilerErrorDetailOptions['suggestions'] {
return this.options.suggestions;
}
get category(): ErrorCategory {
return this.options.category;
}
primaryLocation(): SourceLocation | null {
return this.loc;
@@ -289,14 +266,13 @@ export class CompilerError extends Error {
static invariant(
condition: unknown,
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): asserts condition {
if (!condition) {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
category: ErrorCategory.Invariant,
severity: ErrorSeverity.Invariant,
}),
);
@@ -311,28 +287,23 @@ export class CompilerError extends Error {
}
static throwTodo(
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
}),
new CompilerErrorDetail({...options, severity: ErrorSeverity.Todo}),
);
throw errors;
}
static throwInvalidJS(
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
}),
);
throw errors;
@@ -352,14 +323,13 @@ export class CompilerError extends Error {
}
static throwInvalidConfig(
options: Omit<CompilerErrorDetailOptions, 'severity' | 'category'>,
options: Omit<CompilerErrorDetailOptions, 'severity'>,
): never {
const errors = new CompilerError();
errors.pushErrorDetail(
new CompilerErrorDetail({
...options,
severity: ErrorSeverity.InvalidConfig,
category: ErrorCategory.Config,
}),
);
throw errors;
@@ -423,7 +393,6 @@ export class CompilerError extends Error {
push(options: CompilerErrorDetailOptions): CompilerErrorDetail {
const detail = new CompilerErrorDetail({
category: options.category,
reason: options.reason,
description: options.description ?? null,
severity: options.severity,
@@ -524,345 +493,3 @@ function printErrorSummary(severity: ErrorSeverity, message: string): string {
}
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',
// Checks that manual memoization is preserved
PreserveManualMemo = 'PreserveManualMemo',
// 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;
/**
* 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,
name: 'automatic-effect-dependencies',
description:
'Verifies that automatic effect dependencies are compiled if opted-in',
recommended: false,
};
}
case ErrorCategory.CapitalizedCalls: {
return {
category,
name: 'capitalized-calls',
description:
'Validates against calling capitalized functions/methods instead of using JSX',
recommended: false,
};
}
case ErrorCategory.Config: {
return {
category,
name: 'config',
description: 'Validates the compiler configuration options',
recommended: true,
};
}
case ErrorCategory.EffectDependencies: {
return {
category,
name: 'memoized-effect-dependencies',
description: 'Validates that effect dependencies are memoized',
recommended: false,
};
}
case ErrorCategory.EffectDerivationsOfState: {
return {
category,
name: 'no-deriving-state-in-effects',
description:
'Validates against deriving values from state in an effect',
recommended: false,
};
}
case ErrorCategory.EffectSetState: {
return {
category,
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,
name: 'error-boundaries',
description:
'Validates usage of error boundaries instead of try/catch for errors in child components',
recommended: true,
};
}
case ErrorCategory.FBT: {
return {
category,
name: 'fbt',
description: 'Validates usage of fbt',
recommended: false,
};
}
case ErrorCategory.Fire: {
return {
category,
name: 'fire',
description: 'Validates usage of `fire`',
recommended: false,
};
}
case ErrorCategory.Gating: {
return {
category,
name: 'gating',
description:
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
recommended: true,
};
}
case ErrorCategory.Globals: {
return {
category,
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,
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,
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,
name: 'invariant',
description: 'Internal invariants',
recommended: false,
};
}
case ErrorCategory.PreserveManualMemo: {
return {
category,
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,
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,
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,
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,
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,
name: 'rule-suppression',
description: 'Validates against suppression of other rules',
recommended: false,
};
}
case ErrorCategory.Syntax: {
return {
category,
name: 'syntax',
description: 'Validates against invalid syntax',
recommended: false,
};
}
case ErrorCategory.Todo: {
return {
category,
name: 'todo',
description: 'Unimplemented features',
recommended: false,
};
}
case ErrorCategory.UnsupportedSyntax: {
return {
category,
name: 'unsupported-syntax',
description:
'Validates against syntax that we do not plan to support in React Compiler',
recommended: true,
};
}
case ErrorCategory.UseMemo: {
return {
category,
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,
};
}
default: {
assertExhaustive(category, `Unsupported category ${category}`);
}
}
}
export const LintRules: Array<LintRule> = Object.keys(ErrorCategory).map(
category => getRuleForCategory(category as any),
);

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, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {
EnvironmentConfig,
GeneratedSource,
@@ -38,7 +38,6 @@ 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}`,
@@ -206,7 +205,6 @@ 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}`,

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,11 +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';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -226,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
@@ -246,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) {
@@ -270,10 +292,6 @@ function runWithEnvironment(
validateNoSetStateInRender(hir).unwrap();
}
if (env.config.validateNoDerivedComputationsInEffects) {
validateNoDerivedComputationsInEffects(hir);
}
if (env.config.validateNoSetStateInEffects) {
env.logErrors(validateNoSetStateInEffects(hir));
}
@@ -286,7 +304,12 @@ function runWithEnvironment(
validateNoImpureFunctionsInRender(hir).unwrap();
}
validateNoFreezingKnownMutableFunctions(hir).unwrap();
if (
env.config.validateNoFreezingKnownMutableFunctions ||
env.config.enableNewMutationAliasingModel
) {
validateNoFreezingKnownMutableFunctions(hir).unwrap();
}
}
inferReactivePlaces(hir);
@@ -299,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));

View File

@@ -10,7 +10,6 @@ import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {ExternalFunction, ReactFunctionType} from '../HIR/Environment';
@@ -106,7 +105,6 @@ function findDirectivesDynamicGating(
reason: `Dynamic gating directive is not a valid JavaScript identifier`,
description: `Found '${directive.value.value}'`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Gating,
loc: directive.loc ?? null,
suggestions: null,
});
@@ -123,7 +121,6 @@ function findDirectivesDynamicGating(
.map(r => r.directive.value.value)
.join(', ')}]`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Gating,
loc: result[0].directive.loc ?? null,
suggestions: null,
});
@@ -459,7 +456,6 @@ export function compileProgram(
reason:
'Unexpected compiled functions when module scope opt-out is present',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: null,
}),
);
@@ -815,7 +811,6 @@ function shouldSkipCompilation(
description:
"When the 'sources' config options is specified, the React compiler will only compile files with a name",
severity: ErrorSeverity.InvalidConfig,
category: ErrorCategory.Config,
loc: null,
}),
);

View File

@@ -11,7 +11,6 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {assertExhaustive} from '../Utils/utils';
@@ -184,10 +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()}\``,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Suppression,
suggestions: [
{
description: suggestion,

View File

@@ -13,11 +13,7 @@ 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: Omit<CompilerDiagnosticOptions, 'severity'>,
@@ -96,8 +92,7 @@ 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.' +
@@ -128,8 +123,8 @@ 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

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,131 +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',
loc,
});
} 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

@@ -12,7 +12,6 @@ import {
CompilerDiagnostic,
CompilerError,
CompilerSuggestionOperation,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {Err, Ok, Result} from '../Utils/Result';
@@ -109,8 +108,7 @@ export function lower(
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason: 'Could not find binding',
category: 'Could not find binding',
description: `[BuildHIR] Could not find binding for param \`${param.node.name}\`.`,
}).withDetail({
kind: 'error',
@@ -174,8 +172,7 @@ export function lower(
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: `Handle ${param.node.type} parameters`,
category: `Handle ${param.node.type} parameters`,
description: `[BuildHIR] Add support for ${param.node.type} parameters.`,
}).withDetail({
kind: 'error',
@@ -206,8 +203,7 @@ export function lower(
builder.errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
reason: `Unexpected function body kind`,
category: `Unexpected function body kind`,
description: `Expected function body to be an expression or a block statement, got \`${body.type}\`.`,
}).withDetail({
kind: 'error',
@@ -277,7 +273,6 @@ function lowerStatement(
reason:
'(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -465,7 +460,6 @@ function lowerStatement(
} else if (!binding.path.isVariableDeclarator()) {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: 'Unsupported declaration type for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
suggestions: null,
@@ -475,7 +469,6 @@ function lowerStatement(
} else {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason: 'Handle non-const declarations for hoisting',
description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
suggestions: null,
@@ -556,7 +549,6 @@ function lowerStatement(
reason:
'(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -629,7 +621,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -781,7 +772,6 @@ function lowerStatement(
builder.errors.push({
reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: case_.node.loc ?? null,
suggestions: null,
});
@@ -854,7 +844,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -883,7 +872,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: id.node.loc ?? null,
suggestions: null,
});
@@ -901,7 +889,6 @@ function lowerStatement(
builder.errors.push({
reason: `Expect \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: id.node.loc ?? null,
suggestions: [
{
@@ -949,7 +936,6 @@ function lowerStatement(
reason: `Expected variable declaration to be an identifier if no initializer was provided`,
description: `Got a \`${id.type}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1058,7 +1044,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1291,7 +1276,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1301,7 +1285,6 @@ function lowerStatement(
builder.errors.push({
reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.node.loc ?? null,
suggestions: null,
});
@@ -1395,7 +1378,6 @@ function lowerStatement(
reason: `JavaScript 'with' syntax is not supported`,
description: `'with' syntax is considered deprecated and removed from JavaScript standards, consider alternatives`,
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1416,7 +1398,6 @@ function lowerStatement(
reason: 'Inline `class` declarations are not supported',
description: `Move class declarations outside of components/hooks`,
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1446,7 +1427,6 @@ function lowerStatement(
reason:
'JavaScript `import` and `export` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1462,7 +1442,6 @@ function lowerStatement(
reason:
'TypeScript `namespace` statements may only appear at the top level of a module',
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: stmtPath.node.loc ?? null,
suggestions: null,
});
@@ -1541,7 +1520,6 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1567,7 +1545,6 @@ function lowerObjectPropertyKey(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: key.node.loc ?? null,
suggestions: null,
});
@@ -1625,7 +1602,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valuePath.node.loc ?? null,
suggestions: null,
});
@@ -1652,7 +1628,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1674,7 +1649,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyPath.node.loc ?? null,
suggestions: null,
});
@@ -1708,7 +1682,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -1729,7 +1702,6 @@ function lowerExpression(
reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
description: `Got a \`${calleePath.node.type}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1756,7 +1728,6 @@ function lowerExpression(
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`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: calleePath.node.loc ?? null,
suggestions: null,
});
@@ -1791,7 +1762,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1804,7 +1774,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
@@ -1834,7 +1803,6 @@ function lowerExpression(
builder.errors.push({
reason: `Expected sequence expression to have at least one expression`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2047,7 +2015,6 @@ function lowerExpression(
reason: `(BuildHIR::lowerExpression) Unsupported syntax on the left side of an AssignmentExpression`,
description: `Expected an LVal, got: ${left.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: left.node.loc ?? null,
suggestions: null,
});
@@ -2076,7 +2043,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2176,7 +2142,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -2216,7 +2181,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: attribute.node.loc ?? null,
suggestions: null,
});
@@ -2230,7 +2194,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${propName}\``,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: namePath.node.loc ?? null,
suggestions: null,
});
@@ -2261,7 +2224,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valueExpr.node?.loc ?? null,
suggestions: null,
});
@@ -2272,7 +2234,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: valueExpr.node.loc ?? null,
suggestions: null,
});
@@ -2330,8 +2291,7 @@ function lowerExpression(
if (locations.length > 1) {
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: ErrorCategory.FBT,
reason: 'Support duplicate fbt tags',
category: 'Support duplicate fbt tags',
description: `Support \`<${tagName}>\` tags with multiple \`<${tagName}:${name}>\` values`,
details: locations.map(loc => {
return {
@@ -2392,7 +2352,6 @@ function lowerExpression(
reason:
'(BuildHIR::lowerExpression) Handle tagged template with interpolations',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2411,7 +2370,6 @@ function lowerExpression(
reason:
'(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2434,7 +2392,6 @@ function lowerExpression(
builder.errors.push({
reason: `Unexpected quasi and subexpression lengths in template literal`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2445,7 +2402,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2488,7 +2444,6 @@ function lowerExpression(
builder.errors.push({
reason: `Only object properties can be deleted`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2504,7 +2459,6 @@ function lowerExpression(
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: expr.node.loc ?? null,
suggestions: [
{
@@ -2626,7 +2580,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2635,7 +2588,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2656,7 +2608,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: exprLoc,
suggestions: null,
});
@@ -2666,7 +2617,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprLoc,
suggestions: null,
});
@@ -2722,7 +2672,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -2732,7 +2681,6 @@ function lowerExpression(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3030,7 +2978,6 @@ function lowerReorderableExpression(
builder.errors.push({
reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: expr.node.loc ?? null,
suggestions: null,
});
@@ -3227,7 +3174,6 @@ function lowerArguments(
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: argPath.node.loc ?? null,
suggestions: null,
});
@@ -3263,7 +3209,6 @@ function lowerMemberExpression(
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3285,7 +3230,6 @@ function lowerMemberExpression(
builder.errors.push({
reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: propertyNode.node.loc ?? null,
suggestions: null,
});
@@ -3345,7 +3289,6 @@ function lowerJsxElementName(
reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
description: `Got \`${namespace}\` : \`${name}\``,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3360,7 +3303,6 @@ function lowerJsxElementName(
builder.errors.push({
reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3459,7 +3401,6 @@ function lowerJsxElement(
builder.errors.push({
reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3647,7 +3588,6 @@ function lowerIdentifier(
description:
'Eval is an anti-pattern in JavaScript, and the code executed cannot be evaluated by React Compiler',
severity: ErrorSeverity.UnsupportedJS,
category: ErrorCategory.UnsupportedSyntax,
loc: exprPath.node.loc ?? null,
suggestions: null,
});
@@ -3704,7 +3644,6 @@ function lowerIdentifierForAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
loc: path.node.loc ?? null,
suggestions: null,
});
@@ -3717,7 +3656,6 @@ function lowerIdentifierForAssignment(
builder.errors.push({
reason: `Cannot reassign a \`const\` variable`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: path.node.loc ?? null,
description:
binding.identifier.name != null
@@ -3775,7 +3713,6 @@ function lowerAssignment(
builder.errors.push({
reason: `Expected \`const\` declaration not to be reassigned`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3790,7 +3727,6 @@ function lowerAssignment(
builder.errors.push({
reason: `Unexpected context variable kind`,
severity: ErrorSeverity.InvalidJS,
category: ErrorCategory.Syntax,
loc: lvalue.node.loc ?? null,
suggestions: null,
});
@@ -3862,7 +3798,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3875,7 +3810,6 @@ function lowerAssignment(
reason:
'(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property',
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -3941,7 +3875,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -3981,7 +3914,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4055,7 +3987,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: argument.node.loc ?? null,
suggestions: null,
});
@@ -4087,7 +4018,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: property.node.loc ?? GeneratedSource,
@@ -4105,7 +4035,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4115,7 +4044,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: property.node.loc ?? null,
suggestions: null,
});
@@ -4130,7 +4058,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: element.node.loc ?? null,
suggestions: null,
});
@@ -4153,7 +4080,6 @@ function lowerAssignment(
} else if (identifier.kind === 'Global') {
builder.errors.push({
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
reason:
'Expected reassignment of globals to enable forceTemporaries',
loc: element.node.loc ?? GeneratedSource,
@@ -4303,7 +4229,6 @@ function lowerAssignment(
builder.errors.push({
reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: lvaluePath.node.loc ?? null,
suggestions: null,
});

View File

@@ -49,7 +49,6 @@ import {
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
import {FlowTypeEnv} from '../Flood/Types';
export const ReactElementSymbolSchema = z.object({
elementSymbol: z.union([
@@ -245,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
@@ -325,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.
@@ -699,8 +691,6 @@ export class Environment {
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
#flowTypeEnvironment: FlowTypeEnv | null;
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
@@ -769,26 +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',
loc: null,
});
this.#flowTypeEnvironment.init(this, code);
} else {
this.#flowTypeEnvironment = null;
}
}
get typeContext(): FlowTypeEnv {
CompilerError.invariant(this.#flowTypeEnvironment != null, {
reason: 'Flow type environment not initialized',
loc: null,
});
return this.#flowTypeEnvironment;
}
get isInferredMemoEnabled(): boolean {

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',
},
],
},
}),
],
]),
],
[

View File

@@ -7,14 +7,13 @@
import {BindingKind} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError} 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';
/*
* *******************************************************************************************
@@ -282,13 +281,30 @@ export type HIRFunction = {
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
@@ -1304,21 +1320,12 @@ export function forkTemporaryIdentifier(
* original source code.
*/
export function makeIdentifierName(name: string): ValidatedIdentifier {
if (isReservedWord(name)) {
CompilerError.throwInvalidJS({
reason: 'Expected a non-reserved identifier name',
loc: GeneratedSource,
description: `\`${name}\` is a reserved word in JavaScript and cannot be used as an identifier name`,
suggestions: null,
});
} else {
CompilerError.invariant(t.isValidIdentifier(name), {
reason: `Expected a valid identifier name`,
loc: GeneratedSource,
description: `\`${name}\` is not a valid JavaScript identifier`,
suggestions: null,
});
}
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,

View File

@@ -7,7 +7,7 @@
import {Binding, NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {Environment} from './Environment';
import {
BasicBlock,
@@ -310,8 +310,7 @@ export default class HIRBuilder {
if (node.name === 'fbt') {
CompilerError.throwDiagnostic({
severity: ErrorSeverity.Todo,
category: ErrorCategory.FBT,
reason: 'Support local variables named `fbt`',
category: 'Support local variables named `fbt`',
description:
'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
details: [

View File

@@ -142,7 +142,6 @@ function parseAliasingSignatureConfig(
const effects = typeConfig.effects.map(
(effect: AliasingEffectConfig): AliasingEffect => {
switch (effect.kind) {
case 'ImmutableCapture':
case 'CreateFrom':
case 'Capture':
case 'Alias':

View File

@@ -554,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': {
@@ -986,13 +998,13 @@ export function printAliasingEffect(effect: AliasingEffect): string {
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

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

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

@@ -11,7 +11,6 @@ import {
ErrorSeverity,
SourceLocation,
} from '..';
import {ErrorCategory} from '../CompilerError';
import {
CallExpression,
Effect,
@@ -301,9 +300,8 @@ function extractManualMemoizationArgs(
if (fnPlace == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected a callback function to be passed to ${kind}`,
category: `Expected a callback function to be passed to ${kind}`,
description: `Expected a callback function to be passed to ${kind}`,
suggestions: null,
}).withDetail({
@@ -317,9 +315,8 @@ function extractManualMemoizationArgs(
if (fnPlace.kind === 'Spread' || depsListPlace?.kind === 'Spread') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Unexpected spread argument to ${kind}`,
category: `Unexpected spread argument to ${kind}`,
description: `Unexpected spread argument to ${kind}`,
suggestions: null,
}).withDetail({
@@ -338,9 +335,8 @@ function extractManualMemoizationArgs(
if (maybeDepsList == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list for ${kind} to be an array literal`,
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,
}).withDetail({
@@ -357,9 +353,8 @@ function extractManualMemoizationArgs(
if (maybeDep == null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
category: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
description: `Expected the dependency list to be an array of simple expressions (e.g. \`x\`, \`x.y.z\`, \`x?.y?.z\`)`,
suggestions: null,
}).withDetail({
@@ -464,8 +459,7 @@ export function dropManualMemoization(
errors.pushDiagnostic(
CompilerDiagnostic.create({
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.UseMemo,
reason: 'useMemo() callbacks must return a value',
category: 'useMemo() callbacks must return a value',
description: `This ${
manualMemo.loadInstr.value.kind === 'PropertyLoad'
? 'React.useMemo'
@@ -504,9 +498,8 @@ export function dropManualMemoization(
if (!sidemap.functions.has(fnPlace.identifier.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: `Expected the first argument to be an inline function expression`,
category: `Expected the first argument to be an inline function expression`,
description: `Expected the first argument to be an inline function expression`,
suggestions: [],
}).withDetail({

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

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

@@ -19,7 +19,6 @@ import {
DeclarationId,
Environment,
FunctionExpression,
GeneratedSource,
HIRFunction,
Hole,
IdentifierId,
@@ -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,
@@ -61,15 +65,10 @@ import {
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;
@@ -446,35 +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,
severity: ErrorSeverity.InvalidReact,
reason: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetail({
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`,
}),
});
}
}
@@ -1038,9 +1027,8 @@ function applyEffect(
effect.value.identifier.declarationId,
);
const diagnostic = CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access variable before it is declared',
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) {
@@ -1071,31 +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,
severity: ErrorSeverity.InvalidReact,
reason: 'This value cannot be modified',
description: `${reason}.`,
}).withDetail({
kind: 'error',
loc: effect.value.loc,
message: `${variable} cannot be modified`,
});
if (
effect.kind === 'Mutate' &&
effect.reason?.kind === 'AssignCurrentProperty'
) {
diagnostic.withDetail({
kind: 'hint',
message: `Hint: If this value is a Ref (value returned by \`useRef()\`), rename the variable to end in "Ref".`,
});
}
applyEffect(
context,
state,
@@ -1105,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,
@@ -1702,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,
@@ -2037,9 +2007,8 @@ function computeSignatureForInstruction(
kind: 'MutateGlobal',
place: value.value,
error: CompilerDiagnostic.create({
category: ErrorCategory.Globals,
severity: ErrorSeverity.InvalidReact,
reason:
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)`,
}).withDetail({
@@ -2137,9 +2106,8 @@ function computeEffectsForLegacySignature(
kind: 'Impure',
place: receiver,
error: CompilerDiagnostic.create({
category: ErrorCategory.Purity,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot call impure function during render',
category: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `
@@ -2566,196 +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,
loc: GeneratedSource,
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}`,
loc: GeneratedSource,
},
);
return ValueKind.Primitive;
}
}

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

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

View File

@@ -255,6 +255,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -262,7 +263,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
reversePostorderBlocks(fn.body);

View File

@@ -370,6 +370,7 @@ function emitOutlinedFn(
returnTypeAnnotation: null,
returns: createTemporaryPlace(env, GeneratedSource),
context: [],
effects: null,
body: {
entry: block.id,
blocks: new Map([[block.id, block]]),
@@ -377,7 +378,6 @@ function emitOutlinedFn(
generator: false,
async: false,
directives: [],
aliasingEffects: [],
};
return fn;
}

View File

@@ -13,7 +13,7 @@ import {
pruneUnusedLabels,
renameVariables,
} from '.';
import {CompilerError, ErrorCategory, ErrorSeverity} from '../CompilerError';
import {CompilerError, ErrorSeverity} from '../CompilerError';
import {Environment, ExternalFunction} from '../HIR';
import {
ArrayPattern,
@@ -2185,7 +2185,6 @@ function codegenInstructionValue(
(declarator.id as t.Identifier).name
}'`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: declarator.loc ?? null,
suggestions: null,
});
@@ -2194,7 +2193,6 @@ function codegenInstructionValue(
cx.errors.push({
reason: `(CodegenReactiveFunction::codegenInstructionValue) Handle conversion of ${stmt.type} to expression`,
severity: ErrorSeverity.Todo,
category: ErrorCategory.Todo,
loc: stmt.loc ?? null,
suggestions: null,
});

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();
@@ -216,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;
}
@@ -243,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}]`,
@@ -274,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,
@@ -440,7 +426,6 @@ function areLValuesLastUsedByScope(
function canMergeScopes(
current: ReactiveScopeBlock,
next: ReactiveScopeBlock,
temporaries: Map<DeclarationId, DeclarationId>,
): boolean {
// Don't merge scopes with reassignments
if (
@@ -480,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,
),
))
) {
@@ -495,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

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

View File

@@ -42,7 +42,6 @@ import {
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';
import {USE_FIRE_FUNCTION_NAME} from '../HIR/Environment';
import {ErrorCategory} from '../CompilerError';
/*
* TODO(jmbrown):
@@ -134,7 +133,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason: '[InsertFire] No LoadGlobal found for useEffect call',
suggestions: null,
});
@@ -181,7 +179,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -192,7 +189,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
description:
'You must use an array literal for an effect dependency array when that effect uses `fire()`',
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -227,7 +223,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description: null,
severity: ErrorSeverity.Invariant,
category: ErrorCategory.Invariant,
reason:
'[InsertFire] No loadLocal found for fire call argument',
suggestions: null,
@@ -251,7 +246,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
description:
'`fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed',
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -270,7 +264,6 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
loc: value.loc,
description,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -402,7 +395,6 @@ function ensureNoRemainingCalleeCaptures(
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
severity: ErrorSeverity.InvalidReact,
category: ErrorCategory.Fire,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
@@ -419,7 +411,6 @@ function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
category: ErrorCategory.Fire,
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {HIRFunction, IdentifierId, Type, typeEquals} from '../HIR';
/**
* Temporary workaround for InferTypes not propagating the types of phis.
* Previously, LeaveSSA would replace all the identifiers for each phi (operands and
* the phi itself) with a single "canonical" identifier, generally chosen as the first
* operand to flow into the phi. In case of a phi whose operand was a phi, this could
* sometimes be an operand from the earlier phi.
*
* As a result, even though InferTypes did not propagate types for phis, LeaveSSA
* could end up replacing the phi Identifier with another identifer from an operand,
* which _did_ have a type inferred.
*
* This didn't affect the initial construction of mutable ranges because InferMutableRanges
* runs before LeaveSSA - thus, the types propagated by LeaveSSA only affected later optimizations,
* notably MergeScopesThatInvalidateTogether which uses type to determine if a scope's output
* will always invalidate with its input.
*
* The long-term correct approach is to update InferTypes to infer the types of phis,
* but this is complicated because InferMutableRanges inadvertently depends on phis
* never having a known type, such that a Store effect cannot occur on a phi value.
* Once we fix InferTypes to infer phi types, then we'll also have to update InferMutableRanges
* to handle this case.
*
* As a temporary workaround, this pass propagates the type of phis and can be called
* safely *after* InferMutableRanges. Unlike LeaveSSA, this pass only propagates the
* type if all operands have the same type, it's its more correct.
*/
export function propagatePhiTypes(fn: HIRFunction): void {
/**
* We track which SSA ids have had their types propagated to handle nested ternaries,
* see the StoreLocal handling below
*/
const propagated = new Set<IdentifierId>();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
/*
* We replicate the previous LeaveSSA behavior and only propagate types for
* unnamed variables. LeaveSSA would have chosen one of the operands as the
* canonical id and taken its type as the type of all identifiers. We're
* more conservative and only propagate if the types are the same and the
* phi didn't have a type inferred.
*
* Note that this can change output slightly in cases such as
* `cond ? <div /> : null`.
*
* Previously the first operand's type (BuiltInJsx) would have been propagated,
* and this expression may have been merged with subsequent reactive scopes
* since it appears (based on that type) to always invalidate.
*
* But the correct type is `BuiltInJsx | null`, which we can't express and
* so leave as a generic `Type`, which does not always invalidate and therefore
* does not merge with subsequent scopes.
*
* We also don't propagate scopes for named variables, to preserve compatibility
* with previous LeaveSSA behavior.
*/
if (
phi.place.identifier.type.kind !== 'Type' ||
phi.place.identifier.name !== null
) {
continue;
}
let type: Type | null = null;
for (const [, operand] of phi.operands) {
if (type === null) {
type = operand.identifier.type;
} else if (!typeEquals(type, operand.identifier.type)) {
type = null;
break;
}
}
if (type !== null) {
phi.place.identifier.type = type;
propagated.add(phi.place.identifier.id);
}
}
for (const instr of block.instructions) {
const {value} = instr;
switch (value.kind) {
case 'StoreLocal': {
/**
* Nested ternaries can lower to a form with an intermediate StoreLocal where
* the value.lvalue is the temporary of the outer ternary, and the value.value
* is the result of the inner ternary.
*
* This is a common pattern in practice and easy enough to support. Again, the
* long-term approach is to update InferTypes and InferMutableRanges.
*/
const lvalue = value.lvalue.place;
if (
propagated.has(value.value.identifier.id) &&
lvalue.identifier.type.kind === 'Type' &&
lvalue.identifier.name === null
) {
lvalue.identifier.type = value.value.identifier.type;
propagated.add(lvalue.identifier.id);
}
}
}
}
}
}

View File

@@ -78,10 +78,6 @@ export default class DisjointSet<T> {
return root;
}
has(item: T): boolean {
return this.#entries.has(item);
}
/*
* Forces the set into canonical form, ie with all items pointing directly to
* their root, and returns a Map representing the mapping of items to their roots.

View File

@@ -1,87 +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.
*/
/**
* https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-keywords-and-reserved-words
*/
/**
* Note: `await` and `yield` are contextually allowed as identifiers.
* await: reserved inside async functions and modules
* yield: reserved inside generator functions
*
* Note: `async` is not reserved.
*/
const RESERVED_WORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'new',
'null',
'return',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
]);
/**
* Reserved when a module has a 'use strict' directive.
*/
const STRICT_MODE_RESERVED_WORDS = new Set([
'let',
'static',
'implements',
'interface',
'package',
'private',
'protected',
'public',
]);
/**
* The names arguments and eval are not keywords, but they are subject to some restrictions in
* strict mode code.
*/
const STRICT_MODE_RESTRICTED_WORDS = new Set(['eval', 'arguments']);
/**
* Conservative check for whether an identifer name is reserved or not. We assume that code is
* written with strict mode.
*/
export function isReservedWord(identifierName: string): boolean {
return (
RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESERVED_WORDS.has(identifierName) ||
STRICT_MODE_RESTRICTED_WORDS.has(identifierName)
);
}

View File

@@ -90,13 +90,10 @@ export function Ok<T>(val: T): OkImpl<T> {
}
class OkImpl<T> implements Result<T, never> {
#val: T;
constructor(val: T) {
this.#val = val;
}
constructor(private val: T) {}
map<U>(fn: (val: T) => U): Result<U, never> {
return new OkImpl(fn(this.#val));
return new OkImpl(fn(this.val));
}
mapErr<F>(_fn: (val: never) => F): Result<T, F> {
@@ -104,15 +101,15 @@ class OkImpl<T> implements Result<T, never> {
}
mapOr<U>(_fallback: U, fn: (val: T) => U): U {
return fn(this.#val);
return fn(this.val);
}
mapOrElse<U>(_fallback: () => U, fn: (val: T) => U): U {
return fn(this.#val);
return fn(this.val);
}
andThen<U>(fn: (val: T) => Result<U, never>): Result<U, never> {
return fn(this.#val);
return fn(this.val);
}
and<U>(res: Result<U, never>): Result<U, never> {
@@ -136,30 +133,30 @@ class OkImpl<T> implements Result<T, never> {
}
expect(_msg: string): T {
return this.#val;
return this.val;
}
expectErr(msg: string): never {
throw new Error(`${msg}: ${this.#val}`);
throw new Error(`${msg}: ${this.val}`);
}
unwrap(): T {
return this.#val;
return this.val;
}
unwrapOr(_fallback: T): T {
return this.#val;
return this.val;
}
unwrapOrElse(_fallback: (val: never) => T): T {
return this.#val;
return this.val;
}
unwrapErr(): never {
if (this.#val instanceof Error) {
throw this.#val;
if (this.val instanceof Error) {
throw this.val;
}
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.#val}`);
throw new Error(`Can't unwrap \`Ok\` to \`Err\`: ${this.val}`);
}
}
@@ -168,17 +165,14 @@ export function Err<E>(val: E): ErrImpl<E> {
}
class ErrImpl<E> implements Result<never, E> {
#val: E;
constructor(val: E) {
this.#val = val;
}
constructor(private val: E) {}
map<U>(_fn: (val: never) => U): Result<U, E> {
return this;
}
mapErr<F>(fn: (val: E) => F): Result<never, F> {
return new ErrImpl(fn(this.#val));
return new ErrImpl(fn(this.val));
}
mapOr<U>(fallback: U, _fn: (val: never) => U): U {
@@ -202,7 +196,7 @@ class ErrImpl<E> implements Result<never, E> {
}
orElse<F>(fn: (val: E) => ErrImpl<F>): Result<never, F> {
return fn(this.#val);
return fn(this.val);
}
isOk(): this is OkImpl<never> {
@@ -214,18 +208,18 @@ class ErrImpl<E> implements Result<never, E> {
}
expect(msg: string): never {
throw new Error(`${msg}: ${this.#val}`);
throw new Error(`${msg}: ${this.val}`);
}
expectErr(_msg: string): E {
return this.#val;
return this.val;
}
unwrap(): never {
if (this.#val instanceof Error) {
throw this.#val;
if (this.val instanceof Error) {
throw this.val;
}
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.#val}`);
throw new Error(`Can't unwrap \`Err\` to \`Ok\`: ${this.val}`);
}
unwrapOr<T>(fallback: T): T {
@@ -233,10 +227,10 @@ class ErrImpl<E> implements Result<never, E> {
}
unwrapOrElse<T>(fallback: (val: E) => T): T {
return fallback(this.#val);
return fallback(this.val);
}
unwrapErr(): E {
return this.#val;
return this.val;
}
}

View File

@@ -33,12 +33,12 @@ export function assertExhaustive(_: never, errorMsg: string): never {
// Modifies @param array in place, retaining only the items where the predicate returns true.
export function retainWhere<T>(
array: Array<T>,
predicate: (item: T, index: number) => boolean,
predicate: (item: T) => boolean,
): void {
let writeIndex = 0;
for (let readIndex = 0; readIndex < array.length; readIndex++) {
const item = array[readIndex];
if (predicate(item, readIndex) === true) {
if (predicate(item) === true) {
array[writeIndex++] = item;
}
}

View File

@@ -9,7 +9,6 @@ import * as t from '@babel/types';
import {
CompilerError,
CompilerErrorDetail,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks';
@@ -125,7 +124,6 @@ export function validateHooksUsage(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason,
loc: place.loc,
@@ -142,7 +140,6 @@ export function validateHooksUsage(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values',
@@ -160,7 +157,6 @@ export function validateHooksUsage(
recordError(
place.loc,
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
description: null,
reason:
'Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks',
@@ -428,7 +424,7 @@ export function validateHooksUsage(
}
for (const [, error] of errorsByPlace) {
errors.pushErrorDetail(error);
errors.push(error);
}
return errors.asResult();
}
@@ -452,7 +448,6 @@ function visitFunctionExpression(errors: CompilerError, fn: HIRFunction): void {
if (hookKind != null) {
errors.pushErrorDetail(
new CompilerErrorDetail({
category: ErrorCategory.Hooks,
severity: ErrorSeverity.InvalidReact,
reason:
'Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)',

View File

@@ -6,14 +6,13 @@
*/
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId, Place} from '../HIR';
import {
eachInstructionLValue,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
/**
* Validates that local variables cannot be reassigned after render.
@@ -37,9 +36,8 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable after render completes',
category: 'Cannot reassign variable after render completes',
description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
}).withDetail({
kind: 'error',
@@ -93,9 +91,8 @@ function getContextReassignment(
: 'variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot reassign variable in async function',
category: 'Cannot reassign variable in async function',
description:
'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
}).withDetail({

View File

@@ -6,7 +6,6 @@
*/
import {CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {
Identifier,
Instruction,
@@ -109,7 +108,6 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
isUnmemoized(deps.identifier, this.scopes))
) {
state.push({
category: ErrorCategory.EffectDependencies,
reason:
'React Compiler has skipped optimizing this component because the effect dependencies could not be memoized. Unmemoized effect dependencies can trigger an infinite loop or other unexpected behavior',
description: null,

View File

@@ -6,7 +6,6 @@
*/
import {CompilerError, EnvironmentConfig, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction, IdentifierId} from '../HIR';
import {DEFAULT_GLOBALS} from '../HIR/Globals';
import {Result} from '../Utils/Result';
@@ -57,7 +56,6 @@ export function validateNoCapitalizedCalls(
const calleeName = capitalLoadGlobals.get(calleeIdentifier);
if (calleeName != null) {
CompilerError.throwInvalidReact({
category: ErrorCategory.CapitalizedCalls,
reason,
description: `${calleeName} may be a component.`,
loc: value.loc,
@@ -81,7 +79,6 @@ export function validateNoCapitalizedCalls(
const propertyName = capitalizedProperties.get(propertyIdentifier);
if (propertyName != null) {
errors.push({
category: ErrorCategory.CapitalizedCalls,
severity: ErrorSeverity.InvalidReact,
reason,
description: `${propertyName} may be a component.`,

View File

@@ -1,232 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, SourceLocation} from '..';
import {ErrorCategory} from '../CompilerError';
import {
ArrayExpression,
BlockId,
FunctionExpression,
HIRFunction,
IdentifierId,
isSetStateType,
isUseEffectHookType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
/**
* Validates that useEffect is not used for derived computations which could/should
* be performed in render.
*
* See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
*
* Example:
*
* ```
* // 🔴 Avoid: redundant state and unnecessary Effect
* const [fullName, setFullName] = useState('');
* useEffect(() => {
* setFullName(firstName + ' ' + lastName);
* }, [firstName, lastName]);
* ```
*
* Instead use:
*
* ```
* // ✅ Good: calculated during rendering
* const fullName = firstName + ' ' + lastName;
* ```
*/
export function validateNoDerivedComputationsInEffects(fn: HIRFunction): void {
const candidateDependencies: Map<IdentifierId, ArrayExpression> = new Map();
const functions: Map<IdentifierId, FunctionExpression> = new Map();
const locals: Map<IdentifierId, IdentifierId> = new Map();
const errors = new CompilerError();
for (const block of fn.body.blocks.values()) {
for (const instr of block.instructions) {
const {lvalue, value} = instr;
if (value.kind === 'LoadLocal') {
locals.set(lvalue.identifier.id, value.place.identifier.id);
} else if (value.kind === 'ArrayExpression') {
candidateDependencies.set(lvalue.identifier.id, value);
} else if (value.kind === 'FunctionExpression') {
functions.set(lvalue.identifier.id, value);
} else if (
value.kind === 'CallExpression' ||
value.kind === 'MethodCall'
) {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
if (
isUseEffectHookType(callee.identifier) &&
value.args.length === 2 &&
value.args[0].kind === 'Identifier' &&
value.args[1].kind === 'Identifier'
) {
const effectFunction = functions.get(value.args[0].identifier.id);
const deps = candidateDependencies.get(value.args[1].identifier.id);
if (
effectFunction != null &&
deps != null &&
deps.elements.length !== 0 &&
deps.elements.every(element => element.kind === 'Identifier')
) {
const dependencies: Array<IdentifierId> = deps.elements.map(dep => {
CompilerError.invariant(dep.kind === 'Identifier', {
reason: `Dependency is checked as a place above`,
loc: value.loc,
});
return locals.get(dep.identifier.id) ?? dep.identifier.id;
});
validateEffect(
effectFunction.loweredFunc.func,
dependencies,
errors,
);
}
}
}
}
}
if (errors.hasErrors()) {
throw errors;
}
}
function validateEffect(
effectFunction: HIRFunction,
effectDeps: Array<IdentifierId>,
errors: CompilerError,
): void {
for (const operand of effectFunction.context) {
if (isSetStateType(operand.identifier)) {
continue;
} else if (effectDeps.find(dep => dep === operand.identifier.id) != null) {
continue;
} else {
// Captured something other than the effect dep or setState
return;
}
}
for (const dep of effectDeps) {
if (
effectFunction.context.find(operand => operand.identifier.id === dep) ==
null
) {
// effect dep wasn't actually used in the function
return;
}
}
const seenBlocks: Set<BlockId> = new Set();
const values: Map<IdentifierId, Array<IdentifierId>> = new Map();
for (const dep of effectDeps) {
values.set(dep, [dep]);
}
const setStateLocations: Array<SourceLocation> = [];
for (const block of effectFunction.body.blocks.values()) {
for (const pred of block.preds) {
if (!seenBlocks.has(pred)) {
// skip if block has a back edge
return;
}
}
for (const phi of block.phis) {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of phi.operands.values()) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(phi.place.identifier.id, Array.from(aggregateDeps));
}
}
for (const instr of block.instructions) {
switch (instr.value.kind) {
case 'Primitive':
case 'JSXText':
case 'LoadGlobal': {
break;
}
case 'LoadLocal': {
const deps = values.get(instr.value.place.identifier.id);
if (deps != null) {
values.set(instr.lvalue.identifier.id, deps);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad':
case 'BinaryExpression':
case 'TemplateLiteral':
case 'CallExpression':
case 'MethodCall': {
const aggregateDeps: Set<IdentifierId> = new Set();
for (const operand of eachInstructionValueOperand(instr.value)) {
const deps = values.get(operand.identifier.id);
if (deps != null) {
for (const dep of deps) {
aggregateDeps.add(dep);
}
}
}
if (aggregateDeps.size !== 0) {
values.set(instr.lvalue.identifier.id, Array.from(aggregateDeps));
}
if (
instr.value.kind === 'CallExpression' &&
isSetStateType(instr.value.callee.identifier) &&
instr.value.args.length === 1 &&
instr.value.args[0].kind === 'Identifier'
) {
const deps = values.get(instr.value.args[0].identifier.id);
if (deps != null && new Set(deps).size === effectDeps.length) {
setStateLocations.push(instr.value.callee.loc);
} else {
// doesn't depend on any deps
return;
}
}
break;
}
default: {
return;
}
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (values.has(operand.identifier.id)) {
//
return;
}
}
seenBlocks.add(block.id);
}
for (const loc of setStateLocations) {
errors.push({
category: ErrorCategory.EffectDerivationsOfState,
reason:
'Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)',
description: null,
severity: ErrorSeverity.InvalidReact,
loc,
suggestions: null,
});
}
}

View File

@@ -6,10 +6,11 @@
*/
import {CompilerDiagnostic, CompilerError, Effect, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {
FunctionEffect,
HIRFunction,
IdentifierId,
isMutableEffect,
isRefOrRefLikeMutableType,
Place,
} from '../HIR';
@@ -17,8 +18,8 @@ import {
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {AliasingEffect} from '../Inference/AliasingEffects';
import {Result} from '../Utils/Result';
import {Iterable_some} from '../Utils/utils';
/**
* Validates that functions with known mutations (ie due to types) cannot be passed
@@ -49,14 +50,14 @@ export function validateNoFreezingKnownMutableFunctions(
const errors = new CompilerError();
const contextMutationEffects: Map<
IdentifierId,
Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
Extract<FunctionEffect, {kind: 'ContextMutation'}>
> = new Map();
function visitOperand(operand: Place): void {
if (operand.effect === Effect.Freeze) {
const effect = contextMutationEffects.get(operand.identifier.id);
if (effect != null) {
const place = effect.value;
const place = [...effect.places][0];
const variable =
place != null &&
place.identifier.name != null &&
@@ -65,9 +66,8 @@ export function validateNoFreezingKnownMutableFunctions(
: 'a local variable';
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Immutability,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot modify local variables after render completes',
category: 'Cannot modify local variables after render completes',
description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead.`,
})
.withDetail({
@@ -77,7 +77,7 @@ export function validateNoFreezingKnownMutableFunctions(
})
.withDetail({
kind: 'error',
loc: effect.value.loc,
loc: effect.loc,
message: `This modifies ${variable}`,
}),
);
@@ -108,7 +108,27 @@ export function validateNoFreezingKnownMutableFunctions(
break;
}
case 'FunctionExpression': {
if (value.loweredFunc.func.aliasingEffects != null) {
const knownMutation = (value.loweredFunc.func.effects ?? []).find(
effect => {
return (
effect.kind === 'ContextMutation' &&
(effect.effect === Effect.Store ||
effect.effect === Effect.Mutate) &&
Iterable_some(effect.places, place => {
return (
isMutableEffect(place.effect, place.loc) &&
!isRefOrRefLikeMutableType(place.identifier.type)
);
})
);
},
);
if (knownMutation && knownMutation.kind === 'ContextMutation') {
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
} else if (
fn.env.config.enableNewMutationAliasingModel &&
value.loweredFunc.func.aliasingEffects != null
) {
const context = new Set(
value.loweredFunc.func.context.map(p => p.identifier.id),
);
@@ -129,7 +149,12 @@ export function validateNoFreezingKnownMutableFunctions(
context.has(effect.value.identifier.id) &&
!isRefOrRefLikeMutableType(effect.value.identifier.type)
) {
contextMutationEffects.set(lvalue.identifier.id, effect);
contextMutationEffects.set(lvalue.identifier.id, {
kind: 'ContextMutation',
effect: Effect.Mutate,
loc: effect.value.loc,
places: new Set([effect.value]),
});
break effects;
}
break;

View File

@@ -6,9 +6,8 @@
*/
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {HIRFunction} from '../HIR';
import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
import {getFunctionCallSignature} from '../Inference/InferReferenceEffects';
import {Result} from '../Utils/Result';
/**
@@ -37,8 +36,7 @@ export function validateNoImpureFunctionsInRender(
if (signature != null && signature.impure === true) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Purity,
reason: 'Cannot call impure function during render',
category: 'Cannot call impure function during render',
description:
(signature.canonicalName != null
? `\`${signature.canonicalName}\` is an impure function. `

View File

@@ -6,7 +6,6 @@
*/
import {CompilerDiagnostic, CompilerError, ErrorSeverity} from '..';
import {ErrorCategory} from '../CompilerError';
import {BlockId, HIRFunction} from '../HIR';
import {Result} from '../Utils/Result';
import {retainWhere} from '../Utils/utils';
@@ -37,9 +36,8 @@ export function validateNoJSXInTryStatement(
case 'JsxFragment': {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.ErrorBoundaries,
severity: ErrorSeverity.InvalidReact,
reason: 'Avoid constructing JSX within try/catch',
category: 'Avoid constructing JSX within try/catch',
description: `React does not immediately render components when JSX is rendered, so any errors from this component will not be caught by the try/catch. To catch errors in rendering a given component, wrap that component in an error boundary. (https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)`,
}).withDetail({
kind: 'error',

View File

@@ -8,7 +8,6 @@
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {
@@ -469,9 +468,8 @@ function validateNoRefAccessInRenderImpl(
didError = true;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
@@ -733,9 +731,8 @@ function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
if (env.get(operand.identifier.id)?.kind === 'Guard') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
@@ -758,9 +755,8 @@ function validateNoRefValueAccess(
) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
@@ -785,9 +781,8 @@ function validateNoRefPassedToFunction(
) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
@@ -808,9 +803,8 @@ function validateNoRefUpdate(
if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',
@@ -830,9 +824,8 @@ function validateNoDirectRefValueAccess(
if (type?.kind === 'RefValue') {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.Refs,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot access refs during render',
category: 'Cannot access refs during render',
description: ERROR_DESCRIPTION,
}).withDetail({
kind: 'error',

View File

@@ -8,7 +8,6 @@
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {
@@ -97,8 +96,7 @@ export function validateNoSetStateInEffects(
if (setState !== undefined) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.EffectSetState,
reason:
category:
'Calling setState synchronously within an effect can trigger cascading renders',
description:
'Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. ' +

View File

@@ -8,7 +8,6 @@
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {HIRFunction, IdentifierId, isSetStateType} from '../HIR';
@@ -129,8 +128,7 @@ function validateNoSetStateInRenderImpl(
if (activeManualMemoId !== null) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
category:
'Calling setState from useMemo may trigger an infinite loop',
description:
'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)',
@@ -145,8 +143,7 @@ function validateNoSetStateInRenderImpl(
} else if (unconditionalBlocks.has(block.id)) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.RenderSetState,
reason:
category:
'Calling setState during render may trigger an infinite loop',
description:
'Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState)',
@@ -155,7 +152,7 @@ function validateNoSetStateInRenderImpl(
}).withDetail({
kind: 'error',
loc: callee.loc,
message: 'Found setState() in render',
message: 'Found setState() within useMemo()',
}),
);
}

View File

@@ -8,7 +8,6 @@
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {
@@ -282,9 +281,8 @@ function validateInferredDep(
}
errorState.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
category:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
@@ -537,9 +535,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
) {
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
category:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. ',
@@ -586,9 +583,8 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
if (isUnmemoized(identifier, this.scopes)) {
state.errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.PreserveManualMemo,
severity: ErrorSeverity.CannotPreserveMemoization,
reason:
category:
'Compilation skipped because existing memoization could not be preserved',
description: [
'React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. ',

View File

@@ -8,7 +8,6 @@
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {HIRFunction, IdentifierId, SourceLocation} from '../HIR';
@@ -66,9 +65,8 @@ export function validateStaticComponents(
if (location != null) {
error.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.StaticComponents,
severity: ErrorSeverity.InvalidReact,
reason: 'Cannot create components during render',
category: 'Cannot create components during render',
description: `Components created during render will reset their state each time they are created. Declare components outside of render. `,
})
.withDetail({

View File

@@ -8,7 +8,6 @@
import {
CompilerDiagnostic,
CompilerError,
ErrorCategory,
ErrorSeverity,
} from '../CompilerError';
import {FunctionExpression, HIRFunction, IdentifierId} from '../HIR';
@@ -75,9 +74,8 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
: firstParam.place.loc;
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason: 'useMemo() callbacks may not accept parameters',
category: 'useMemo() callbacks may not accept parameters',
description:
'useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.',
suggestions: null,
@@ -92,9 +90,8 @@ export function validateUseMemo(fn: HIRFunction): Result<void, CompilerError> {
if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
errors.pushDiagnostic(
CompilerDiagnostic.create({
category: ErrorCategory.UseMemo,
severity: ErrorSeverity.InvalidReact,
reason:
category:
'useMemo() callbacks may not be async or generator functions',
description:
'useMemo() callbacks are called once and must synchronously return a value.',

View File

@@ -1,39 +0,0 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
};
// spreading a function is weird, but it doesn't call the function so this is allowed
return <div {...foo} />;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
function Component() {
const $ = _c(1);
const foo = _temp;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <div {...foo} />;
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}
function _temp() {
someGlobal = true;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -19,7 +19,7 @@ function Component(props) {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(5);
const $ = _c(7);
let t0;
if ($[0] !== props.x) {
t0 = foo(props.x);
@@ -31,19 +31,26 @@ function Component(props) {
const x = t0;
let t1;
if ($[2] !== props || $[3] !== x) {
const fn = function () {
t1 = function () {
const arr = [...bar(props)];
return arr.at(x);
};
t1 = fn();
$[2] = props;
$[3] = x;
$[4] = t1;
} else {
t1 = $[4];
}
const fnResult = t1;
const fn = t1;
let t2;
if ($[5] !== fn) {
t2 = fn();
$[5] = fn;
$[6] = t2;
} else {
t2 = $[6];
}
const fnResult = t2;
return fnResult;
}

View File

@@ -23,18 +23,34 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(6);
let t0;
if ($[0] !== props.a) {
const item = { a: props.a };
const items = [item];
t0 = items.map(_temp);
t0 = { a: props.a };
$[0] = props.a;
$[1] = t0;
} else {
t0 = $[1];
}
const mapped = t0;
const item = t0;
let t1;
if ($[2] !== item) {
t1 = [item];
$[2] = item;
$[3] = t1;
} else {
t1 = $[3];
}
const items = t1;
let t2;
if ($[4] !== items) {
t2 = items.map(_temp);
$[4] = items;
$[5] = t2;
} else {
t2 = $[5];
}
const mapped = t2;
return mapped;
}
function _temp(item_0) {

View File

@@ -21,18 +21,26 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component(props) {
const $ = _c(2);
const $ = _c(4);
const f = _temp;
let t0;
if ($[0] !== props.items) {
const x = [...props.items].map(f);
t0 = [x, f];
t0 = [...props.items].map(f);
$[0] = props.items;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
const x = t0;
let t1;
if ($[2] !== x) {
t1 = [x, f];
$[2] = x;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
function _temp(item) {
return item;

View File

@@ -0,0 +1,107 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(5);
const { a, b } = t0;
let t1;
if ($[0] !== a || $[1] !== b) {
const x = [];
const y = { value: a };
arrayPush(x, y);
const y_alias = y;
let t2;
if ($[3] !== y_alias.value) {
t2 = () => y_alias.value;
$[3] = y_alias.value;
$[4] = t2;
} else {
t2 = $[4];
}
const cb = t2;
setPropertyByKey(x[0], "value", b);
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = b;
$[2] = t1;
} else {
t1 = $[2];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2, b: 10 }],
sequentialRenders: [
{ a: 2, b: 10 },
{ a: 2, b: 11 },
],
};
```

View File

@@ -0,0 +1,53 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
/**
* 1. `InferMutableRanges` derives the mutable range of identifiers and their
* aliases from `LoadLocal`, `PropertyLoad`, etc
* - After this pass, y's mutable range only extends to `arrayPush(x, y)`
* - We avoid assigning mutable ranges to loads after y's mutable range, as
* these are working with an immutable value. As a result, `LoadLocal y` and
* `PropertyLoad y` do not get mutable ranges
* 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
* as according to the 'co-mutation' of different values
* - Here, we infer that
* - `arrayPush(y, x)` might alias `x` and `y` to each other
* - `setPropertyKey(x, ...)` may mutate both `x` and `y`
* - This pass correctly extends the mutable range of `y`
* - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
* PropertyLoads still don't have a mutable range
*
* Note that the this bug is an edge case. Compiler output is only invalid for:
* - function expressions with
* `enableTransitivelyFreezeFunctionExpressions:false`
* - functions that throw and get retried without clearing the memocache
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
*/
function useFoo({a, b}: {a: number, b: number}) {
const x = [];
const y = {value: a};
arrayPush(x, y); // x and y co-mutate
const y_alias = y;
const cb = () => y_alias.value;
setPropertyByKey(x[0], 'value', b); // might overwrite y.value
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2, b: 10}],
sequentialRenders: [
{a: 2, b: 10},
{a: 2, b: 11},
],
};

View File

@@ -0,0 +1,87 @@
## Input
```javascript
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { setPropertyByKey, Stringify } from "shared-runtime";
function useFoo(t0) {
const $ = _c(4);
const { a } = t0;
let t1;
if ($[0] !== a) {
const arr = [];
const obj = { value: a };
setPropertyByKey(obj, "arr", arr);
const obj_alias = obj;
let t2;
if ($[2] !== obj_alias.arr.length) {
t2 = () => obj_alias.arr.length;
$[2] = obj_alias.arr.length;
$[3] = t2;
} else {
t2 = $[3];
}
const cb = t2;
for (let i = 0; i < a; i++) {
arr.push(i);
}
t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{ a: 2 }],
sequentialRenders: [{ a: 2 }, { a: 3 }],
};
```

View File

@@ -0,0 +1,34 @@
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
import {setPropertyByKey, Stringify} from 'shared-runtime';
/**
* Variation of bug in `bug-aliased-capture-aliased-mutate`
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
* Forget:
* (kind: ok)
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
* <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
*/
function useFoo({a}: {a: number, b: number}) {
const arr = [];
const obj = {value: a};
setPropertyByKey(obj, 'arr', arr);
const obj_alias = obj;
const cb = () => obj_alias.arr.length;
for (let i = 0; i < a; i++) {
arr.push(i);
}
return <Stringify cb={cb} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useFoo,
params: [{a: 2}],
sequentialRenders: [{a: 2}, {a: 3}],
};

View File

@@ -85,11 +85,19 @@ import { makeArray, mutate } from "shared-runtime";
* used when we analyze CallExpressions.
*/
function Component(t0) {
const $ = _c(3);
const $ = _c(5);
const { foo, bar } = t0;
let t1;
if ($[0] !== foo) {
t1 = { foo };
$[0] = foo;
$[1] = t1;
} else {
t1 = $[1];
}
const x = t1;
let y;
if ($[0] !== bar || $[1] !== foo) {
const x = { foo };
if ($[2] !== bar || $[3] !== x) {
y = { bar };
const f0 = function () {
const a = makeArray(y);
@@ -100,11 +108,11 @@ function Component(t0) {
f0();
mutate(y.x);
$[0] = bar;
$[1] = foo;
$[2] = y;
$[2] = bar;
$[3] = x;
$[4] = y;
} else {
y = $[2];
y = $[4];
}
return y;
}

View File

@@ -0,0 +1,92 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const $ = _c(4);
const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [obj?.inner];
$[0] = t0;
} else {
t0 = $[0];
}
const boxedInner = t0;
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error("invariant broken");
}
let t1;
if ($[1] !== boxedInner || $[2] !== obj) {
t1 = <Stringify obj={obj} inner={boxedInner} />;
$[1] = boxedInner;
$[2] = obj;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ arg: 0 }],
sequentialRenders: [{ arg: 0 }, { arg: 1 }],
};
```

View File

@@ -0,0 +1,31 @@
// @enableNewMutationAliasingModel:false
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
/**
* Fixture showing an edge case for ReactiveScope variable propagation.
*
* Found differences in evaluator results
* Non-forget (expected):
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* Forget:
* <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
* [[ (exception in render) Error: invariant broken ]]
*
*/
function Component() {
const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
const boxedInner = [obj?.inner];
useIdentity(null);
mutate(obj);
if (boxedInner[0] !== obj?.inner) {
throw new Error('invariant broken');
}
return <Stringify obj={obj} inner={boxedInner} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{arg: 0}],
sequentialRenders: [{arg: 0}, {arg: 1}],
};

View File

@@ -0,0 +1,110 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
import { identity, mutate } from "shared-runtime";
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const $ = _c(8);
let key;
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
key = {};
t0 = (mutate(key), key);
$[0] = key;
$[1] = t0;
} else {
key = $[0];
t0 = $[1];
}
const tmp = t0;
let t1;
if ($[2] !== props.value) {
t1 = identity([props.value]);
$[2] = props.value;
$[3] = t1;
} else {
t1 = $[3];
}
let t2;
if ($[4] !== t1) {
t2 = { [tmp]: t1 };
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
const context = t2;
mutate(key);
let t3;
if ($[6] !== context) {
t3 = [context, key];
$[6] = context;
$[7] = t3;
} else {
t3 = $[7];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{ value: 42 }],
sequentialRenders: [{ value: 42 }, { value: 42 }],
};
```

View File

@@ -0,0 +1,32 @@
// @enableNewMutationAliasingModel:false
import {identity, mutate} from 'shared-runtime';
/**
* Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
* with the mutation hoisted to a named variable instead of being directly
* inlined into the Object key.
*
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* Forget:
* (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
* [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
*/
function Component(props) {
const key = {};
const tmp = (mutate(key), key);
const context = {
// Here, `tmp` is frozen (as it's inferred to be a primitive/string)
[tmp]: identity([props.value]),
};
mutate(key);
return [context, key];
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
params: [{value: 42}],
sequentialRenders: [{value: 42}, {value: 42}],
};

View File

@@ -23,19 +23,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = () => {
console.log(z);
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = () => {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -23,19 +23,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = function () {
console.log(z);
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -22,19 +22,35 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c } from "react/compiler-runtime";
import { Stringify } from "shared-runtime";
function Component(t0) {
const $ = _c(2);
const $ = _c(6);
const { a } = t0;
let t1;
if ($[0] !== a) {
const z = { a };
const p = () => <Stringify>{z}</Stringify>;
t1 = p();
t1 = { a };
$[0] = a;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
const z = t1;
let t2;
if ($[2] !== z) {
t2 = () => <Stringify>{z}</Stringify>;
$[2] = z;
$[3] = t2;
} else {
t2 = $[3];
}
const p = t2;
let t3;
if ($[4] !== p) {
t3 = p();
$[4] = p;
$[5] = t3;
} else {
t3 = $[5];
}
return t3;
}
export const FIXTURE_ENTRYPOINT = {

View File

@@ -25,19 +25,27 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = function () {
console.log(z);
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
console.log(z);
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -25,21 +25,29 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function component(a) {
const $ = _c(2);
const $ = _c(4);
let t0;
if ($[0] !== a) {
const z = { a };
t0 = function () {
(function () {
console.log(z);
})();
};
t0 = { a };
$[0] = a;
$[1] = t0;
} else {
t0 = $[1];
}
const x = t0;
const z = t0;
let t1;
if ($[2] !== z) {
t1 = function () {
(function () {
console.log(z);
})();
};
$[2] = z;
$[3] = t1;
} else {
t1 = $[3];
}
const x = t1;
return x;
}

View File

@@ -1,32 +0,0 @@
## Input
```javascript
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}
```
## Error
```
Found 1 error:
Error: Expected a non-reserved identifier name
`this` is a reserved word in JavaScript and cannot be used as an identifier name.
```

View File

@@ -1,13 +0,0 @@
import {useRef} from 'react';
function useThing(fn) {
const fnRef = useRef(fn);
const ref = useRef(null);
if (ref.current === null) {
ref.current = function (this: unknown, ...args) {
return fnRef.current.call(this, ...args);
};
}
return ref.current;
}

View File

@@ -0,0 +1,33 @@
## Input
```javascript
// @enableNewMutationAliasingModel:false
function Component() {
const foo = () => {
someGlobal = true;
};
return <div {...foo} />;
}
```
## Error
```
Found 1 error:
Error: 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)
error.assign-global-in-jsx-spread-attribute.ts:4:4
2 | function Component() {
3 | const foo = () => {
> 4 | someGlobal = true;
| ^^^^^^^^^^ 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)
5 | };
6 | return <div {...foo} />;
7 | }
```

View File

@@ -3,6 +3,5 @@ function Component() {
const foo = () => {
someGlobal = true;
};
// spreading a function is weird, but it doesn't call the function so this is allowed
return <div {...foo} />;
}

View File

@@ -1,46 +0,0 @@
## Input
```javascript
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}
```
## Error
```
Found 1 error:
Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized
<unknown> thunk$14.
error.bug-infer-mutation-aliasing-effects.ts:10:22
8 | function thunk(action) {
9 | if (typeof action === 'function') {
> 10 | return action(thunk, () => stateRef.current, extraArg);
| ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized
11 | } else {
12 | dispatch(action);
13 | return undefined;
```

View File

@@ -1,18 +0,0 @@
import {useCallback, useRef} from 'react';
export default function useThunkDispatch(state, dispatch, extraArg) {
const stateRef = useRef(state);
stateRef.current = state;
return useCallback(
function thunk(action) {
if (typeof action === 'function') {
return action(thunk, () => stateRef.current, extraArg);
} else {
dispatch(action);
return undefined;
}
},
[dispatch, extraArg]
);
}

View File

@@ -1,31 +0,0 @@
## Input
```javascript
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};
```
## Error
```
Found 1 error:
Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
error.bug-invariant-codegen-methodcall.ts:3:17
1 | const YearsAndMonthsSince = () => {
2 | const diff = foo();
> 3 | const months = Math.floor(diff.bar());
| ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier`
4 | return <>{months}</>;
5 | };
6 |
```

View File

@@ -1,5 +0,0 @@
const YearsAndMonthsSince = () => {
const diff = foo();
const months = Math.floor(diff.bar());
return <>{months}</>;
};

View File

@@ -1,37 +0,0 @@
## Input
```javascript
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}
```
## Error
```
Found 1 error:
Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration.
error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14
5 | try {
6 | // do something
> 7 | } catch ({status}) {
| ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration.
8 | // do something
9 | }
10 | }, []);
```

View File

@@ -1,11 +0,0 @@
import {useEffect} from 'react';
export function Foo() {
useEffect(() => {
try {
// do something
} catch ({status}) {
// do something
}
}, []);
}

View File

@@ -1,32 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}
```
## Error
```
Found 1 error:
Invariant: Expected a break target
```

View File

@@ -1,15 +0,0 @@
import {useMemo} from 'react';
export default function useFoo(text) {
return useMemo(() => {
try {
let formattedText = '';
try {
formattedText = format(text);
} catch {
console.log('error');
}
return formattedText || '';
} catch (e) {}
}, [text]);
}

View File

@@ -1,44 +0,0 @@
## Input
```javascript
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};
```
## Error
```
Found 1 error:
Invariant: Expected consistent kind for destructuring
Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const.
error.bug-invariant-expected-consistent-destructuring.ts:9:9
7 |
8 | if (data) {
> 9 | ({a, b} = data);
| ^ Expected consistent kind for destructuring
10 | }
11 |
12 | const foo = useFoo(a);
```

View File

@@ -1,16 +0,0 @@
import {useMemo} from 'react';
import {useFoo, formatB, Baz} from './lib';
export const Example = ({data}) => {
let a;
let b;
if (data) {
({a, b} = data);
}
const foo = useFoo(a);
const bar = useMemo(() => formatB(b), [b]);
return <Baz foo={foo} bar={bar} />;
};

View File

@@ -1,46 +0,0 @@
## Input
```javascript
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};
```
## Error
```
Found 1 error:
Invariant: Expected all references to a variable to be consistently local or context references
Identifier <unknown> err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable.
error.bug-invariant-local-or-context-references.ts:15:13
13 | setState(_prevState => ({
14 | loading: false,
> 15 | error: err,
| ^^^ Expected all references to a variable to be consistently local or context references
16 | }));
17 | }
18 | };
```

View File

@@ -1,18 +0,0 @@
import {useState} from 'react';
import {bar} from './bar';
export const useFoot = () => {
const [, setState] = useState(null);
try {
const {data} = bar();
setState({
data,
error: null,
});
} catch (err) {
setState(_prevState => ({
loading: false,
error: err,
}));
}
};

View File

@@ -1,34 +0,0 @@
## Input
```javascript
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};
```
## Error
```
Found 1 error:
Invariant: Unexpected terminal in optional
error.bug-invariant-unexpected-terminal-in-optional.ts:3:16
1 | const Foo = ({json}) => {
2 | try {
> 3 | const foo = JSON.parse(json)?.foo;
| ^^^^ Unexpected terminal in optional
4 | return <span>{foo}</span>;
5 | } catch {
6 | return null;
```

View File

@@ -1,8 +0,0 @@
const Foo = ({json}) => {
try {
const foo = JSON.parse(json)?.foo;
return <span>{foo}</span>;
} catch {
return null;
}
};

View File

@@ -1,30 +0,0 @@
## Input
```javascript
import Bar from './Bar';
export function Foo() {
return (
<Bar
renderer={(...props) => {
return <span {...props}>{displayValue}</span>;
}}
/>
);
}
```
## Error
```
Found 1 error:
Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass
identifier 15 is unnamed.
```

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