Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e64b889dd3 |
@@ -579,7 +579,6 @@ module.exports = {
|
||||
JSONValue: 'readonly',
|
||||
JSResourceReference: 'readonly',
|
||||
MouseEventHandler: 'readonly',
|
||||
NavigateEvent: 'readonly',
|
||||
PropagationPhases: 'readonly',
|
||||
PropertyDescriptor: 'readonly',
|
||||
React$AbstractComponent: 'readonly',
|
||||
@@ -635,6 +634,5 @@ module.exports = {
|
||||
AsyncLocalStorage: 'readonly',
|
||||
async_hooks: 'readonly',
|
||||
globalThis: 'readonly',
|
||||
navigation: 'readonly',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
@@ -332,10 +332,10 @@ jobs:
|
||||
git --no-pager diff -U0 --cached | grep '^[+-]' | head -n 100
|
||||
echo "===================="
|
||||
# Ignore REVISION or lines removing @generated headers.
|
||||
if git diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
if git diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" > /dev/null; then
|
||||
echo "Changes detected"
|
||||
echo "===== Changes ====="
|
||||
git --no-pager diff --cached ':(exclude)*REVISION' ':(exclude)*/eslint-plugin-react-hooks/package.json' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
|
||||
git --no-pager diff --cached ':(exclude)*REVISION' | grep -vE "^(@@|diff|index|\-\-\-|\+\+\+|\- \* @generated SignedSource)" | grep "^[+-]" | head -n 50
|
||||
echo "==================="
|
||||
echo "should_commit=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
||||
1
.github/workflows/runtime_discord_notify.yml
vendored
1
.github/workflows/runtime_discord_notify.yml
vendored
@@ -15,7 +15,6 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
@@ -17,7 +17,6 @@ jobs:
|
||||
outputs:
|
||||
is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }}
|
||||
steps:
|
||||
- run: echo ${{ github.event.pull_request.author_association }}
|
||||
- name: Check is member or collaborator
|
||||
id: check_is_member_or_collaborator
|
||||
if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
## 19.1.0-rc.2 (May 14, 2025)
|
||||
|
||||
## babel-plugin-react-compiler
|
||||
|
||||
* Fix for string attribute values with emoji [#33096](https://github.com/facebook/react/pull/33096) by [@josephsavona](https://github.com/josephsavona)
|
||||
|
||||
## 19.1.0-rc.1 (April 21, 2025)
|
||||
|
||||
## eslint-plugin-react-hooks
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { c as _c } from "react/compiler-runtime"; //
|
||||
@compilationMode:"all"
|
||||
@compilationMode(all)
|
||||
function nonReactFn() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function useFoo(propVal: {+baz: number}) {
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-infer',
|
||||
input: `// @compilationMode:"infer"
|
||||
input: `// @compilationMode(infer)
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
@@ -101,7 +101,7 @@ function nonReactFn() {
|
||||
},
|
||||
{
|
||||
name: 'compilationMode-all',
|
||||
input: `// @compilationMode:"all"
|
||||
input: `// @compilationMode(all)
|
||||
function nonReactFn() {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -115,14 +115,6 @@ export class CompilerErrorDetail {
|
||||
export class CompilerError extends Error {
|
||||
details: Array<CompilerErrorDetail> = [];
|
||||
|
||||
static from(details: Array<CompilerErrorDetailOptions>): CompilerError {
|
||||
const error = new CompilerError();
|
||||
for (const detail of details) {
|
||||
error.push(detail);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
static invariant(
|
||||
condition: unknown,
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
|
||||
@@ -18,9 +18,8 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {ExternalFunction, isHookName} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {LoggerEvent, PluginOptions} from './Options';
|
||||
import {BabelFn, getReactCompilerRuntimeModule} from './Program';
|
||||
import {SuppressionRange} from './Suppression';
|
||||
import {CompilerReactTarget} from './Options';
|
||||
import {getReactCompilerRuntimeModule} from './Program';
|
||||
|
||||
export function validateRestrictedImports(
|
||||
path: NodePath<t.Program>,
|
||||
@@ -53,65 +52,32 @@ export function validateRestrictedImports(
|
||||
}
|
||||
}
|
||||
|
||||
type ProgramContextOptions = {
|
||||
program: NodePath<t.Program>;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
};
|
||||
export class ProgramContext {
|
||||
/**
|
||||
* Program and environment context
|
||||
*/
|
||||
/* Program and environment context */
|
||||
scope: BabelScope;
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
code: string | null;
|
||||
reactRuntimeModule: string;
|
||||
suppressions: Array<SuppressionRange>;
|
||||
hasModuleScopeOptOut: boolean;
|
||||
hookPattern: string | null;
|
||||
|
||||
/*
|
||||
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
|
||||
* consistently respect the `skip()` function to avoid revisiting a node within
|
||||
* a pass, so we use this set to track nodes that we have compiled.
|
||||
*/
|
||||
alreadyCompiled: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
|
||||
// known generated or referenced identifiers in the program
|
||||
knownReferencedNames: Set<string> = new Set();
|
||||
// generated imports
|
||||
imports: Map<string, Map<string, NonLocalImportSpecifier>> = new Map();
|
||||
|
||||
/**
|
||||
* Metadata from compilation
|
||||
*/
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
inferredEffectLocations: Set<t.SourceLocation> = new Set();
|
||||
|
||||
constructor({
|
||||
program,
|
||||
suppressions,
|
||||
opts,
|
||||
filename,
|
||||
code,
|
||||
hasModuleScopeOptOut,
|
||||
}: ProgramContextOptions) {
|
||||
constructor(
|
||||
program: NodePath<t.Program>,
|
||||
reactRuntimeModule: CompilerReactTarget,
|
||||
hookPattern: string | null,
|
||||
) {
|
||||
this.hookPattern = hookPattern;
|
||||
this.scope = program.scope;
|
||||
this.opts = opts;
|
||||
this.filename = filename;
|
||||
this.code = code;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(opts.target);
|
||||
this.suppressions = suppressions;
|
||||
this.hasModuleScopeOptOut = hasModuleScopeOptOut;
|
||||
this.reactRuntimeModule = getReactCompilerRuntimeModule(reactRuntimeModule);
|
||||
}
|
||||
|
||||
isHookName(name: string): boolean {
|
||||
if (this.opts.environment.hookPattern == null) {
|
||||
if (this.hookPattern == null) {
|
||||
return isHookName(name);
|
||||
} else {
|
||||
const match = new RegExp(this.opts.environment.hookPattern).exec(name);
|
||||
const match = new RegExp(this.hookPattern).exec(name);
|
||||
return (
|
||||
match != null && typeof match[1] === 'string' && isHookName(match[1])
|
||||
);
|
||||
@@ -213,12 +179,6 @@ export class ProgramContext {
|
||||
});
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
logEvent(event: LoggerEvent): void {
|
||||
if (this.opts.logger != null) {
|
||||
this.opts.logger.logEvent(this.filename, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExistingImports(
|
||||
|
||||
@@ -98,7 +98,7 @@ export type PluginOptions = {
|
||||
* provided rules will skip compilation. To disable this feature (never bailout of compilation
|
||||
* even if the default ESLint is suppressed), pass an empty array.
|
||||
*/
|
||||
eslintSuppressionRules: Array<string> | null | undefined;
|
||||
eslintSuppressionRules?: Array<string> | null | undefined;
|
||||
|
||||
flowSuppressions: boolean;
|
||||
/*
|
||||
@@ -106,7 +106,7 @@ export type PluginOptions = {
|
||||
*/
|
||||
ignoreUseNoForget: boolean;
|
||||
|
||||
sources: Array<string> | ((filename: string) => boolean) | null;
|
||||
sources?: Array<string> | ((filename: string) => boolean) | null;
|
||||
|
||||
/**
|
||||
* The compiler has customized support for react-native-reanimated, intended as a temporary workaround.
|
||||
|
||||
@@ -104,8 +104,6 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF
|
||||
import {CompilerError} from '..';
|
||||
import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
|
||||
import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
|
||||
import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
|
||||
import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
|
||||
|
||||
export type CompilerPipelineValue =
|
||||
| {kind: 'ast'; name: string; value: CodegenFunction}
|
||||
@@ -228,27 +226,15 @@ function runWithEnvironment(
|
||||
analyseFunctions(hir);
|
||||
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
|
||||
|
||||
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();
|
||||
}
|
||||
const fnEffectErrors = inferReferenceEffects(hir);
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (fnEffectErrors.length > 0) {
|
||||
CompilerError.throw(fnEffectErrors[0]);
|
||||
}
|
||||
}
|
||||
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
|
||||
// Note: Has to come after infer reference effects because "dead" code may still affect inference
|
||||
deadCodeElimination(hir);
|
||||
@@ -262,21 +248,8 @@ function runWithEnvironment(
|
||||
pruneMaybeThrows(hir);
|
||||
log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
|
||||
|
||||
if (!env.config.enableNewMutationAliasingModel) {
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
} else {
|
||||
const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
|
||||
isFunctionExpression: false,
|
||||
});
|
||||
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (mutabilityAliasingErrors.isErr()) {
|
||||
throw mutabilityAliasingErrors.unwrapErr();
|
||||
}
|
||||
validateLocalsNotReassignedAfterRender(hir);
|
||||
}
|
||||
}
|
||||
inferMutableRanges(hir);
|
||||
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
|
||||
|
||||
if (env.isInferredMemoEnabled) {
|
||||
if (env.config.assertValidMutableRanges) {
|
||||
@@ -303,10 +276,7 @@ function runWithEnvironment(
|
||||
validateNoImpureFunctionsInRender(hir).unwrap();
|
||||
}
|
||||
|
||||
if (
|
||||
env.config.validateNoFreezingKnownMutableFunctions ||
|
||||
env.config.enableNewMutationAliasingModel
|
||||
) {
|
||||
if (env.config.validateNoFreezingKnownMutableFunctions) {
|
||||
validateNoFreezingKnownMutableFunctions(hir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
CompilerErrorDetail,
|
||||
ErrorSeverity,
|
||||
} from '../CompilerError';
|
||||
import {ReactFunctionType} from '../HIR/Environment';
|
||||
import {EnvironmentConfig, ReactFunctionType} from '../HIR/Environment';
|
||||
import {CodegenFunction} from '../ReactiveScopes';
|
||||
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
|
||||
import {isHookDeclaration} from '../Utils/HookDeclaration';
|
||||
@@ -43,21 +43,17 @@ export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
|
||||
|
||||
export function findDirectiveEnablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): t.Directive | null {
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_IN_DIRECTIVES.has(directive.value.value),
|
||||
);
|
||||
}
|
||||
|
||||
export function findDirectiveDisablingMemoization(
|
||||
directives: Array<t.Directive>,
|
||||
): t.Directive | null {
|
||||
return (
|
||||
directives.find(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
) ?? null
|
||||
): Array<t.Directive> {
|
||||
return directives.filter(directive =>
|
||||
OPT_OUT_DIRECTIVES.has(directive.value.value),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,16 +88,13 @@ export type CompileResult = {
|
||||
|
||||
function logError(
|
||||
err: unknown,
|
||||
context: {
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
},
|
||||
pass: CompilerPass,
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
if (context.opts.logger) {
|
||||
if (pass.opts.logger) {
|
||||
if (err instanceof CompilerError) {
|
||||
for (const detail of err.details) {
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
pass.opts.logger.logEvent(pass.filename, {
|
||||
kind: 'CompileError',
|
||||
fnLoc,
|
||||
detail: detail.options,
|
||||
@@ -115,7 +108,7 @@ function logError(
|
||||
stringifiedError = err?.toString() ?? '[ null ]';
|
||||
}
|
||||
|
||||
context.opts.logger.logEvent(context.filename, {
|
||||
pass.opts.logger.logEvent(pass.filename, {
|
||||
kind: 'PipelineError',
|
||||
fnLoc,
|
||||
data: stringifiedError,
|
||||
@@ -125,17 +118,13 @@ function logError(
|
||||
}
|
||||
function handleError(
|
||||
err: unknown,
|
||||
context: {
|
||||
opts: PluginOptions;
|
||||
filename: string | null;
|
||||
},
|
||||
pass: CompilerPass,
|
||||
fnLoc: t.SourceLocation | null,
|
||||
): void {
|
||||
logError(err, context, fnLoc);
|
||||
logError(err, pass, fnLoc);
|
||||
if (
|
||||
context.opts.panicThreshold === 'all_errors' ||
|
||||
(context.opts.panicThreshold === 'critical_errors' &&
|
||||
isCriticalError(err)) ||
|
||||
pass.opts.panicThreshold === 'all_errors' ||
|
||||
(pass.opts.panicThreshold === 'critical_errors' && isCriticalError(err)) ||
|
||||
isConfigError(err) // Always throws regardless of panic threshold
|
||||
) {
|
||||
throw err;
|
||||
@@ -198,6 +187,7 @@ export function createNewFunctionNode(
|
||||
}
|
||||
}
|
||||
// Avoid visiting the new transformed version
|
||||
ALREADY_COMPILED.add(transformedFn);
|
||||
return transformedFn;
|
||||
}
|
||||
|
||||
@@ -249,6 +239,13 @@ function insertNewOutlinedFunctionNode(
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This is a hack to work around what seems to be a Babel bug. Babel doesn't
|
||||
* consistently respect the `skip()` function to avoid revisiting a node within
|
||||
* a pass, so we use this set to track nodes that we have compiled.
|
||||
*/
|
||||
const ALREADY_COMPILED: WeakSet<object> | Set<object> = new (WeakSet ?? Set)();
|
||||
|
||||
const DEFAULT_ESLINT_SUPPRESSIONS = [
|
||||
'react-hooks/exhaustive-deps',
|
||||
'react-hooks/rules-of-hooks',
|
||||
@@ -271,43 +268,41 @@ function isFilePartOfSources(
|
||||
return false;
|
||||
}
|
||||
|
||||
export type CompileProgramMetadata = {
|
||||
export type CompileProgramResult = {
|
||||
retryErrors: Array<{fn: BabelFn; error: CompilerError}>;
|
||||
inferredEffectLocations: Set<t.SourceLocation>;
|
||||
};
|
||||
/**
|
||||
* Main entrypoint for React Compiler.
|
||||
*
|
||||
* @param program The Babel program node to compile
|
||||
* @param pass Compiler configuration and context
|
||||
* @returns Compilation results or null if compilation was skipped
|
||||
* `compileProgram` is directly invoked by the react-compiler babel plugin, so
|
||||
* exceptions thrown by this function will fail the babel build.
|
||||
* - call `handleError` if your error is recoverable.
|
||||
* Unless the error is a warning / info diagnostic, compilation of a function
|
||||
* / entire file should also be skipped.
|
||||
* - throw an exception if the error is fatal / not recoverable.
|
||||
* Examples of this are invalid compiler configs or failure to codegen outlined
|
||||
* functions *after* already emitting optimized components / hooks that invoke
|
||||
* the outlined functions.
|
||||
*/
|
||||
export function compileProgram(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
): CompileProgramMetadata | null {
|
||||
/**
|
||||
* This is directly invoked by the react-compiler babel plugin, so exceptions
|
||||
* thrown by this function will fail the babel build.
|
||||
* - call `handleError` if your error is recoverable.
|
||||
* Unless the error is a warning / info diagnostic, compilation of a function
|
||||
* / entire file should also be skipped.
|
||||
* - throw an exception if the error is fatal / not recoverable.
|
||||
* Examples of this are invalid compiler configs or failure to codegen outlined
|
||||
* functions *after* already emitting optimized components / hooks that invoke
|
||||
* the outlined functions.
|
||||
*/
|
||||
): CompileProgramResult | null {
|
||||
if (shouldSkipCompilation(program, pass)) {
|
||||
return null;
|
||||
}
|
||||
const restrictedImportsErr = validateRestrictedImports(
|
||||
program,
|
||||
pass.opts.environment,
|
||||
);
|
||||
|
||||
const environment = pass.opts.environment;
|
||||
const restrictedImportsErr = validateRestrictedImports(program, environment);
|
||||
if (restrictedImportsErr) {
|
||||
handleError(restrictedImportsErr, pass, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
const programContext = new ProgramContext(
|
||||
program,
|
||||
pass.opts.target,
|
||||
environment.hookPattern,
|
||||
);
|
||||
/*
|
||||
* Record lint errors and critical errors as depending on Forget's config,
|
||||
* we may still need to run Forget's analysis on every function (even if we
|
||||
@@ -318,102 +313,16 @@ export function compileProgram(
|
||||
pass.opts.eslintSuppressionRules ?? DEFAULT_ESLINT_SUPPRESSIONS,
|
||||
pass.opts.flowSuppressions,
|
||||
);
|
||||
|
||||
const programContext = new ProgramContext({
|
||||
program: program,
|
||||
opts: pass.opts,
|
||||
filename: pass.filename,
|
||||
code: pass.code,
|
||||
suppressions,
|
||||
hasModuleScopeOptOut:
|
||||
findDirectiveDisablingMemoization(program.node.directives) != null,
|
||||
});
|
||||
|
||||
const queue: Array<CompileSource> = findFunctionsToCompile(
|
||||
program,
|
||||
pass,
|
||||
programContext,
|
||||
);
|
||||
const queue: Array<{
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
fnType: ReactFunctionType;
|
||||
}> = [];
|
||||
const compiledFns: Array<CompileResult> = [];
|
||||
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType, programContext);
|
||||
|
||||
if (compiled != null) {
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
current.fn,
|
||||
outlined.fn,
|
||||
);
|
||||
fn.skip();
|
||||
programContext.alreadyCompiled.add(fn.node);
|
||||
if (outlined.type !== null) {
|
||||
queue.push({
|
||||
kind: 'outlined',
|
||||
fn,
|
||||
fnType: outlined.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
compiledFns.push({
|
||||
kind: current.kind,
|
||||
originalFn: current.fn,
|
||||
compiledFn: compiled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid modifying the program if we find a program level opt-out
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
if (compiledFns.length > 0) {
|
||||
const error = new CompilerError();
|
||||
error.pushErrorDetail(
|
||||
new CompilerErrorDetail({
|
||||
reason:
|
||||
'Unexpected compiled functions when module scope opt-out is present',
|
||||
severity: ErrorSeverity.Invariant,
|
||||
loc: null,
|
||||
}),
|
||||
);
|
||||
handleError(error, programContext, null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Insert React Compiler generated functions into the Babel AST
|
||||
applyCompiledFunctions(program, compiledFns, pass, programContext);
|
||||
|
||||
return {
|
||||
retryErrors: programContext.retryErrors,
|
||||
inferredEffectLocations: programContext.inferredEffectLocations,
|
||||
};
|
||||
}
|
||||
|
||||
type CompileSource = {
|
||||
kind: 'original' | 'outlined';
|
||||
fn: BabelFn;
|
||||
fnType: ReactFunctionType;
|
||||
};
|
||||
/**
|
||||
* Find all React components and hooks that need to be compiled
|
||||
*
|
||||
* @returns An array of React functions from @param program to transform
|
||||
*/
|
||||
function findFunctionsToCompile(
|
||||
program: NodePath<t.Program>,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): Array<CompileSource> {
|
||||
const queue: Array<CompileSource> = [];
|
||||
const traverseFunction = (fn: BabelFn, pass: CompilerPass): void => {
|
||||
const fnType = getReactFunctionType(fn, pass);
|
||||
if (fnType === null || programContext.alreadyCompiled.has(fn.node)) {
|
||||
const fnType = getReactFunctionType(fn, pass, environment);
|
||||
if (fnType === null || ALREADY_COMPILED.has(fn.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,7 +331,7 @@ function findFunctionsToCompile(
|
||||
* traversal will loop infinitely.
|
||||
* Ensure we avoid visiting the original function again.
|
||||
*/
|
||||
programContext.alreadyCompiled.add(fn.node);
|
||||
ALREADY_COMPILED.add(fn.node);
|
||||
fn.skip();
|
||||
|
||||
queue.push({kind: 'original', fn, fnType});
|
||||
@@ -437,6 +346,7 @@ function findFunctionsToCompile(
|
||||
* can reference `this` which is unsafe for compilation
|
||||
*/
|
||||
node.skip();
|
||||
return;
|
||||
},
|
||||
|
||||
ClassExpression(node: NodePath<t.ClassExpression>) {
|
||||
@@ -445,6 +355,7 @@ function findFunctionsToCompile(
|
||||
* can reference `this` which is unsafe for compilation
|
||||
*/
|
||||
node.skip();
|
||||
return;
|
||||
},
|
||||
|
||||
FunctionDeclaration: traverseFunction,
|
||||
@@ -459,206 +370,205 @@ function findFunctionsToCompile(
|
||||
filename: pass.filename ?? null,
|
||||
},
|
||||
);
|
||||
return queue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to compile a source function, taking into account all local suppressions,
|
||||
* opt-ins, and opt-outs.
|
||||
*
|
||||
* Errors encountered during compilation are either logged (if recoverable) or
|
||||
* thrown (if non-recoverable).
|
||||
*
|
||||
* @returns the compiled function or null if the function was skipped (due to
|
||||
* config settings and/or outputs)
|
||||
*/
|
||||
function processFn(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): null | CodegenFunction {
|
||||
let directives;
|
||||
if (fn.node.body.type !== 'BlockStatement') {
|
||||
directives = {optIn: null, optOut: null};
|
||||
} else {
|
||||
directives = {
|
||||
optIn: findDirectiveEnablingMemoization(fn.node.body.directives),
|
||||
optOut: findDirectiveDisablingMemoization(fn.node.body.directives),
|
||||
};
|
||||
}
|
||||
|
||||
let compiledFn: CodegenFunction;
|
||||
const compileResult = tryCompileFunction(fn, fnType, programContext);
|
||||
if (compileResult.kind === 'error') {
|
||||
if (directives.optOut != null) {
|
||||
logError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
} else {
|
||||
handleError(compileResult.error, programContext, fn.node.loc ?? null);
|
||||
const retryErrors: Array<{fn: BabelFn; error: CompilerError}> = [];
|
||||
const inferredEffectLocations = new Set<t.SourceLocation>();
|
||||
const processFn = (
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
): null | CodegenFunction => {
|
||||
let optInDirectives: Array<t.Directive> = [];
|
||||
let optOutDirectives: Array<t.Directive> = [];
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
optInDirectives = findDirectiveEnablingMemoization(
|
||||
fn.node.body.directives,
|
||||
);
|
||||
optOutDirectives = findDirectiveDisablingMemoization(
|
||||
fn.node.body.directives,
|
||||
);
|
||||
}
|
||||
const retryResult = retryCompileFunction(fn, fnType, programContext);
|
||||
if (retryResult == null) {
|
||||
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
suppressions,
|
||||
fn,
|
||||
);
|
||||
let compileResult:
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown};
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
compileResult = {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
compileResult = {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
compileResult = {kind: 'error', error: err};
|
||||
}
|
||||
}
|
||||
|
||||
if (compileResult.kind === 'error') {
|
||||
/**
|
||||
* If an opt out directive is present, log only instead of throwing and don't mark as
|
||||
* containing a critical error.
|
||||
*/
|
||||
if (optOutDirectives.length > 0) {
|
||||
logError(compileResult.error, pass, fn.node.loc ?? null);
|
||||
} else {
|
||||
handleError(compileResult.error, pass, fn.node.loc ?? null);
|
||||
}
|
||||
// If non-memoization features are enabled, retry regardless of error kind
|
||||
if (
|
||||
!(environment.enableFire || environment.inferEffectDependencies != null)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
compileResult = {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
programContext,
|
||||
pass.opts.logger,
|
||||
pass.filename,
|
||||
pass.code,
|
||||
),
|
||||
};
|
||||
if (
|
||||
!compileResult.compiledFn.hasFireRewrite &&
|
||||
!compileResult.compiledFn.hasInferredEffect
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: we might want to log error here, but this will also result in duplicate logging
|
||||
if (err instanceof CompilerError) {
|
||||
retryErrors.push({fn, error: err});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Otherwise if 'use no forget/memo' is present, we still run the code through the compiler
|
||||
* for validation but we don't mutate the babel AST. This allows us to flag if there is an
|
||||
* unused 'use no forget/memo' directive.
|
||||
*/
|
||||
if (pass.opts.ignoreUseNoForget === false && optOutDirectives.length > 0) {
|
||||
for (const directive of optOutDirectives) {
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directive.value.value}' directive.`,
|
||||
loc: directive.loc ?? null,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
compiledFn = retryResult;
|
||||
} else {
|
||||
compiledFn = compileResult.compiledFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* If 'use no forget/memo' is present and we still ran the code through the
|
||||
* compiler for validation, log a skip event and don't mutate the babel AST.
|
||||
* This allows us to flag if there is an unused 'use no forget/memo'
|
||||
* directive.
|
||||
*/
|
||||
if (
|
||||
programContext.opts.ignoreUseNoForget === false &&
|
||||
directives.optOut != null
|
||||
) {
|
||||
programContext.logEvent({
|
||||
kind: 'CompileSkip',
|
||||
fnLoc: fn.node.body.loc ?? null,
|
||||
reason: `Skipped due to '${directives.optOut.value}' directive.`,
|
||||
loc: directives.optOut.loc ?? null,
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
fnName: compileResult.compiledFn.id?.name ?? null,
|
||||
memoSlots: compileResult.compiledFn.memoSlotsUsed,
|
||||
memoBlocks: compileResult.compiledFn.memoBlocks,
|
||||
memoValues: compileResult.compiledFn.memoValues,
|
||||
prunedMemoBlocks: compileResult.compiledFn.prunedMemoBlocks,
|
||||
prunedMemoValues: compileResult.compiledFn.prunedMemoValues,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
programContext.logEvent({
|
||||
kind: 'CompileSuccess',
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
fnName: compiledFn.id?.name ?? null,
|
||||
memoSlots: compiledFn.memoSlotsUsed,
|
||||
memoBlocks: compiledFn.memoBlocks,
|
||||
memoValues: compiledFn.memoValues,
|
||||
prunedMemoBlocks: compiledFn.prunedMemoBlocks,
|
||||
prunedMemoValues: compiledFn.prunedMemoValues,
|
||||
});
|
||||
|
||||
if (programContext.hasModuleScopeOptOut) {
|
||||
return null;
|
||||
} else if (programContext.opts.noEmit) {
|
||||
/**
|
||||
* Always compile functions with opt in directives.
|
||||
*/
|
||||
if (optInDirectives.length > 0) {
|
||||
return compileResult.compiledFn;
|
||||
} else if (pass.opts.compilationMode === 'annotation') {
|
||||
/**
|
||||
* No opt-in directive in annotation mode, so don't insert the compiled function.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pass.opts.noEmit) {
|
||||
return compileResult.compiledFn;
|
||||
}
|
||||
/**
|
||||
* inferEffectDependencies + noEmit is currently only used for linting. In
|
||||
* this mode, add source locations for where the compiler *can* infer effect
|
||||
* dependencies.
|
||||
*/
|
||||
for (const loc of compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) {
|
||||
programContext.inferredEffectLocations.add(loc);
|
||||
for (const loc of compileResult.compiledFn.inferredEffectLocations) {
|
||||
if (loc !== GeneratedSource) inferredEffectLocations.add(loc);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.shift()!;
|
||||
const compiled = processFn(current.fn, current.fnType);
|
||||
if (compiled === null) {
|
||||
continue;
|
||||
}
|
||||
for (const outlined of compiled.outlined) {
|
||||
CompilerError.invariant(outlined.fn.outlined.length === 0, {
|
||||
reason: 'Unexpected nested outlined functions',
|
||||
loc: outlined.fn.loc,
|
||||
});
|
||||
const fn = insertNewOutlinedFunctionNode(
|
||||
program,
|
||||
current.fn,
|
||||
outlined.fn,
|
||||
);
|
||||
fn.skip();
|
||||
ALREADY_COMPILED.add(fn.node);
|
||||
if (outlined.type !== null) {
|
||||
queue.push({
|
||||
kind: 'outlined',
|
||||
fn,
|
||||
fnType: outlined.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else if (
|
||||
programContext.opts.compilationMode === 'annotation' &&
|
||||
directives.optIn == null
|
||||
) {
|
||||
/**
|
||||
* If no opt-in directive is found and the compiler is configured in
|
||||
* annotation mode, don't insert the compiled function.
|
||||
*/
|
||||
return null;
|
||||
} else {
|
||||
return compiledFn;
|
||||
compiledFns.push({
|
||||
kind: current.kind,
|
||||
compiledFn: compiled,
|
||||
originalFn: current.fn,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function tryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
):
|
||||
| {kind: 'compile'; compiledFn: CodegenFunction}
|
||||
| {kind: 'error'; error: unknown} {
|
||||
/**
|
||||
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
|
||||
* Program node itself. We need to figure out whether an eslint suppression range
|
||||
* applies to this function first.
|
||||
* Do not modify source if there is a module scope level opt out directive.
|
||||
*/
|
||||
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
|
||||
programContext.suppressions,
|
||||
fn,
|
||||
const moduleScopeOptOutDirectives = findDirectiveDisablingMemoization(
|
||||
program.node.directives,
|
||||
);
|
||||
if (suppressionsInFunction.length > 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
error: suppressionsToCompilerError(suppressionsInFunction),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
kind: 'compile',
|
||||
compiledFn: compileFn(
|
||||
fn,
|
||||
programContext.opts.environment,
|
||||
fnType,
|
||||
'all_features',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
),
|
||||
};
|
||||
} catch (err) {
|
||||
return {kind: 'error', error: err};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If non-memo feature flags are enabled, retry compilation with a more minimal
|
||||
* feature set.
|
||||
*
|
||||
* @returns a CodegenFunction if retry was successful
|
||||
*/
|
||||
function retryCompileFunction(
|
||||
fn: BabelFn,
|
||||
fnType: ReactFunctionType,
|
||||
programContext: ProgramContext,
|
||||
): CodegenFunction | null {
|
||||
const environment = programContext.opts.environment;
|
||||
if (
|
||||
!(environment.enableFire || environment.inferEffectDependencies != null)
|
||||
) {
|
||||
if (moduleScopeOptOutDirectives.length > 0) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Note that function suppressions are not checked in the retry pipeline, as
|
||||
* they only affect auto-memoization features.
|
||||
/*
|
||||
* Only insert Forget-ified functions if we have not encountered a critical
|
||||
* error elsewhere in the file, regardless of bailout mode.
|
||||
*/
|
||||
try {
|
||||
const retryResult = compileFn(
|
||||
fn,
|
||||
environment,
|
||||
fnType,
|
||||
'no_inferred_memo',
|
||||
programContext,
|
||||
programContext.opts.logger,
|
||||
programContext.filename,
|
||||
programContext.code,
|
||||
);
|
||||
|
||||
if (!retryResult.hasFireRewrite && !retryResult.hasInferredEffect) {
|
||||
return null;
|
||||
}
|
||||
return retryResult;
|
||||
} catch (err) {
|
||||
// TODO: we might want to log error here, but this will also result in duplicate logging
|
||||
if (err instanceof CompilerError) {
|
||||
programContext.retryErrors.push({fn, error: err});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies React Compiler generated functions to the babel AST by replacing
|
||||
* existing functions in place or inserting new declarations.
|
||||
*/
|
||||
function applyCompiledFunctions(
|
||||
program: NodePath<t.Program>,
|
||||
compiledFns: Array<CompileResult>,
|
||||
pass: CompilerPass,
|
||||
programContext: ProgramContext,
|
||||
): void {
|
||||
const referencedBeforeDeclared =
|
||||
pass.opts.gating != null
|
||||
? getFunctionReferencedBeforeDeclarationAtTopLevel(program, compiledFns)
|
||||
@@ -666,7 +576,6 @@ function applyCompiledFunctions(
|
||||
for (const result of compiledFns) {
|
||||
const {kind, originalFn, compiledFn} = result;
|
||||
const transformedFn = createNewFunctionNode(originalFn, compiledFn);
|
||||
programContext.alreadyCompiled.add(transformedFn);
|
||||
|
||||
if (referencedBeforeDeclared != null && kind === 'original') {
|
||||
CompilerError.invariant(pass.opts.gating != null, {
|
||||
@@ -689,6 +598,7 @@ function applyCompiledFunctions(
|
||||
if (compiledFns.length > 0) {
|
||||
addImportsToProgram(program, programContext);
|
||||
}
|
||||
return {retryErrors, inferredEffectLocations};
|
||||
}
|
||||
|
||||
function shouldSkipCompilation(
|
||||
@@ -730,10 +640,14 @@ function shouldSkipCompilation(
|
||||
function getReactFunctionType(
|
||||
fn: BabelFn,
|
||||
pass: CompilerPass,
|
||||
/**
|
||||
* TODO(mofeiZ): remove once we validate PluginOptions with Zod
|
||||
*/
|
||||
environment: EnvironmentConfig,
|
||||
): ReactFunctionType | null {
|
||||
const hookPattern = pass.opts.environment.hookPattern;
|
||||
const hookPattern = environment.hookPattern;
|
||||
if (fn.node.body.type === 'BlockStatement') {
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives) != null)
|
||||
if (findDirectiveEnablingMemoization(fn.node.body.directives).length > 0)
|
||||
return getComponentOrHookLike(fn, hookPattern) ?? 'Other';
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
import {Environment} from '../HIR';
|
||||
import {DEFAULT_EXPORT} from '../HIR/Environment';
|
||||
import {CompileProgramMetadata} from './Program';
|
||||
import {CompileProgramResult} from './Program';
|
||||
|
||||
function throwInvalidReact(
|
||||
options: Omit<CompilerErrorDetailOptions, 'severity'>,
|
||||
@@ -109,7 +109,7 @@ export default function validateNoUntransformedReferences(
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
env: EnvironmentConfig,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
compileResult: CompileProgramResult | null,
|
||||
): void {
|
||||
const moduleLoadChecks = new Map<
|
||||
string,
|
||||
@@ -236,7 +236,7 @@ function transformProgram(
|
||||
moduleLoadChecks: Map<string, Map<string, CheckInvalidReferenceFn>>,
|
||||
filename: string | null,
|
||||
logger: Logger | null,
|
||||
compileResult: CompileProgramMetadata | null,
|
||||
compileResult: CompileProgramResult | null,
|
||||
): void {
|
||||
const traversalState: TraversalState = {
|
||||
shouldInvalidateScopes: true,
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, MutableRange, Place} from './HIR';
|
||||
import invariant from 'invariant';
|
||||
import {HIRFunction, Identifier, MutableRange} from './HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionOperand,
|
||||
eachTerminalOperand,
|
||||
} from './visitors';
|
||||
import {CompilerError} from '..';
|
||||
import {printPlace} from './PrintHIR';
|
||||
|
||||
/*
|
||||
* Checks that all mutable ranges in the function are well-formed, with
|
||||
@@ -21,43 +20,38 @@ import {printPlace} from './PrintHIR';
|
||||
export function assertValidMutableRanges(fn: HIRFunction): void {
|
||||
for (const [, block] of fn.body.blocks) {
|
||||
for (const phi of block.phis) {
|
||||
visit(phi.place, `phi for block bb${block.id}`);
|
||||
for (const [pred, operand] of phi.operands) {
|
||||
visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
|
||||
visitIdentifier(phi.place.identifier);
|
||||
for (const [, operand] of phi.operands) {
|
||||
visitIdentifier(operand.identifier);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const operand of eachInstructionLValue(instr)) {
|
||||
visit(operand, `instruction [${instr.id}]`);
|
||||
visitIdentifier(operand.identifier);
|
||||
}
|
||||
for (const operand of eachInstructionOperand(instr)) {
|
||||
visit(operand, `instruction [${instr.id}]`);
|
||||
visitIdentifier(operand.identifier);
|
||||
}
|
||||
}
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
visit(operand, `terminal [${block.terminal.id}]`);
|
||||
visitIdentifier(operand.identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function visit(place: Place, description: string): void {
|
||||
validateMutableRange(place, place.identifier.mutableRange, description);
|
||||
if (place.identifier.scope !== null) {
|
||||
validateMutableRange(place, place.identifier.scope.range, description);
|
||||
function visitIdentifier(identifier: Identifier): void {
|
||||
validateMutableRange(identifier.mutableRange);
|
||||
if (identifier.scope !== null) {
|
||||
validateMutableRange(identifier.scope.range);
|
||||
}
|
||||
}
|
||||
|
||||
function validateMutableRange(
|
||||
place: Place,
|
||||
range: MutableRange,
|
||||
description: string,
|
||||
): void {
|
||||
CompilerError.invariant(
|
||||
(range.start === 0 && range.end === 0) || range.end > range.start,
|
||||
{
|
||||
reason: `Invalid mutable range: [${range.start}:${range.end}]`,
|
||||
description: `${printPlace(place)} in ${description}`,
|
||||
loc: place.loc,
|
||||
},
|
||||
function validateMutableRange(mutableRange: MutableRange): void {
|
||||
invariant(
|
||||
(mutableRange.start === 0 && mutableRange.end === 0) ||
|
||||
mutableRange.end > mutableRange.start,
|
||||
'Identifier scope mutableRange was invalid: [%s:%s]',
|
||||
mutableRange.start,
|
||||
mutableRange.end,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
makeType,
|
||||
promoteTemporary,
|
||||
} from './HIR';
|
||||
import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
|
||||
import HIRBuilder, {Bindings} from './HIRBuilder';
|
||||
import {BuiltInArrayId} from './ObjectShape';
|
||||
|
||||
/*
|
||||
@@ -179,7 +179,6 @@ export function lower(
|
||||
loc: GeneratedSource,
|
||||
value: lowerExpressionToTemporary(builder, body),
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
};
|
||||
builder.terminateWithContinuation(terminal, fallthrough);
|
||||
} else if (body.isBlockStatement()) {
|
||||
@@ -209,7 +208,6 @@ export function lower(
|
||||
loc: GeneratedSource,
|
||||
}),
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -220,7 +218,6 @@ export function lower(
|
||||
fnType: parent == null ? env.fnType : 'Other',
|
||||
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
|
||||
body: builder.build(),
|
||||
context,
|
||||
generator: func.node.generator === true,
|
||||
@@ -228,7 +225,6 @@ export function lower(
|
||||
loc: func.node.loc ?? GeneratedSource,
|
||||
env,
|
||||
effects: null,
|
||||
aliasingEffects: null,
|
||||
directives,
|
||||
});
|
||||
}
|
||||
@@ -289,7 +285,6 @@ function lowerStatement(
|
||||
loc: stmt.node.loc ?? GeneratedSource,
|
||||
value,
|
||||
id: makeInstructionId(0),
|
||||
effects: null,
|
||||
};
|
||||
builder.terminate(terminal, 'block');
|
||||
return;
|
||||
@@ -1240,7 +1235,6 @@ function lowerStatement(
|
||||
kind: 'Debugger',
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
return;
|
||||
@@ -1898,7 +1892,6 @@ function lowerExpression(
|
||||
place: leftValue,
|
||||
loc: exprLoc,
|
||||
},
|
||||
effects: null,
|
||||
loc: exprLoc,
|
||||
});
|
||||
builder.terminateWithContinuation(
|
||||
@@ -2834,7 +2827,6 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
@@ -2848,7 +2840,6 @@ function lowerOptionalCallExpression(
|
||||
args,
|
||||
loc,
|
||||
},
|
||||
effects: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -3475,10 +3466,9 @@ function lowerValueToTemporary(
|
||||
const place: Place = buildTemporaryPlace(builder, value.loc);
|
||||
builder.push({
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...place},
|
||||
value: value,
|
||||
effects: null,
|
||||
loc: value.loc,
|
||||
lvalue: {...place},
|
||||
});
|
||||
return place;
|
||||
}
|
||||
|
||||
@@ -243,11 +243,6 @@ export const EnvironmentConfigSchema = z.object({
|
||||
*/
|
||||
enableUseTypeAnnotations: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enable a new model for mutability and aliasing inference
|
||||
*/
|
||||
enableNewMutationAliasingModel: z.boolean().default(false),
|
||||
|
||||
/**
|
||||
* Enables inference of optional dependency chains. Without this flag
|
||||
* a property chain such as `props?.items?.foo` will infer as a dep on
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BUILTIN_SHAPES,
|
||||
BuiltInArrayId,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
addFunction,
|
||||
addHook,
|
||||
addObject,
|
||||
signatureArgument,
|
||||
} from './ObjectShape';
|
||||
import {BuiltInType, ObjectType, PolyType} from './Types';
|
||||
import {TypeConfig} from './TypeSchema';
|
||||
@@ -643,41 +642,6 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
|
||||
calleeEffect: Effect.Read,
|
||||
hookKind: 'useEffect',
|
||||
returnValueKind: ValueKind.Frozen,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [signatureArgument(3)],
|
||||
effects: [
|
||||
// Freezes the function and deps
|
||||
{
|
||||
kind: 'Freeze',
|
||||
value: signatureArgument(1),
|
||||
reason: ValueReason.Effect,
|
||||
},
|
||||
// Internally creates an effect object that captures the function and deps
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(3),
|
||||
value: ValueKind.Frozen,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The effect stores the function and dependencies
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(3),
|
||||
},
|
||||
// Returns undefined
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
BuiltInUseEffectHookId,
|
||||
),
|
||||
|
||||
@@ -13,7 +13,6 @@ import {Environment, ReactFunctionType} from './Environment';
|
||||
import type {HookKind} from './ObjectShape';
|
||||
import {Type, makeType} from './Types';
|
||||
import {z} from 'zod';
|
||||
import {AliasingEffect} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
/*
|
||||
* *******************************************************************************************
|
||||
@@ -101,7 +100,6 @@ export type ReactiveInstruction = {
|
||||
id: InstructionId;
|
||||
lvalue: Place | null;
|
||||
value: ReactiveValue;
|
||||
effects?: Array<AliasingEffect> | null; // TODO make non-optional
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -280,14 +278,12 @@ export type HIRFunction = {
|
||||
params: Array<Place | SpreadPattern>;
|
||||
returnTypeAnnotation: t.FlowType | t.TSType | null;
|
||||
returnType: Type;
|
||||
returns: Place;
|
||||
context: Array<Place>;
|
||||
effects: Array<FunctionEffect> | null;
|
||||
body: HIR;
|
||||
generator: boolean;
|
||||
async: boolean;
|
||||
directives: Array<string>;
|
||||
aliasingEffects?: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type FunctionEffect =
|
||||
@@ -453,7 +449,6 @@ export type ReturnTerminal = {
|
||||
value: Place;
|
||||
id: InstructionId;
|
||||
fallthrough?: never;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type GotoTerminal = {
|
||||
@@ -614,7 +609,6 @@ export type MaybeThrowTerminal = {
|
||||
id: InstructionId;
|
||||
loc: SourceLocation;
|
||||
fallthrough?: never;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export type ReactiveScopeTerminal = {
|
||||
@@ -651,18 +645,12 @@ export type Instruction = {
|
||||
lvalue: Place;
|
||||
value: InstructionValue;
|
||||
loc: SourceLocation;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
};
|
||||
|
||||
export function todoPopulateAliasingEffects(): Array<AliasingEffect> | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export type TInstruction<T extends InstructionValue> = {
|
||||
id: InstructionId;
|
||||
lvalue: Place;
|
||||
value: T;
|
||||
effects: Array<AliasingEffect> | null;
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
@@ -1392,11 +1380,6 @@ export enum ValueReason {
|
||||
*/
|
||||
JsxCaptured = 'jsx-captured',
|
||||
|
||||
/**
|
||||
* Passed to an effect
|
||||
*/
|
||||
Effect = 'effect',
|
||||
|
||||
/**
|
||||
* Return value of a function with known frozen return value, e.g. `useState`.
|
||||
*/
|
||||
|
||||
@@ -165,7 +165,6 @@ export default class HIRBuilder {
|
||||
handler: exceptionHandler,
|
||||
id: makeInstructionId(0),
|
||||
loc: instruction.loc,
|
||||
effects: null,
|
||||
},
|
||||
continuationBlock,
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
GeneratedSource,
|
||||
HIRFunction,
|
||||
Instruction,
|
||||
Place,
|
||||
} from './HIR';
|
||||
import {markPredecessors} from './HIRBuilder';
|
||||
import {terminalFallthrough, terminalHasFallthrough} from './visitors';
|
||||
@@ -81,22 +80,20 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
|
||||
suggestions: null,
|
||||
});
|
||||
const operand = Array.from(phi.operands.values())[0]!;
|
||||
const lvalue: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: phi.place.identifier,
|
||||
effect: Effect.ConditionallyMutate,
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
const instr: Instruction = {
|
||||
id: predecessor.terminal.id,
|
||||
lvalue: {...lvalue},
|
||||
lvalue: {
|
||||
kind: 'Identifier',
|
||||
identifier: phi.place.identifier,
|
||||
effect: Effect.ConditionallyMutate,
|
||||
reactive: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {...operand},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
predecessor.instructions.push(instr);
|
||||
|
||||
@@ -6,21 +6,10 @@
|
||||
*/
|
||||
|
||||
import {CompilerError} from '../CompilerError';
|
||||
import {AliasingSignature} from '../Inference/InferMutationAliasingEffects';
|
||||
import {
|
||||
Effect,
|
||||
GeneratedSource,
|
||||
makeDeclarationId,
|
||||
makeIdentifierId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
ValueKind,
|
||||
ValueReason,
|
||||
} from './HIR';
|
||||
import {Effect, ValueKind, ValueReason} from './HIR';
|
||||
import {
|
||||
BuiltInType,
|
||||
FunctionType,
|
||||
makeType,
|
||||
ObjectType,
|
||||
PolyType,
|
||||
PrimitiveType,
|
||||
@@ -190,9 +179,6 @@ export type FunctionSignature = {
|
||||
impure?: boolean;
|
||||
|
||||
canonicalName?: string;
|
||||
|
||||
aliasing?: AliasingSignature | null;
|
||||
todo_aliasing?: AliasingSignature | null;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -316,30 +302,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnType: PRIMITIVE_TYPE,
|
||||
calleeEffect: Effect.Store,
|
||||
returnValueKind: ValueKind.Primitive,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Push directly mutates the array itself
|
||||
{kind: 'Mutate', value: signatureArgument(0)},
|
||||
// The arguments are captured into the array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
},
|
||||
// Returns the new length, a primitive
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -370,62 +332,6 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
noAlias: true,
|
||||
mutableOnlyIfOperandsAreMutable: true,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [makeIdentifierId(1)],
|
||||
rest: null,
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [
|
||||
// Temporary representing captured items of the receiver
|
||||
signatureArgument(3),
|
||||
// Temporary representing the result of the callback
|
||||
signatureArgument(4),
|
||||
/*
|
||||
* Undefined `this` arg to the callback. Note the signature does not
|
||||
* support passing an explicit thisArg second param
|
||||
*/
|
||||
signatureArgument(5),
|
||||
],
|
||||
effects: [
|
||||
// Map creates a new mutable array
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(2),
|
||||
value: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// The first arg to the callback is an item extracted from the receiver array
|
||||
{
|
||||
kind: 'CreateFrom',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(3),
|
||||
},
|
||||
// The undefined this for the callback
|
||||
{
|
||||
kind: 'Create',
|
||||
into: signatureArgument(5),
|
||||
value: ValueKind.Primitive,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
},
|
||||
// calls the callback, returning the result into a temporary
|
||||
{
|
||||
kind: 'Apply',
|
||||
receiver: signatureArgument(5),
|
||||
args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
|
||||
function: signatureArgument(1),
|
||||
into: signatureArgument(4),
|
||||
signature: null,
|
||||
mutatesFunction: false,
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
// captures the result of the callback into the return array
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(4),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -573,32 +479,6 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
|
||||
calleeEffect: Effect.Store,
|
||||
// returnValueKind is technically dependent on the ValueKind of the set itself
|
||||
returnValueKind: ValueKind.Mutable,
|
||||
aliasing: {
|
||||
receiver: makeIdentifierId(0),
|
||||
params: [],
|
||||
rest: makeIdentifierId(1),
|
||||
returns: makeIdentifierId(2),
|
||||
temporaries: [],
|
||||
effects: [
|
||||
// Set.add returns the receiver Set
|
||||
{
|
||||
kind: 'Assign',
|
||||
from: signatureArgument(0),
|
||||
into: signatureArgument(2),
|
||||
},
|
||||
// Set.add mutates the set itself
|
||||
{
|
||||
kind: 'Mutate',
|
||||
value: signatureArgument(0),
|
||||
},
|
||||
// Captures the rest params into the set
|
||||
{
|
||||
kind: 'Capture',
|
||||
from: signatureArgument(1),
|
||||
into: signatureArgument(0),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
@@ -1289,22 +1169,3 @@ export const DefaultNonmutatingHook = addHook(
|
||||
},
|
||||
'DefaultNonmutatingHook',
|
||||
);
|
||||
|
||||
export function signatureArgument(id: number): Place {
|
||||
const place: Place = {
|
||||
kind: 'Identifier',
|
||||
effect: Effect.Unknown,
|
||||
loc: GeneratedSource,
|
||||
reactive: false,
|
||||
identifier: {
|
||||
declarationId: makeDeclarationId(id),
|
||||
id: makeIdentifierId(id),
|
||||
loc: GeneratedSource,
|
||||
mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
|
||||
name: null,
|
||||
scope: null,
|
||||
type: makeType(),
|
||||
},
|
||||
};
|
||||
return place;
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@ import type {
|
||||
Type,
|
||||
} from './HIR';
|
||||
import {GotoVariant, InstructionKind} from './HIR';
|
||||
import {
|
||||
AliasingEffect,
|
||||
AliasingSignature,
|
||||
} from '../Inference/InferMutationAliasingEffects';
|
||||
|
||||
export type Options = {
|
||||
indent: number;
|
||||
@@ -71,15 +67,13 @@ export function printFunction(fn: HIRFunction): string {
|
||||
})
|
||||
.join(', ') +
|
||||
')';
|
||||
} else {
|
||||
definition += '()';
|
||||
}
|
||||
if (definition.length !== 0) {
|
||||
output.push(definition);
|
||||
}
|
||||
output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
|
||||
output.push(...fn.directives);
|
||||
output.push(printType(fn.returnType));
|
||||
output.push(printHIR(fn.body));
|
||||
output.push(...fn.directives);
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
@@ -157,10 +151,7 @@ export function printMixedHIR(
|
||||
|
||||
export function printInstruction(instr: ReactiveInstruction): string {
|
||||
const id = `[${instr.id}]`;
|
||||
let value = printInstructionValue(instr.value);
|
||||
if (instr.effects != null) {
|
||||
value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
const value = printInstructionValue(instr.value);
|
||||
|
||||
if (instr.lvalue !== null) {
|
||||
return `${id} ${printPlace(instr.lvalue)} = ${value}`;
|
||||
@@ -222,9 +213,6 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
value = `[${terminal.id}] Return${
|
||||
terminal.value != null ? ' ' + printPlace(terminal.value) : ''
|
||||
}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'goto': {
|
||||
@@ -293,9 +281,6 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
|
||||
}
|
||||
case 'maybe-throw': {
|
||||
value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
|
||||
if (terminal.effects != null) {
|
||||
value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'scope': {
|
||||
@@ -570,11 +555,8 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
|
||||
}
|
||||
})
|
||||
.join(', ') ?? '';
|
||||
const aliasingEffects =
|
||||
instrValue.loweredFunc.func.aliasingEffects
|
||||
?.map(printAliasingEffect)
|
||||
?.join(', ') ?? '';
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
|
||||
const type = printType(instrValue.loweredFunc.func.returnType).trim();
|
||||
value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
|
||||
break;
|
||||
}
|
||||
case 'TaggedTemplateExpression': {
|
||||
@@ -940,107 +922,3 @@ function getFunctionName(
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function printAliasingEffect(effect: AliasingEffect): string {
|
||||
switch (effect.kind) {
|
||||
case 'Assign': {
|
||||
return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Alias': {
|
||||
return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Capture': {
|
||||
return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
|
||||
}
|
||||
case 'Create': {
|
||||
return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
|
||||
}
|
||||
case 'CreateFrom': {
|
||||
return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
|
||||
}
|
||||
case 'CreateFunction': {
|
||||
return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
|
||||
}
|
||||
case 'Apply': {
|
||||
const receiverCallee =
|
||||
effect.receiver.identifier.id === effect.function.identifier.id
|
||||
? printPlaceForAliasEffect(effect.receiver)
|
||||
: `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
|
||||
const args = effect.args
|
||||
.map(arg => {
|
||||
if (arg.kind === 'Identifier') {
|
||||
return printPlaceForAliasEffect(arg);
|
||||
} else if (arg.kind === 'Hole') {
|
||||
return ' ';
|
||||
}
|
||||
return `...${printPlaceForAliasEffect(arg.place)}`;
|
||||
})
|
||||
.join(', ');
|
||||
let signature = '';
|
||||
if (effect.signature != null) {
|
||||
if (effect.signature.aliasing != null) {
|
||||
signature = printAliasingSignature(effect.signature.aliasing);
|
||||
} else {
|
||||
signature = JSON.stringify(effect.signature, null, 2);
|
||||
}
|
||||
}
|
||||
return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
|
||||
}
|
||||
case 'Freeze': {
|
||||
return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
|
||||
}
|
||||
case 'MutateFrozen': {
|
||||
return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'MutateGlobal': {
|
||||
return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Impure': {
|
||||
return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
|
||||
}
|
||||
case 'Render': {
|
||||
return `Render ${printPlaceForAliasEffect(effect.place)}`;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printPlaceForAliasEffect(place: Place): string {
|
||||
return printIdentifier(place.identifier);
|
||||
}
|
||||
|
||||
export function printAliasingSignature(signature: AliasingSignature): string {
|
||||
const tokens: Array<string> = ['function '];
|
||||
if (signature.temporaries.length !== 0) {
|
||||
tokens.push('<');
|
||||
tokens.push(
|
||||
signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
|
||||
);
|
||||
tokens.push('>');
|
||||
}
|
||||
tokens.push('(');
|
||||
tokens.push('this=$' + String(signature.receiver));
|
||||
for (const param of signature.params) {
|
||||
tokens.push(', $' + String(param));
|
||||
}
|
||||
if (signature.rest != null) {
|
||||
tokens.push(`, ...$${String(signature.rest)}`);
|
||||
}
|
||||
tokens.push('): ');
|
||||
tokens.push('$' + String(signature.returns) + ':');
|
||||
for (const effect of signature.effects) {
|
||||
tokens.push('\n ' + printAliasingEffect(effect));
|
||||
}
|
||||
return tokens.join('');
|
||||
}
|
||||
|
||||
@@ -735,7 +735,6 @@ export function mapTerminalSuccessors(
|
||||
loc: terminal.loc,
|
||||
value: terminal.value,
|
||||
id: makeInstructionId(0),
|
||||
effects: terminal.effects,
|
||||
};
|
||||
}
|
||||
case 'throw': {
|
||||
@@ -843,7 +842,6 @@ export function mapTerminalSuccessors(
|
||||
handler,
|
||||
id: makeInstructionId(0),
|
||||
loc: terminal.loc,
|
||||
effects: terminal.effects,
|
||||
};
|
||||
}
|
||||
case 'try': {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
LoweredFunction,
|
||||
isRefOrRefValue,
|
||||
makeInstructionId,
|
||||
@@ -20,10 +19,6 @@ 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 {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
|
||||
import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
|
||||
|
||||
export default function analyseFunctions(func: HIRFunction): void {
|
||||
for (const [_, block] of func.body.blocks) {
|
||||
@@ -31,12 +26,8 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
switch (instr.value.kind) {
|
||||
case 'ObjectMethod':
|
||||
case 'FunctionExpression': {
|
||||
if (!func.env.config.enableNewMutationAliasingModel) {
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
} else {
|
||||
lowerWithMutationAliasing(instr.value.loweredFunc.func);
|
||||
}
|
||||
lower(instr.value.loweredFunc.func);
|
||||
infer(instr.value.loweredFunc);
|
||||
|
||||
/**
|
||||
* Reset mutable range for outer inferReferenceEffects
|
||||
@@ -53,79 +44,6 @@ export default function analyseFunctions(func: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
function lowerWithMutationAliasing(fn: HIRFunction): void {
|
||||
analyseFunctions(fn);
|
||||
inferMutationAliasingEffects(fn, {isFunctionExpression: true});
|
||||
deadCodeElimination(fn);
|
||||
inferMutationAliasingRanges(fn, {isFunctionExpression: true});
|
||||
rewriteInstructionKindsBasedOnReassignment(fn);
|
||||
inferReactiveScopeVariables(fn);
|
||||
const effects = inferMutationAliasingFunctionEffects(fn);
|
||||
fn.env.logger?.debugLogIRs?.({
|
||||
kind: 'hir',
|
||||
name: 'AnalyseFunction (inner)',
|
||||
value: fn,
|
||||
});
|
||||
if (effects != null) {
|
||||
fn.aliasingEffects ??= [];
|
||||
fn.aliasingEffects?.push(...effects);
|
||||
}
|
||||
|
||||
const capturedOrMutated = new Set<IdentifierId>();
|
||||
for (const effect of effects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
capturedOrMutated.add(effect.from.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'Mutate':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitive':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
capturedOrMutated.add(effect.value.identifier.id);
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal':
|
||||
case 'CreateFunction':
|
||||
case 'Create':
|
||||
case 'Freeze':
|
||||
case 'ImmutableCapture': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind ${(effect as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const operand of fn.context) {
|
||||
if (
|
||||
capturedOrMutated.has(operand.identifier.id) ||
|
||||
operand.effect === Effect.Capture
|
||||
) {
|
||||
operand.effect = Effect.Capture;
|
||||
} else {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lower(func: HIRFunction): void {
|
||||
analyseFunctions(func);
|
||||
inferReferenceEffects(func, {isFunctionExpression: true});
|
||||
|
||||
@@ -197,7 +197,6 @@ function makeManualMemoizationMarkers(
|
||||
deps: depsList,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
{
|
||||
@@ -209,7 +208,6 @@ function makeManualMemoizationMarkers(
|
||||
decl: {...memoDecl},
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
effects: null,
|
||||
loc: fnExpr.loc,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
isSetStateType,
|
||||
isFireFunctionType,
|
||||
makeScopeId,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {collectHoistablePropertyLoadsInInnerFn} from '../HIR/CollectHoistablePropertyLoads';
|
||||
import {collectOptionalChainSidemap} from '../HIR/CollectOptionalChainDependencies';
|
||||
@@ -237,10 +236,9 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
value: deps,
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
});
|
||||
|
||||
// Step 2: push the inferred deps array as an argument of the useEffect
|
||||
@@ -251,10 +249,9 @@ export function inferEffectDependencies(fn: HIRFunction): void {
|
||||
// Global functions have no reactive dependencies, so we can insert an empty array
|
||||
newInstructions.push({
|
||||
id: makeInstructionId(0),
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
value: deps,
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...depsPlace, effect: Effect.Mutate},
|
||||
value: deps,
|
||||
});
|
||||
value.args.push({...depsPlace, effect: Effect.Freeze});
|
||||
rewriteInstrs.set(instr.id, newInstructions);
|
||||
@@ -319,25 +316,21 @@ function writeDependencyToInstructions(
|
||||
const instructions: Array<Instruction> = [];
|
||||
let currValue = createTemporaryPlace(env, GeneratedSource);
|
||||
currValue.reactive = reactive;
|
||||
const dependencyPlace: Place = {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
};
|
||||
instructions.push({
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: {...currValue, effect: Effect.Mutate},
|
||||
value: {
|
||||
kind: 'LoadLocal',
|
||||
place: {...dependencyPlace},
|
||||
place: {
|
||||
kind: 'Identifier',
|
||||
identifier: dep.identifier,
|
||||
effect: Effect.Capture,
|
||||
reactive,
|
||||
loc: loc,
|
||||
},
|
||||
loc: loc,
|
||||
},
|
||||
effects: [
|
||||
{kind: 'Alias', from: {...dependencyPlace}, into: {...currValue}},
|
||||
],
|
||||
});
|
||||
for (const path of dep.path) {
|
||||
if (path.optional) {
|
||||
@@ -366,7 +359,6 @@ function writeDependencyToInstructions(
|
||||
property: path.property,
|
||||
loc: loc,
|
||||
},
|
||||
effects: [{kind: 'Capture', from: {...currValue}, into: {...nextValue}}],
|
||||
});
|
||||
currValue = nextValue;
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
|
||||
return effect.kind === 'GlobalMutation';
|
||||
}
|
||||
|
||||
export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
if (abstractValue.reason.has(ValueReason.Global)) {
|
||||
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
|
||||
} else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
|
||||
@@ -339,8 +339,6 @@ export function getWriteErrorReason(abstractValue: AbstractValue): string {
|
||||
return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.ReducerState)) {
|
||||
return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
|
||||
} else if (abstractValue.reason.has(ValueReason.Effect)) {
|
||||
return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
|
||||
} else {
|
||||
return 'This mutates a variable that React considers immutable';
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
|
||||
function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
|
||||
import {getOrInsertDefault} from '../Utils/utils';
|
||||
import {AliasingEffect} from './InferMutationAliasingEffects';
|
||||
|
||||
export function inferMutationAliasingFunctionEffects(
|
||||
fn: HIRFunction,
|
||||
): Array<AliasingEffect> | null {
|
||||
const effects: Array<AliasingEffect> = [];
|
||||
|
||||
/**
|
||||
* Map used to identify tracked variables: params, context vars, return value
|
||||
* This is used to detect mutation/capturing/aliasing of params/context vars
|
||||
*/
|
||||
const tracked = new Map<IdentifierId, Place>();
|
||||
tracked.set(fn.returns.identifier.id, fn.returns);
|
||||
for (const operand of [...fn.context, ...fn.params]) {
|
||||
const place = operand.kind === 'Identifier' ? operand : operand.place;
|
||||
tracked.set(place.identifier.id, place);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track capturing/aliasing of context vars and params into each other and into the return.
|
||||
* We don't need to track locals and intermediate values, since we're only concerned with effects
|
||||
* as they relate to arguments visible outside the function.
|
||||
*
|
||||
* For each aliased identifier we track capture/alias/createfrom and then merge this with how
|
||||
* the value is used. Eg capturing an alias => capture. See joinEffects() helper.
|
||||
*/
|
||||
type AliasedIdentifier = {
|
||||
kind: AliasingKind;
|
||||
place: Place;
|
||||
};
|
||||
const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
|
||||
|
||||
/*
|
||||
* Check for aliasing of tracked values. Also joins the effects of how the value is
|
||||
* used (@param kind) with the aliasing type of each value
|
||||
*/
|
||||
function lookup(
|
||||
place: Place,
|
||||
kind: AliasedIdentifier['kind'],
|
||||
): Array<AliasedIdentifier> | null {
|
||||
if (tracked.has(place.identifier.id)) {
|
||||
return [{kind, place}];
|
||||
}
|
||||
return (
|
||||
dataFlow.get(place.identifier.id)?.map(aliased => ({
|
||||
kind: joinEffects(aliased.kind, kind),
|
||||
place: aliased.place,
|
||||
})) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
// todo: fixpoint
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
const operands: Array<AliasedIdentifier> = [];
|
||||
for (const operand of phi.operands.values()) {
|
||||
const inputs = lookup(operand, 'Alias');
|
||||
if (inputs != null) {
|
||||
operands.push(...inputs);
|
||||
}
|
||||
}
|
||||
if (operands.length !== 0) {
|
||||
dataFlow.set(phi.place.identifier.id, operands);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (
|
||||
effect.kind === 'Assign' ||
|
||||
effect.kind === 'Capture' ||
|
||||
effect.kind === 'Alias' ||
|
||||
effect.kind === 'CreateFrom'
|
||||
) {
|
||||
const from = lookup(effect.from, effect.kind);
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
const into = lookup(effect.into, 'Alias');
|
||||
if (into == null) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
} else {
|
||||
for (const aliased of into) {
|
||||
getOrInsertDefault(
|
||||
dataFlow,
|
||||
aliased.place.identifier.id,
|
||||
[],
|
||||
).push(...from);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
effect.kind === 'Create' ||
|
||||
effect.kind === 'CreateFunction'
|
||||
) {
|
||||
getOrInsertDefault(dataFlow, effect.into.identifier.id, [
|
||||
{kind: 'Alias', place: effect.into},
|
||||
]);
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure' ||
|
||||
effect.kind === 'Render'
|
||||
) {
|
||||
effects.push(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
const from = lookup(block.terminal.value, 'Alias');
|
||||
if (from != null) {
|
||||
getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
|
||||
...from,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create aliasing effects based on observed data flow
|
||||
let hasReturn = false;
|
||||
for (const [into, from] of dataFlow) {
|
||||
const input = tracked.get(into);
|
||||
if (input == null) {
|
||||
continue;
|
||||
}
|
||||
for (const aliased of from) {
|
||||
if (
|
||||
aliased.place.identifier.id === input.identifier.id ||
|
||||
!tracked.has(aliased.place.identifier.id)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const effect = {kind: aliased.kind, from: aliased.place, into: input};
|
||||
effects.push(effect);
|
||||
if (
|
||||
into === fn.returns.identifier.id &&
|
||||
(aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
|
||||
) {
|
||||
hasReturn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: more precise return effect inference
|
||||
if (!hasReturn) {
|
||||
effects.unshift({
|
||||
kind: 'Create',
|
||||
into: fn.returns,
|
||||
value:
|
||||
fn.returnType.kind === 'Primitive'
|
||||
? ValueKind.Primitive
|
||||
: ValueKind.Mutable,
|
||||
reason: ValueReason.KnownReturnSignature,
|
||||
});
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
export enum MutationKind {
|
||||
None = 0,
|
||||
Conditional = 1,
|
||||
Definite = 2,
|
||||
}
|
||||
|
||||
type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
|
||||
function joinEffects(
|
||||
effect1: AliasingKind,
|
||||
effect2: AliasingKind,
|
||||
): AliasingKind {
|
||||
if (effect1 === 'Capture' || effect2 === 'Capture') {
|
||||
return 'Capture';
|
||||
} else if (effect1 === 'Assign' || effect2 === 'Assign') {
|
||||
return 'Assign';
|
||||
} else {
|
||||
return 'Alias';
|
||||
}
|
||||
}
|
||||
@@ -1,719 +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 prettyFormat from 'pretty-format';
|
||||
import {CompilerError, SourceLocation} from '..';
|
||||
import {
|
||||
BlockId,
|
||||
Effect,
|
||||
HIRFunction,
|
||||
Identifier,
|
||||
IdentifierId,
|
||||
InstructionId,
|
||||
makeInstructionId,
|
||||
Place,
|
||||
} from '../HIR/HIR';
|
||||
import {
|
||||
eachInstructionLValue,
|
||||
eachInstructionValueOperand,
|
||||
eachTerminalOperand,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
|
||||
import {printFunction} from '../HIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {MutationKind} from './InferMutationAliasingFunctionEffects';
|
||||
import {Result} from '../Utils/Result';
|
||||
|
||||
const DEBUG = false;
|
||||
const VERBOSE = false;
|
||||
|
||||
/**
|
||||
* Infers mutable ranges for all values.
|
||||
*/
|
||||
export function inferMutationAliasingRanges(
|
||||
fn: HIRFunction,
|
||||
{isFunctionExpression}: {isFunctionExpression: boolean},
|
||||
): Result<void, CompilerError> {
|
||||
if (VERBOSE) {
|
||||
console.log();
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
/**
|
||||
* Part 1: Infer mutable ranges for values. We build an abstract model of
|
||||
* values, the alias/capture edges between them, and the set of mutations.
|
||||
* Edges and mutations are ordered, with mutations processed against the
|
||||
* abstract model only after it is fully constructed by visiting all blocks
|
||||
* _and_ connecting phis. Phis are considered ordered at the time of the
|
||||
* phi node.
|
||||
*
|
||||
* This should (may?) mean that mutations are able to see the full state
|
||||
* of the graph and mark all the appropriate identifiers as mutated at
|
||||
* the correct point, accounting for both backward and forward edges.
|
||||
* Ie a mutation of x accounts for both values that flowed into x,
|
||||
* and values that x flowed into.
|
||||
*/
|
||||
const state = new AliasingState();
|
||||
type PendingPhiOperand = {from: Place; into: Place; index: number};
|
||||
const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
|
||||
const mutations: Array<{
|
||||
index: number;
|
||||
id: InstructionId;
|
||||
transitive: boolean;
|
||||
kind: MutationKind;
|
||||
place: Place;
|
||||
}> = [];
|
||||
const renders: Array<{index: number; place: Place}> = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
const errors = new CompilerError();
|
||||
|
||||
for (const param of [...fn.params, ...fn.context, fn.returns]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
state.create(place, {kind: 'Object'});
|
||||
}
|
||||
const seenBlocks = new Set<BlockId>();
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
state.create(phi.place, {kind: 'Phi'});
|
||||
for (const [pred, operand] of phi.operands) {
|
||||
if (!seenBlocks.has(pred)) {
|
||||
// NOTE: annotation required to actually typecheck and not silently infer `any`
|
||||
const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
|
||||
pendingPhis,
|
||||
pred,
|
||||
() => [],
|
||||
);
|
||||
blockPhis.push({from: operand, into: phi.place, index: index++});
|
||||
} else {
|
||||
state.assign(index++, operand, phi.place);
|
||||
}
|
||||
}
|
||||
}
|
||||
seenBlocks.add(block.id);
|
||||
|
||||
for (const instr of block.instructions) {
|
||||
if (
|
||||
instr.value.kind === 'FunctionExpression' ||
|
||||
instr.value.kind === 'ObjectMethod'
|
||||
) {
|
||||
state.create(instr.lvalue, {
|
||||
kind: 'Function',
|
||||
function: instr.value.loweredFunc.func,
|
||||
});
|
||||
} else {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
state.create(lvalue, {kind: 'Object'});
|
||||
}
|
||||
}
|
||||
|
||||
if (instr.effects == null) continue;
|
||||
for (const effect of instr.effects) {
|
||||
if (effect.kind === 'Create') {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
} else if (effect.kind === 'CreateFunction') {
|
||||
state.create(effect.into, {
|
||||
kind: 'Function',
|
||||
function: effect.function.loweredFunc.func,
|
||||
});
|
||||
} else if (effect.kind === 'CreateFrom') {
|
||||
state.createFrom(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Assign') {
|
||||
if (!state.nodes.has(effect.into.identifier)) {
|
||||
state.create(effect.into, {kind: 'Object'});
|
||||
}
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else if (effect.kind === 'Capture') {
|
||||
state.capture(index++, effect.from, effect.into);
|
||||
} else if (
|
||||
effect.kind === 'MutateTransitive' ||
|
||||
effect.kind === 'MutateTransitiveConditionally'
|
||||
) {
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: true,
|
||||
kind:
|
||||
effect.kind === 'MutateTransitive'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'Mutate' ||
|
||||
effect.kind === 'MutateConditionally'
|
||||
) {
|
||||
mutations.push({
|
||||
index: index++,
|
||||
id: instr.id,
|
||||
transitive: false,
|
||||
kind:
|
||||
effect.kind === 'Mutate'
|
||||
? MutationKind.Definite
|
||||
: MutationKind.Conditional,
|
||||
place: effect.value,
|
||||
});
|
||||
} else if (
|
||||
effect.kind === 'MutateFrozen' ||
|
||||
effect.kind === 'MutateGlobal' ||
|
||||
effect.kind === 'Impure'
|
||||
) {
|
||||
errors.push(effect.error);
|
||||
} else if (effect.kind === 'Render') {
|
||||
renders.push({index: index++, place: effect.place});
|
||||
}
|
||||
}
|
||||
}
|
||||
const blockPhis = pendingPhis.get(block.id);
|
||||
if (blockPhis != null) {
|
||||
for (const {from, into, index} of blockPhis) {
|
||||
state.assign(index, from, into);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
state.assign(index++, block.terminal.value, fn.returns);
|
||||
}
|
||||
|
||||
if (
|
||||
(block.terminal.kind === 'maybe-throw' ||
|
||||
block.terminal.kind === 'return') &&
|
||||
block.terminal.effects != null
|
||||
) {
|
||||
for (const effect of block.terminal.effects) {
|
||||
if (effect.kind === 'Alias') {
|
||||
state.assign(index++, effect.from, effect.into);
|
||||
} else {
|
||||
CompilerError.invariant(effect.kind === 'Freeze', {
|
||||
reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
|
||||
loc: block.terminal.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(state.debug());
|
||||
console.log(pretty(mutations));
|
||||
}
|
||||
for (const mutation of mutations) {
|
||||
state.mutate(
|
||||
mutation.index,
|
||||
mutation.place.identifier,
|
||||
makeInstructionId(mutation.id + 1),
|
||||
mutation.transitive,
|
||||
mutation.kind,
|
||||
mutation.place.loc,
|
||||
errors,
|
||||
);
|
||||
}
|
||||
for (const render of renders) {
|
||||
state.render(render.index, render.place.identifier, errors);
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(pretty([...state.nodes.keys()]));
|
||||
}
|
||||
fn.aliasingEffects ??= [];
|
||||
for (const param of [...fn.context, ...fn.params]) {
|
||||
const place = param.kind === 'Identifier' ? param : param.place;
|
||||
const node = state.nodes.get(place.identifier);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
let mutated = false;
|
||||
if (node.local != null) {
|
||||
if (node.local.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateConditionally',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
} else if (node.local.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'Mutate',
|
||||
value: {...place, loc: node.local.loc},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (node.transitive != null) {
|
||||
if (node.transitive.kind === MutationKind.Conditional) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitiveConditionally',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
} else if (node.transitive.kind === MutationKind.Definite) {
|
||||
mutated = true;
|
||||
fn.aliasingEffects.push({
|
||||
kind: 'MutateTransitive',
|
||||
value: {...place, loc: node.transitive.loc},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mutated) {
|
||||
place.effect = Effect.Capture;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Part 2
|
||||
* Add legacy operand-specific effects based on instruction effects and mutable ranges.
|
||||
* Also fixes up operand mutable ranges, making sure that start is non-zero if the value
|
||||
* is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
|
||||
* to filter spurious mutations of globals, which we now guard against more precisely)
|
||||
*/
|
||||
for (const block of fn.body.blocks.values()) {
|
||||
for (const phi of block.phis) {
|
||||
// TODO: we don't actually set these effects today!
|
||||
phi.place.effect = Effect.Store;
|
||||
const isPhiMutatedAfterCreation: boolean =
|
||||
phi.place.identifier.mutableRange.end >
|
||||
(block.instructions.at(0)?.id ?? block.terminal.id);
|
||||
for (const operand of phi.operands.values()) {
|
||||
operand.effect = isPhiMutatedAfterCreation
|
||||
? Effect.Capture
|
||||
: Effect.Read;
|
||||
}
|
||||
if (
|
||||
isPhiMutatedAfterCreation &&
|
||||
phi.place.identifier.mutableRange.start === 0
|
||||
) {
|
||||
/*
|
||||
* TODO: ideally we'd construct a precise start range, but what really
|
||||
* matters is that the phi's range appears mutable (end > start + 1)
|
||||
* so we just set the start to the previous instruction before this block
|
||||
*/
|
||||
const firstInstructionIdOfBlock =
|
||||
block.instructions.at(0)?.id ?? block.terminal.id;
|
||||
phi.place.identifier.mutableRange.start = makeInstructionId(
|
||||
firstInstructionIdOfBlock - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const instr of block.instructions) {
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
lvalue.effect = Effect.ConditionallyMutate;
|
||||
if (lvalue.identifier.mutableRange.start === 0) {
|
||||
lvalue.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
if (lvalue.identifier.mutableRange.end === 0) {
|
||||
lvalue.identifier.mutableRange.end = makeInstructionId(
|
||||
Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
if (instr.effects == null) {
|
||||
continue;
|
||||
}
|
||||
const operandEffects = new Map<IdentifierId, Effect>();
|
||||
for (const effect of instr.effects) {
|
||||
switch (effect.kind) {
|
||||
case 'Assign':
|
||||
case 'Alias':
|
||||
case 'Capture':
|
||||
case 'CreateFrom': {
|
||||
const isMutatedOrReassigned =
|
||||
effect.into.identifier.mutableRange.end > instr.id;
|
||||
if (isMutatedOrReassigned) {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Capture);
|
||||
operandEffects.set(effect.into.identifier.id, Effect.Store);
|
||||
} else {
|
||||
operandEffects.set(effect.from.identifier.id, Effect.Read);
|
||||
operandEffects.set(effect.into.identifier.id, Effect.Store);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CreateFunction':
|
||||
case 'Create': {
|
||||
break;
|
||||
}
|
||||
case 'Mutate': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Store);
|
||||
break;
|
||||
}
|
||||
case 'Apply': {
|
||||
CompilerError.invariant(false, {
|
||||
reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
|
||||
loc: effect.function.loc,
|
||||
});
|
||||
}
|
||||
case 'MutateTransitive':
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
operandEffects.set(
|
||||
effect.value.identifier.id,
|
||||
Effect.ConditionallyMutate,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'Freeze': {
|
||||
operandEffects.set(effect.value.identifier.id, Effect.Freeze);
|
||||
break;
|
||||
}
|
||||
case 'ImmutableCapture': {
|
||||
// no-op, Read is the default
|
||||
break;
|
||||
}
|
||||
case 'Impure':
|
||||
case 'Render':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
// no-op
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
effect,
|
||||
`Unexpected effect kind ${(effect as any).kind}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const lvalue of eachInstructionLValue(instr)) {
|
||||
const effect =
|
||||
operandEffects.get(lvalue.identifier.id) ??
|
||||
Effect.ConditionallyMutate;
|
||||
lvalue.effect = effect;
|
||||
}
|
||||
for (const operand of eachInstructionValueOperand(instr.value)) {
|
||||
if (
|
||||
operand.identifier.mutableRange.end > instr.id &&
|
||||
operand.identifier.mutableRange.start === 0
|
||||
) {
|
||||
operand.identifier.mutableRange.start = instr.id;
|
||||
}
|
||||
const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
|
||||
operand.effect = effect;
|
||||
}
|
||||
|
||||
/**
|
||||
* This case is targeted at hoisted functions like:
|
||||
*
|
||||
* ```
|
||||
* x();
|
||||
* function x() { ... }
|
||||
* ```
|
||||
*
|
||||
* Which turns into:
|
||||
*
|
||||
* t0 = DeclareContext HoistedFunction x
|
||||
* t1 = LoadContext x
|
||||
* t2 = CallExpression t1 ( )
|
||||
* t3 = FunctionExpression ...
|
||||
* t4 = StoreContext Function x = t3
|
||||
*
|
||||
* If the function had captured mutable values, it would already have its
|
||||
* range extended to include the StoreContext. But if the function doesn't
|
||||
* capture any mutable values its range won't have been extended yet. We
|
||||
* want to ensure that the value is memoized along with the context variable,
|
||||
* not independently of it (bc of the way we do codegen for hoisted functions).
|
||||
* So here we check for StoreContext rvalues and if they haven't already had
|
||||
* their range extended to at least this instruction, we extend it.
|
||||
*/
|
||||
if (
|
||||
instr.value.kind === 'StoreContext' &&
|
||||
instr.value.value.identifier.mutableRange.end <= instr.id
|
||||
) {
|
||||
instr.value.value.identifier.mutableRange.end = makeInstructionId(
|
||||
instr.id + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (block.terminal.kind === 'return') {
|
||||
block.terminal.value.effect = isFunctionExpression
|
||||
? Effect.Read
|
||||
: Effect.Freeze;
|
||||
} else {
|
||||
for (const operand of eachTerminalOperand(block.terminal)) {
|
||||
operand.effect = Effect.Read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
console.log(printFunction(fn));
|
||||
}
|
||||
return errors.asResult();
|
||||
}
|
||||
|
||||
function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
|
||||
for (const effect of fn.aliasingEffects ?? []) {
|
||||
switch (effect.kind) {
|
||||
case 'Impure':
|
||||
case 'MutateFrozen':
|
||||
case 'MutateGlobal': {
|
||||
errors.push(effect.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Node = {
|
||||
id: Identifier;
|
||||
createdFrom: Map<Identifier, number>;
|
||||
captures: Map<Identifier, number>;
|
||||
aliases: Map<Identifier, number>;
|
||||
edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
|
||||
transitive: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
local: {kind: MutationKind; loc: SourceLocation} | null;
|
||||
value:
|
||||
| {kind: 'Object'}
|
||||
| {kind: 'Phi'}
|
||||
| {kind: 'Function'; function: HIRFunction};
|
||||
};
|
||||
class AliasingState {
|
||||
nodes: Map<Identifier, Node> = new Map();
|
||||
|
||||
create(place: Place, value: Node['value']): void {
|
||||
this.nodes.set(place.identifier, {
|
||||
id: place.identifier,
|
||||
createdFrom: new Map(),
|
||||
captures: new Map(),
|
||||
aliases: new Map(),
|
||||
edges: [],
|
||||
transitive: null,
|
||||
local: null,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
createFrom(index: number, from: Place, into: Place): void {
|
||||
this.create(into, {kind: 'Object'});
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.createdFrom.has(from.identifier)) {
|
||||
toNode.createdFrom.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
capture(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
|
||||
if (!toNode.captures.has(from.identifier)) {
|
||||
toNode.captures.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
assign(index: number, from: Place, into: Place): void {
|
||||
const fromNode = this.nodes.get(from.identifier);
|
||||
const toNode = this.nodes.get(into.identifier);
|
||||
if (fromNode == null || toNode == null) {
|
||||
if (VERBOSE) {
|
||||
console.log(
|
||||
`skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
|
||||
if (!toNode.aliases.has(from.identifier)) {
|
||||
toNode.aliases.set(from.identifier, index);
|
||||
}
|
||||
}
|
||||
|
||||
render(index: number, start: Identifier, errors: CompilerError): void {
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<Identifier> = [start];
|
||||
while (queue.length !== 0) {
|
||||
const current = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null || node.transitive != null || node.local != null) {
|
||||
continue;
|
||||
}
|
||||
if (node.value.kind === 'Function') {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(alias);
|
||||
}
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push(capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutate(
|
||||
index: number,
|
||||
start: Identifier,
|
||||
end: InstructionId,
|
||||
transitive: boolean,
|
||||
kind: MutationKind,
|
||||
loc: SourceLocation,
|
||||
errors: CompilerError,
|
||||
): void {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
|
||||
);
|
||||
}
|
||||
const seen = new Set<Identifier>();
|
||||
const queue: Array<{
|
||||
place: Identifier;
|
||||
transitive: boolean;
|
||||
direction: 'backwards' | 'forwards';
|
||||
}> = [{place: start, transitive, direction: 'backwards'}];
|
||||
while (queue.length !== 0) {
|
||||
const {place: current, transitive, direction} = queue.pop()!;
|
||||
if (seen.has(current)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(current);
|
||||
const node = this.nodes.get(current);
|
||||
if (node == null) {
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
`no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (DEBUG) {
|
||||
console.log(
|
||||
` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
|
||||
);
|
||||
}
|
||||
node.id.mutableRange.end = makeInstructionId(
|
||||
Math.max(node.id.mutableRange.end, end),
|
||||
);
|
||||
if (
|
||||
node.value.kind === 'Function' &&
|
||||
node.transitive == null &&
|
||||
node.local == null
|
||||
) {
|
||||
appendFunctionErrors(errors, node.value.function);
|
||||
}
|
||||
if (transitive) {
|
||||
if (node.transitive == null || node.transitive.kind < kind) {
|
||||
node.transitive = {kind, loc};
|
||||
}
|
||||
} else {
|
||||
if (node.local == null || node.local.kind < kind) {
|
||||
node.local = {kind, loc};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* all mutations affect "forward" edges by the rules:
|
||||
* - Capture a -> b, mutate(a) => mutate(b)
|
||||
* - Alias a -> b, mutate(a) => mutate(b)
|
||||
*/
|
||||
for (const edge of node.edges) {
|
||||
if (edge.index >= index) {
|
||||
break;
|
||||
}
|
||||
queue.push({place: edge.node, transitive, direction: 'forwards'});
|
||||
}
|
||||
for (const [alias, when] of node.createdFrom) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive: true, direction: 'backwards'});
|
||||
}
|
||||
if (direction === 'backwards' || node.value.kind !== 'Phi') {
|
||||
/**
|
||||
* all mutations affect backward alias edges by the rules:
|
||||
* - Alias a -> b, mutate(b) => mutate(a)
|
||||
* - Alias a -> b, mutateTransitive(b) => mutate(a)
|
||||
*
|
||||
* However, if we reached a phi because one of its inputs was mutated
|
||||
* (and we're advancing "forwards" through that node's edges), then
|
||||
* we know we've already processed the mutation at its source. The
|
||||
* phi's other inputs can't be affected.
|
||||
*/
|
||||
for (const [alias, when] of node.aliases) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: alias, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* but only transitive mutations affect captures
|
||||
*/
|
||||
if (transitive) {
|
||||
for (const [capture, when] of node.captures) {
|
||||
if (when >= index) {
|
||||
continue;
|
||||
}
|
||||
queue.push({place: capture, transitive, direction: 'backwards'});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
const nodes = new Map();
|
||||
for (const id of seen) {
|
||||
const node = this.nodes.get(id);
|
||||
nodes.set(id.id, node);
|
||||
}
|
||||
console.log(pretty(nodes));
|
||||
}
|
||||
}
|
||||
|
||||
debug(): string {
|
||||
return pretty(this.nodes);
|
||||
}
|
||||
}
|
||||
|
||||
export function pretty(v: any): string {
|
||||
return prettyFormat(v, {
|
||||
plugins: [
|
||||
{
|
||||
test: v =>
|
||||
v !== null && typeof v === 'object' && v.kind === 'Identifier',
|
||||
serialize: v => printPlace(v),
|
||||
},
|
||||
{
|
||||
test: v =>
|
||||
v !== null &&
|
||||
typeof v === 'object' &&
|
||||
typeof v.declarationId === 'number',
|
||||
serialize: v =>
|
||||
`${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
eachTerminalOperand,
|
||||
eachTerminalSuccessor,
|
||||
} from '../HIR/visitors';
|
||||
import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
|
||||
import {assertExhaustive} from '../Utils/utils';
|
||||
import {
|
||||
inferTerminalFunctionEffects,
|
||||
inferInstructionFunctionEffects,
|
||||
@@ -779,7 +779,7 @@ function inferParam(
|
||||
* │ Mutable │───┘
|
||||
* └──────────────────────────┘
|
||||
*/
|
||||
export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
|
||||
if (a === b) {
|
||||
return a;
|
||||
} else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
|
||||
@@ -821,16 +821,28 @@ export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `a` is a superset of `b`.
|
||||
*/
|
||||
function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
|
||||
for (const v of b) {
|
||||
if (!a.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function mergeAbstractValues(
|
||||
a: AbstractValue,
|
||||
b: AbstractValue,
|
||||
): AbstractValue {
|
||||
const kind = mergeValueKinds(a.kind, b.kind);
|
||||
const kind = mergeValues(a.kind, b.kind);
|
||||
if (
|
||||
kind === a.kind &&
|
||||
kind === b.kind &&
|
||||
Set_isSuperset(a.reason, b.reason) &&
|
||||
Set_isSuperset(a.context, b.context)
|
||||
isSuperset(a.reason, b.reason) &&
|
||||
isSuperset(a.context, b.context)
|
||||
) {
|
||||
return a;
|
||||
}
|
||||
@@ -1977,7 +1989,7 @@ function areArgumentsImmutableAndNonMutating(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getArgumentEffect(
|
||||
function getArgumentEffect(
|
||||
signatureEffect: Effect | null,
|
||||
arg: Place | SpreadPattern,
|
||||
): Effect {
|
||||
|
||||
@@ -235,7 +235,6 @@ function rewriteBlock(
|
||||
type: null,
|
||||
loc: terminal.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
block.terminal = {
|
||||
kind: 'goto',
|
||||
@@ -264,6 +263,5 @@ function declareTemporary(
|
||||
type: null,
|
||||
loc: result.loc,
|
||||
},
|
||||
effects: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -509,73 +509,6 @@ function evaluateInstruction(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'TemplateLiteral': {
|
||||
if (value.subexprs.length === 0) {
|
||||
const result: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
value: value.quasis.map(q => q.cooked).join(''),
|
||||
loc: value.loc,
|
||||
};
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (value.subexprs.length !== value.quasis.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.quasis.some(q => q.cooked === undefined)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let quasiIndex = 0;
|
||||
let resultString = value.quasis[quasiIndex].cooked as string;
|
||||
++quasiIndex;
|
||||
|
||||
for (const subExpr of value.subexprs) {
|
||||
const subExprValue = read(constants, subExpr);
|
||||
if (!subExprValue || subExprValue.kind !== 'Primitive') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expressionValue = subExprValue.value;
|
||||
if (
|
||||
typeof expressionValue !== 'number' &&
|
||||
typeof expressionValue !== 'string' &&
|
||||
typeof expressionValue !== 'boolean' &&
|
||||
!(typeof expressionValue === 'object' && expressionValue === null)
|
||||
) {
|
||||
// value is not supported (function, object) or invalid (symbol), or something else
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = value.quasis[quasiIndex].cooked;
|
||||
++quasiIndex;
|
||||
|
||||
if (suffix === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Spec states that concat calls ToString(argument) internally on its parameters
|
||||
* -> we don't have to implement ToString(argument) ourselves and just use the engine implementation
|
||||
* Refs:
|
||||
* - https://tc39.es/ecma262/2024/#sec-tostring
|
||||
* - https://tc39.es/ecma262/2024/#sec-string.prototype.concat
|
||||
* - https://tc39.es/ecma262/2024/#sec-template-literals-runtime-semantics-evaluation
|
||||
*/
|
||||
resultString = resultString.concat(expressionValue as string, suffix);
|
||||
}
|
||||
|
||||
const result: InstructionValue = {
|
||||
kind: 'Primitive',
|
||||
value: resultString,
|
||||
loc: value.loc,
|
||||
};
|
||||
|
||||
instr.value = result;
|
||||
return result;
|
||||
}
|
||||
case 'LoadLocal': {
|
||||
const placeValue = read(constants, value.place);
|
||||
if (placeValue !== null) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
Place,
|
||||
promoteTemporary,
|
||||
SpreadPattern,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {
|
||||
createTemporaryPlace,
|
||||
@@ -152,7 +151,6 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(varInstruction);
|
||||
@@ -169,7 +167,6 @@ export function inlineJsxTransform(
|
||||
},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
currentBlockInstructions.push(devGlobalInstruction);
|
||||
@@ -223,7 +220,6 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
thenBlockInstructions.push(reassignElseInstruction);
|
||||
@@ -296,7 +292,6 @@ export function inlineJsxTransform(
|
||||
],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reactElementInstruction);
|
||||
@@ -314,7 +309,6 @@ export function inlineJsxTransform(
|
||||
type: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
elseBlockInstructions.push(reassignConditionalInstruction);
|
||||
@@ -442,7 +436,6 @@ function createSymbolProperty(
|
||||
binding: {kind: 'Global', name: 'Symbol'},
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolInstruction);
|
||||
@@ -457,7 +450,6 @@ function createSymbolProperty(
|
||||
property: makePropertyLiteral('for'),
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolForInstruction);
|
||||
@@ -471,7 +463,6 @@ function createSymbolProperty(
|
||||
value: symbolName,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(symbolValueInstruction);
|
||||
@@ -487,7 +478,6 @@ function createSymbolProperty(
|
||||
args: [symbolValueInstruction.lvalue],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
const $$typeofProperty: ObjectProperty = {
|
||||
@@ -518,7 +508,6 @@ function createTagProperty(
|
||||
value: componentTag.name,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
tagProperty = {
|
||||
@@ -645,7 +634,6 @@ function createPropsProperties(
|
||||
elements: [...children],
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
nextInstructions.push(childrenPropInstruction);
|
||||
@@ -669,7 +657,6 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
refProperty = {
|
||||
@@ -691,7 +678,6 @@ function createPropsProperties(
|
||||
value: null,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
keyProperty = {
|
||||
@@ -725,7 +711,6 @@ function createPropsProperties(
|
||||
properties: props,
|
||||
loc: instr.value.loc,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: instr.loc,
|
||||
};
|
||||
propsProperty = {
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
markInstructionIds,
|
||||
promoteTemporary,
|
||||
reversePostorderBlocks,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {enterSSA} from '../SSA';
|
||||
@@ -147,7 +146,6 @@ function emitLoadLoweredContextCallee(
|
||||
id: makeInstructionId(0),
|
||||
loc: GeneratedSource,
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
value: loadGlobal,
|
||||
};
|
||||
}
|
||||
@@ -194,7 +192,6 @@ function emitPropertyLoad(
|
||||
lvalue: object,
|
||||
value: loadObj,
|
||||
id: makeInstructionId(0),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
|
||||
@@ -209,7 +206,6 @@ function emitPropertyLoad(
|
||||
lvalue: element,
|
||||
value: loadProp,
|
||||
id: makeInstructionId(0),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return {
|
||||
@@ -241,7 +237,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: arrayInstr.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
@@ -255,7 +250,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
params: [obj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
@@ -284,7 +278,6 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
lvalue: createTemporaryPlace(env, GeneratedSource),
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return fnInstr;
|
||||
@@ -301,7 +294,6 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
|
||||
id: makeInstructionId(0),
|
||||
value: array,
|
||||
lvalue: arrayLvalue,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
loc: GeneratedSource,
|
||||
};
|
||||
return arrayInstr;
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
Place,
|
||||
promoteTemporary,
|
||||
promoteTemporaryJsxTag,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR/HIR';
|
||||
import {createTemporaryPlace} from '../HIR/HIRBuilder';
|
||||
import {printIdentifier} from '../HIR/PrintHIR';
|
||||
@@ -298,7 +297,6 @@ function emitOutlinedJsx(
|
||||
},
|
||||
loc: GeneratedSource,
|
||||
},
|
||||
effects: null,
|
||||
};
|
||||
promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
|
||||
const jsxExpr: Instruction = {
|
||||
@@ -314,7 +312,6 @@ function emitOutlinedJsx(
|
||||
openingLoc: GeneratedSource,
|
||||
closingLoc: GeneratedSource,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
|
||||
return [loadJsx, jsxExpr];
|
||||
@@ -356,7 +353,6 @@ function emitOutlinedFn(
|
||||
kind: 'return',
|
||||
loc: GeneratedSource,
|
||||
value: instructions.at(-1)!.lvalue,
|
||||
effects: null,
|
||||
},
|
||||
preds: new Set(),
|
||||
phis: new Set(),
|
||||
@@ -370,7 +366,6 @@ function emitOutlinedFn(
|
||||
params: [propsObj],
|
||||
returnTypeAnnotation: null,
|
||||
returnType: makeType(),
|
||||
returns: createTemporaryPlace(env, GeneratedSource),
|
||||
context: [],
|
||||
effects: null,
|
||||
body: {
|
||||
@@ -522,7 +517,6 @@ function emitDestructureProps(
|
||||
loc: GeneratedSource,
|
||||
value: propsObj,
|
||||
},
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
return destructurePropsInstr;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
getHookKind,
|
||||
makeIdentifierName,
|
||||
} from '../HIR/HIR';
|
||||
import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
|
||||
import {printIdentifier, printPlace} from '../HIR/PrintHIR';
|
||||
import {eachPatternOperand} from '../HIR/visitors';
|
||||
import {Err, Ok, Result} from '../Utils/Result';
|
||||
import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
|
||||
@@ -1310,7 +1310,7 @@ function codegenInstructionNullable(
|
||||
});
|
||||
CompilerError.invariant(value?.type === 'FunctionExpression', {
|
||||
reason: 'Expected a function as a function declaration value',
|
||||
description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
|
||||
description: null,
|
||||
loc: instr.value.loc,
|
||||
suggestions: null,
|
||||
});
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
NonLocalImportSpecifier,
|
||||
Place,
|
||||
promoteTemporary,
|
||||
todoPopulateAliasingEffects,
|
||||
} from '../HIR';
|
||||
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
|
||||
import {getOrInsertWith} from '../Utils/utils';
|
||||
@@ -437,7 +436,6 @@ function makeLoadUseFireInstruction(
|
||||
value: instrValue,
|
||||
lvalue: {...useFirePlace},
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -462,7 +460,6 @@ function makeLoadFireCalleeInstruction(
|
||||
},
|
||||
lvalue: {...loadedFireCallee},
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -486,7 +483,6 @@ function makeCallUseFireInstruction(
|
||||
value: useFireCall,
|
||||
lvalue: {...useFireCallResultPlace},
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -515,7 +511,6 @@ function makeStoreUseFireInstruction(
|
||||
},
|
||||
lvalue: fireFunctionBindingLValuePlace,
|
||||
loc: GeneratedSource,
|
||||
effects: todoPopulateAliasingEffects(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {CompilerError} from '../CompilerError';
|
||||
import {
|
||||
CompilationMode,
|
||||
defaultOptions,
|
||||
PanicThresholdOptions,
|
||||
parsePluginOptions,
|
||||
PluginOptions,
|
||||
} from '../Entrypoint';
|
||||
@@ -18,24 +19,14 @@ import {
|
||||
EnvironmentConfigSchema,
|
||||
PartialEnvironmentConfig,
|
||||
} from '../HIR/Environment';
|
||||
import {Err, Ok, Result} from './Result';
|
||||
import {hasOwnProperty} from './utils';
|
||||
|
||||
function tryParseTestPragmaValue(val: string): Result<unknown, unknown> {
|
||||
try {
|
||||
let parsedVal: unknown;
|
||||
const stringMatch = /^"([^"]*)"$/.exec(val);
|
||||
if (stringMatch && stringMatch.length > 1) {
|
||||
parsedVal = stringMatch[1];
|
||||
} else {
|
||||
parsedVal = JSON.parse(val);
|
||||
}
|
||||
return Ok(parsedVal);
|
||||
} catch (e) {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For test fixtures and playground only.
|
||||
*
|
||||
* Pragmas are straightforward to parse for boolean options (`:true` and
|
||||
* `:false`). These are 'enabled' config values for non-boolean configs (i.e.
|
||||
* what is used when parsing `:true`).
|
||||
*/
|
||||
const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
validateNoCapitalizedCalls: [],
|
||||
enableChangeDetectionForDebugging: {
|
||||
@@ -93,37 +84,34 @@ const testComplexConfigDefaults: PartialEnvironmentConfig = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* For snap test fixtures and playground only.
|
||||
*/
|
||||
function parseConfigPragmaEnvironmentForTest(
|
||||
pragma: string,
|
||||
): EnvironmentConfig {
|
||||
const maybeConfig: Partial<Record<keyof EnvironmentConfig, unknown>> = {};
|
||||
const maybeConfig: any = {};
|
||||
// Get the defaults to programmatically check for boolean properties
|
||||
const defaultConfig = EnvironmentConfigSchema.parse({});
|
||||
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
const valIdx = keyVal.indexOf(':');
|
||||
const key = valIdx === -1 ? keyVal : keyVal.slice(0, valIdx);
|
||||
const val = valIdx === -1 ? undefined : keyVal.slice(valIdx + 1);
|
||||
let [key, val = undefined] = keyVal.split(':');
|
||||
const isSet = val === undefined || val === 'true';
|
||||
if (!hasOwnProperty(EnvironmentConfigSchema.shape, key)) {
|
||||
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] =
|
||||
testComplexConfigDefaults[key as keyof PartialEnvironmentConfig];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSet && key in testComplexConfigDefaults) {
|
||||
maybeConfig[key] = testComplexConfigDefaults[key];
|
||||
} else if (isSet) {
|
||||
maybeConfig[key] = true;
|
||||
} else if (val === 'false') {
|
||||
maybeConfig[key] = false;
|
||||
} else if (val) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'customMacros' && typeof parsedVal === 'string') {
|
||||
const valSplit = parsedVal.split('.');
|
||||
if (key === 'customMacros' && val) {
|
||||
const valSplit = val.split('.');
|
||||
if (valSplit.length > 0) {
|
||||
const props = [];
|
||||
for (const elt of valSplit.slice(1)) {
|
||||
if (elt === '*') {
|
||||
@@ -133,9 +121,21 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
}
|
||||
}
|
||||
maybeConfig[key] = [[valSplit[0], props]];
|
||||
continue;
|
||||
}
|
||||
maybeConfig[key] = parsedVal;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
key !== 'enableResetCacheOnSourceFileChanges' &&
|
||||
typeof defaultConfig[key as keyof EnvironmentConfig] !== 'boolean'
|
||||
) {
|
||||
// skip parsing non-boolean properties
|
||||
continue;
|
||||
}
|
||||
if (val === undefined || val === 'true') {
|
||||
maybeConfig[key] = true;
|
||||
} else {
|
||||
maybeConfig[key] = false;
|
||||
}
|
||||
}
|
||||
const config = EnvironmentConfigSchema.safeParse(maybeConfig);
|
||||
@@ -156,13 +156,6 @@ function parseConfigPragmaEnvironmentForTest(
|
||||
suggestions: null,
|
||||
});
|
||||
}
|
||||
|
||||
const testComplexPluginOptionDefaults: Partial<PluginOptions> = {
|
||||
gating: {
|
||||
source: 'ReactForgetFeatureFlag',
|
||||
importSpecifierName: 'isForgetEnabled_Fixtures',
|
||||
},
|
||||
};
|
||||
export function parseConfigPragmaForTests(
|
||||
pragma: string,
|
||||
defaults: {
|
||||
@@ -170,41 +163,44 @@ export function parseConfigPragmaForTests(
|
||||
},
|
||||
): PluginOptions {
|
||||
const environment = parseConfigPragmaEnvironmentForTest(pragma);
|
||||
const options: Record<keyof PluginOptions, unknown> = {
|
||||
...defaultOptions,
|
||||
panicThreshold: 'all_errors',
|
||||
compilationMode: defaults.compilationMode,
|
||||
environment,
|
||||
};
|
||||
let compilationMode: CompilationMode = defaults.compilationMode;
|
||||
let panicThreshold: PanicThresholdOptions = 'all_errors';
|
||||
let noEmit: boolean = defaultOptions.noEmit;
|
||||
for (const token of pragma.split(' ')) {
|
||||
if (!token.startsWith('@')) {
|
||||
continue;
|
||||
}
|
||||
const keyVal = token.slice(1);
|
||||
const idx = keyVal.indexOf(':');
|
||||
const key = idx === -1 ? keyVal : keyVal.slice(0, idx);
|
||||
const val = idx === -1 ? undefined : keyVal.slice(idx + 1);
|
||||
if (!hasOwnProperty(defaultOptions, key)) {
|
||||
continue;
|
||||
}
|
||||
const isSet = val === undefined || val === 'true';
|
||||
if (isSet && key in testComplexPluginOptionDefaults) {
|
||||
options[key] = testComplexPluginOptionDefaults[key];
|
||||
} else if (isSet) {
|
||||
options[key] = true;
|
||||
} else if (val === 'false') {
|
||||
options[key] = false;
|
||||
} else if (val != null) {
|
||||
const parsedVal = tryParseTestPragmaValue(val).unwrap();
|
||||
if (key === 'target' && parsedVal === 'donotuse_meta_internal') {
|
||||
options[key] = {
|
||||
kind: parsedVal,
|
||||
runtimeModule: 'react',
|
||||
};
|
||||
} else {
|
||||
options[key] = parsedVal;
|
||||
switch (token) {
|
||||
case '@compilationMode(annotation)': {
|
||||
compilationMode = 'annotation';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(infer)': {
|
||||
compilationMode = 'infer';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(all)': {
|
||||
compilationMode = 'all';
|
||||
break;
|
||||
}
|
||||
case '@compilationMode(syntax)': {
|
||||
compilationMode = 'syntax';
|
||||
break;
|
||||
}
|
||||
case '@panicThreshold(none)': {
|
||||
panicThreshold = 'none';
|
||||
break;
|
||||
}
|
||||
case '@noEmit': {
|
||||
noEmit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsePluginOptions(options);
|
||||
return parsePluginOptions({
|
||||
environment,
|
||||
compilationMode,
|
||||
panicThreshold,
|
||||
noEmit,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,21 +121,6 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `a` is a superset of `b`.
|
||||
*/
|
||||
export function Set_isSuperset<T>(
|
||||
a: ReadonlySet<T>,
|
||||
b: ReadonlySet<T>,
|
||||
): boolean {
|
||||
for (const v of b) {
|
||||
if (!a.has(v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function Iterable_some<T>(
|
||||
iter: Iterable<T>,
|
||||
pred: (item: T) => boolean,
|
||||
@@ -148,19 +133,6 @@ export function Iterable_some<T>(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function Iterable_filter<T>(
|
||||
iter: Iterable<T>,
|
||||
pred: (item: T) => boolean,
|
||||
): Array<T> {
|
||||
const result: Array<T> = [];
|
||||
for (const item of iter) {
|
||||
if (pred(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function nonNull<T extends NonNullable<U>, U>(
|
||||
value: T | null | undefined,
|
||||
): value is T {
|
||||
|
||||
@@ -58,7 +58,8 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
const effect = contextMutationEffects.get(operand.identifier.id);
|
||||
if (effect != null) {
|
||||
errors.push({
|
||||
reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
|
||||
reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
|
||||
description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
|
||||
loc: operand.loc,
|
||||
severity: ErrorSeverity.InvalidReact,
|
||||
});
|
||||
@@ -111,55 +112,6 @@ export function validateNoFreezingKnownMutableFunctions(
|
||||
);
|
||||
if (knownMutation && knownMutation.kind === 'ContextMutation') {
|
||||
contextMutationEffects.set(lvalue.identifier.id, knownMutation);
|
||||
} else if (
|
||||
fn.env.config.enableNewMutationAliasingModel &&
|
||||
value.loweredFunc.func.aliasingEffects != null
|
||||
) {
|
||||
const context = new Set(
|
||||
value.loweredFunc.func.context.map(p => p.identifier.id),
|
||||
);
|
||||
effects: for (const effect of value.loweredFunc.func
|
||||
.aliasingEffects) {
|
||||
switch (effect.kind) {
|
||||
case 'Mutate':
|
||||
case 'MutateTransitive': {
|
||||
const knownMutation = contextMutationEffects.get(
|
||||
effect.value.identifier.id,
|
||||
);
|
||||
if (knownMutation != null) {
|
||||
contextMutationEffects.set(
|
||||
lvalue.identifier.id,
|
||||
knownMutation,
|
||||
);
|
||||
} else if (
|
||||
context.has(effect.value.identifier.id) &&
|
||||
!isRefOrRefLikeMutableType(effect.value.identifier.type)
|
||||
) {
|
||||
contextMutationEffects.set(lvalue.identifier.id, {
|
||||
kind: 'ContextMutation',
|
||||
effect: Effect.Mutate,
|
||||
loc: effect.value.loc,
|
||||
places: new Set([effect.value]),
|
||||
});
|
||||
break effects;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MutateConditionally':
|
||||
case 'MutateTransitiveConditionally': {
|
||||
const knownMutation = contextMutationEffects.get(
|
||||
effect.value.identifier.id,
|
||||
);
|
||||
if (knownMutation != null) {
|
||||
contextMutationEffects.set(
|
||||
lvalue.identifier.id,
|
||||
knownMutation,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
@@ -15,7 +15,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
|
||||
const Test = () => {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
const Test = () => <div />;
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
|
||||
// @flow @enableTransitivelyFreezeFunctionExpressions:false
|
||||
import {setPropertyByKey, Stringify} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -57,7 +56,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { makeArray, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {makeArray, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -39,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
@@ -40,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { identity, mutate } from "shared-runtime";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {identity, mutate} from 'shared-runtime';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
const Codes = {
|
||||
en: {name: 'English'},
|
||||
ja: {name: 'Japanese'},
|
||||
ko: {name: 'Korean'},
|
||||
zh: {name: 'Chinese'},
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
let keys;
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const options = keys.map(code => {
|
||||
const country = Codes[code];
|
||||
return {
|
||||
name: country.name,
|
||||
code,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: false}],
|
||||
sequentialRenders: [
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: true},
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: false},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
const Codes = {
|
||||
en: { name: "English" },
|
||||
ja: { name: "Japanese" },
|
||||
ko: { name: "Korean" },
|
||||
zh: { name: "Chinese" },
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
const $ = _c(4);
|
||||
let keys;
|
||||
if (a) {
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = Object.keys(Codes);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
keys = t0;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
let t0;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = keys.map(_temp);
|
||||
$[1] = t0;
|
||||
} else {
|
||||
t0 = $[1];
|
||||
}
|
||||
const options = t0;
|
||||
let t1;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = (
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
);
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = (
|
||||
<>
|
||||
{t1}
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
return t2;
|
||||
}
|
||||
function _temp(code) {
|
||||
const country = Codes[code];
|
||||
return { name: country.name, code };
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{ a: false }],
|
||||
sequentialRenders: [
|
||||
{ a: false },
|
||||
{ a: true },
|
||||
{ a: true },
|
||||
{ a: false },
|
||||
{ a: true },
|
||||
{ a: false },
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
const Codes = {
|
||||
en: {name: 'English'},
|
||||
ja: {name: 'Japanese'},
|
||||
ko: {name: 'Korean'},
|
||||
zh: {name: 'Chinese'},
|
||||
};
|
||||
|
||||
function Component(a) {
|
||||
let keys;
|
||||
if (a) {
|
||||
keys = Object.keys(Codes);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const options = keys.map(code => {
|
||||
const country = Codes[code];
|
||||
return {
|
||||
name: country.name,
|
||||
code,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
|
||||
<ValidateMemoization
|
||||
inputs={[]}
|
||||
output={options}
|
||||
onlyCheckCompiled={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{a: false}],
|
||||
sequentialRenders: [
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: true},
|
||||
{a: false},
|
||||
{a: true},
|
||||
{a: false},
|
||||
],
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
@@ -22,7 +22,7 @@ class Component {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
class Component {
|
||||
_renderMessage = () => {
|
||||
const Message = () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
@@ -24,7 +24,7 @@ function Foo(props) {
|
||||
|
||||
```javascript
|
||||
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @compilationMode:"infer"
|
||||
// @flow @compilationMode(infer)
|
||||
export default component Foo(bar: number) {
|
||||
return <Bar bar={bar} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow @compilationMode:"infer"
|
||||
// @flow @compilationMode(infer)
|
||||
export default component Foo(bar: number) {
|
||||
return <Bar bar={bar} />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
shouldInstrument as _shouldInstrument3,
|
||||
useRenderCounter,
|
||||
} from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
import { c as _c } from "react/compiler-runtime"; // @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
import { identity } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation"
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation)
|
||||
|
||||
import {identity} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
try {
|
||||
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
|
||||
} catch {}
|
||||
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
`` === '',
|
||||
`\n` === '\n',
|
||||
`a\nb`,
|
||||
`\n`,
|
||||
`a${1}b`,
|
||||
` abc \u0041\n\u000a\ŧ`,
|
||||
`abc${1}def`,
|
||||
`abc${1}def${2}`,
|
||||
`abc${1}def${2}ghi`,
|
||||
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
|
||||
`1${2}${Math.sin(0)}`,
|
||||
`${NaN}`,
|
||||
`${Infinity}`,
|
||||
`${-Infinity}`,
|
||||
`${Number.MAX_SAFE_INTEGER}`,
|
||||
`${Number.MIN_SAFE_INTEGER}`,
|
||||
`${Number.MAX_VALUE}`,
|
||||
`${Number.MIN_VALUE}`,
|
||||
`${-0}`,
|
||||
`
|
||||
`,
|
||||
`${{}}`,
|
||||
`${[1, 2, 3]}`,
|
||||
`${true}`,
|
||||
`${false}`,
|
||||
`${null}`,
|
||||
`${undefined}`,
|
||||
`123456789${0}`,
|
||||
`${0}123456789`,
|
||||
`${0}123456789${0}`,
|
||||
`${0}1234${5}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
|
||||
`${`${`${`${0}`}`}`}`,
|
||||
`${`${`${`${''}`}`}`}`,
|
||||
`${`${`${`${identity('')}`}`}`}`,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { Stringify, identity } from "shared-runtime";
|
||||
|
||||
function foo() {
|
||||
const $ = _c(1);
|
||||
try {
|
||||
identity(`${Symbol("0")}`);
|
||||
} catch {}
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = (
|
||||
<Stringify
|
||||
value={[
|
||||
true,
|
||||
true,
|
||||
|
||||
"a\nb",
|
||||
"\n",
|
||||
"a1b",
|
||||
" abc A\n\n\u0167",
|
||||
"abc1def",
|
||||
"abc1def2",
|
||||
"abc1def2ghi",
|
||||
"a4bcde6f",
|
||||
`1${2}${Math.sin(0)}`,
|
||||
`${NaN}`,
|
||||
`${Infinity}`,
|
||||
`${-Infinity}`,
|
||||
`${Number.MAX_SAFE_INTEGER}`,
|
||||
`${Number.MIN_SAFE_INTEGER}`,
|
||||
`${Number.MAX_VALUE}`,
|
||||
`${Number.MIN_VALUE}`,
|
||||
"0",
|
||||
"\n ",
|
||||
|
||||
`${{}}`,
|
||||
`${[1, 2, 3]}`,
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
`${undefined}`,
|
||||
"1234567890",
|
||||
"0123456789",
|
||||
"01234567890",
|
||||
"01234567890",
|
||||
"0123401234567890123456789067890",
|
||||
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
|
||||
"0",
|
||||
"",
|
||||
`${`${`${`${identity("")}`}`}`}`,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
### Eval output
|
||||
(kind: ok) <div>{"value":[true,true,"a\nb","\n","a1b"," abc A\n\nŧ","abc1def","abc1def2","abc1def2ghi","a4bcde6f","120","NaN","Infinity","-Infinity","9007199254740991","-9007199254740991","1.7976931348623157e+308","5e-324","0","\n ","[object Object]","1,2,3","true","false","null","undefined","1234567890","0123456789","01234567890","01234567890","0123401234567890123456789067890","012340123456789067890","0","",""]}</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
import {Stringify, identity} from 'shared-runtime';
|
||||
|
||||
function foo() {
|
||||
try {
|
||||
identity(`${Symbol('0')}`); // Uncaught TypeError: Cannot convert a Symbol value to a string (leave as is)
|
||||
} catch {}
|
||||
|
||||
return (
|
||||
<Stringify
|
||||
value={[
|
||||
`` === '',
|
||||
`\n` === '\n',
|
||||
`a\nb`,
|
||||
`\n`,
|
||||
`a${1}b`,
|
||||
` abc \u0041\n\u000a\ŧ`,
|
||||
`abc${1}def`,
|
||||
`abc${1}def${2}`,
|
||||
`abc${1}def${2}ghi`,
|
||||
`a${1 + 3}b${``}c${'d' + `e${2 + 4}f`}`,
|
||||
`1${2}${Math.sin(0)}`,
|
||||
`${NaN}`,
|
||||
`${Infinity}`,
|
||||
`${-Infinity}`,
|
||||
`${Number.MAX_SAFE_INTEGER}`,
|
||||
`${Number.MIN_SAFE_INTEGER}`,
|
||||
`${Number.MAX_VALUE}`,
|
||||
`${Number.MIN_VALUE}`,
|
||||
`${-0}`,
|
||||
`
|
||||
`,
|
||||
`${{}}`,
|
||||
`${[1, 2, 3]}`,
|
||||
`${true}`,
|
||||
`${false}`,
|
||||
`${null}`,
|
||||
`${undefined}`,
|
||||
`123456789${0}`,
|
||||
`${0}123456789`,
|
||||
`${0}123456789${0}`,
|
||||
`${0}1234${5}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${0}123456789${0}`}`}6789${0}`,
|
||||
`${0}1234${`${0}123456789${`${identity(0)}`}`}6789${0}`,
|
||||
`${`${`${`${0}`}`}`}`,
|
||||
`${`${`${`${''}`}`}`}`,
|
||||
`${`${`${`${identity('')}`}`}`}`,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: foo,
|
||||
params: [],
|
||||
isComponent: false,
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
@@ -16,13 +15,13 @@ function Component() {
|
||||
## Error
|
||||
|
||||
```
|
||||
2 | function Component() {
|
||||
3 | const foo = () => {
|
||||
> 4 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: 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) (4:4)
|
||||
5 | };
|
||||
6 | return <div {...foo} />;
|
||||
7 | }
|
||||
1 | function Component() {
|
||||
2 | const foo = () => {
|
||||
> 3 | someGlobal = true;
|
||||
| ^^^^^^^^^^ InvalidReact: 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) (3:3)
|
||||
4 | };
|
||||
5 | return <div {...foo} />;
|
||||
6 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Component() {
|
||||
const foo = () => {
|
||||
someGlobal = true;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @eslintSuppressionRules:["my-app","react-rule"]
|
||||
// @eslintSuppressionRules(my-app/react-rule)
|
||||
|
||||
/* eslint-disable my-app/react-rule */
|
||||
function lowercasecomponent() {
|
||||
@@ -19,7 +19,7 @@ function lowercasecomponent() {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @eslintSuppressionRules:["my-app","react-rule"]
|
||||
1 | // @eslintSuppressionRules(my-app/react-rule)
|
||||
2 |
|
||||
> 3 | /* eslint-disable my-app/react-rule */
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior. eslint-disable my-app/react-rule (3:3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @eslintSuppressionRules:["my-app","react-rule"]
|
||||
// @eslintSuppressionRules(my-app/react-rule)
|
||||
|
||||
/* eslint-disable my-app/react-rule */
|
||||
function lowercasecomponent() {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
18 | );
|
||||
19 | const ref = useRef(null);
|
||||
> 20 | useEffect(() => {
|
||||
| ^^^^^^^
|
||||
> 21 | if (ref.current === null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 22 | update();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | }, [update]);
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (14:14)
|
||||
25 |
|
||||
26 | return 'ok';
|
||||
27 | }
|
||||
```
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
|
||||
|
||||
import {useCallback, useEffect, useRef} from 'react';
|
||||
import {useHook} from 'shared-runtime';
|
||||
|
||||
function Component() {
|
||||
const params = useHook();
|
||||
const update = useCallback(
|
||||
partialParams => {
|
||||
const nextParams = {
|
||||
...params,
|
||||
...partialParams,
|
||||
};
|
||||
nextParams.param = 'value';
|
||||
console.log(nextParams);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
if (ref.current === null) {
|
||||
update();
|
||||
}
|
||||
}, [update]);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
@@ -24,7 +24,7 @@ function useFoo() {
|
||||
> 6 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 7 | });
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (5:7)
|
||||
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (6:6)
|
||||
8 | }
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel
|
||||
import {Stringify, useIdentity} from 'shared-runtime';
|
||||
|
||||
function Component({prop1, prop2}) {
|
||||
'use memo';
|
||||
|
||||
const data = useIdentity(
|
||||
new Map([
|
||||
[0, 'value0'],
|
||||
[1, 'value1'],
|
||||
])
|
||||
);
|
||||
let i = 0;
|
||||
const items = [];
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop1}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
i = i + 1;
|
||||
items.push(
|
||||
<Stringify
|
||||
key={i}
|
||||
onClick={() => data.get(i) + prop2}
|
||||
shouldInvokeFns={true}
|
||||
/>
|
||||
);
|
||||
return <>{items}</>;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
fn: Component,
|
||||
params: [{prop1: 'prop1', prop2: 'prop2'}],
|
||||
sequentialRenders: [
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'prop1', prop2: 'prop2'},
|
||||
{prop1: 'changed', prop2: 'prop2'},
|
||||
],
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
20 | />
|
||||
21 | );
|
||||
> 22 | i = i + 1;
|
||||
| ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. Found mutation of `i` (22:22)
|
||||
23 | items.push(
|
||||
24 | <Stringify
|
||||
25 | key={i}
|
||||
```
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ function Component() {
|
||||
5 | cache.set('key', 'value');
|
||||
6 | };
|
||||
> 7 | return <Foo fn={fn} />;
|
||||
| ^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:7)
|
||||
| ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (5:5)
|
||||
8 | }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
function Component({ref}) {
|
||||
const value = ref.current;
|
||||
return <div>{value}</div>;
|
||||
@@ -14,7 +14,7 @@ function Component({ref}) {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
1 | // @validateRefAccessDuringRender @compilationMode(infer)
|
||||
2 | function Component({ref}) {
|
||||
> 3 | const value = ref.current;
|
||||
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
function Component({ref}) {
|
||||
const value = ref.current;
|
||||
return <div>{value}</div>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
function Component(props) {
|
||||
const value = props.ref.current;
|
||||
return <div>{value}</div>;
|
||||
@@ -14,7 +14,7 @@ function Component(props) {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
1 | // @validateRefAccessDuringRender @compilationMode(infer)
|
||||
2 | function Component(props) {
|
||||
> 3 | const value = props.ref.current;
|
||||
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
function Component(props) {
|
||||
const value = props.ref.current;
|
||||
return <div>{value}</div>;
|
||||
|
||||
@@ -26,7 +26,7 @@ function useFoo() {
|
||||
> 8 | cache.set('key', 'value');
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 9 | };
|
||||
| ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:9)
|
||||
| ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (8:8)
|
||||
10 | }
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @flow @enableNewMutationAliasingModel
|
||||
/**
|
||||
* This hook returns a function that when called with an input object,
|
||||
* will return the result of mapping that input with the supplied map
|
||||
* function. Results are cached, so if the same input is passed again,
|
||||
* the same output object will be returned.
|
||||
*
|
||||
* Note that this technically violates the rules of React and is unsafe:
|
||||
* hooks must return immutable objects and be pure, and a function which
|
||||
* captures and mutates a value when called is inherently not pure.
|
||||
*
|
||||
* However, in this case it is technically safe _if_ the mapping function
|
||||
* is pure *and* the resulting objects are never modified. This is because
|
||||
* the function only caches: the result of `returnedFunction(someInput)`
|
||||
* strictly depends on `returnedFunction` and `someInput`, and cannot
|
||||
* otherwise change over time.
|
||||
*/
|
||||
hook useMemoMap<TInput: interface {}, TOutput>(
|
||||
map: TInput => TOutput
|
||||
): TInput => TOutput {
|
||||
return useMemo(() => {
|
||||
// The original issue is that `cache` was not memoized together with the returned
|
||||
// function. This was because neither appears to ever be mutated — the function
|
||||
// is known to mutate `cache` but the function isn't called.
|
||||
//
|
||||
// The fix is to detect cases like this — functions that are mutable but not called -
|
||||
// and ensure that their mutable captures are aliased together into the same scope.
|
||||
const cache = new WeakMap<TInput, TOutput>();
|
||||
return input => {
|
||||
let output = cache.get(input);
|
||||
if (output == null) {
|
||||
output = map(input);
|
||||
cache.set(input, output);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
}, [map]);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Error
|
||||
|
||||
```
|
||||
19 | map: TInput => TOutput
|
||||
20 | ): TInput => TOutput {
|
||||
> 21 | return useMemo(() => {
|
||||
| ^^^^^^^^^^^^^^^
|
||||
> 22 | // The original issue is that `cache` was not memoized together with the returned
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 23 | // function. This was because neither appears to ever be mutated — the function
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 24 | // is known to mutate `cache` but the function isn't called.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 25 | //
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 27 | // and ensure that their mutable captures are aliased together into the same scope.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 28 | const cache = new WeakMap<TInput, TOutput>();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 29 | return input => {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 30 | let output = cache.get(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 31 | if (output == null) {
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 32 | output = map(input);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 33 | cache.set(input, output);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 34 | }
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 35 | return output;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 36 | };
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
> 37 | }, [map]);
|
||||
| ^^^^^^^^^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (21:37)
|
||||
|
||||
InvalidReact: The function modifies a local variable here (33:33)
|
||||
38 | }
|
||||
39 |
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
function Component(props) {
|
||||
const ref = props.ref;
|
||||
ref.current = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateRefAccessDuringRender @compilationMode:"infer"
|
||||
// @validateRefAccessDuringRender @compilationMode(infer)
|
||||
function Component(props) {
|
||||
const ref = props.ref;
|
||||
ref.current = true;
|
||||
|
||||
@@ -18,7 +18,7 @@ function Component(props) {
|
||||
a.property = true;
|
||||
b.push(false);
|
||||
};
|
||||
return <div onClick={f} />;
|
||||
return <div onClick={f()} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -14,7 +14,7 @@ function Component(props) {
|
||||
a.property = true;
|
||||
b.push(false);
|
||||
};
|
||||
return <div onClick={f} />;
|
||||
return <div onClick={f()} />;
|
||||
}
|
||||
|
||||
export const FIXTURE_ENTRYPOINT = {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
@@ -22,13 +21,13 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Error
|
||||
|
||||
```
|
||||
2 | function Foo() {
|
||||
3 | const x = () => {
|
||||
> 4 | window.href = 'foo';
|
||||
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (4:4)
|
||||
5 | };
|
||||
6 | const y = {x};
|
||||
7 | return <Bar y={y} />;
|
||||
1 | function Foo() {
|
||||
2 | const x = () => {
|
||||
> 3 | window.href = 'foo';
|
||||
| ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (3:3)
|
||||
4 | };
|
||||
5 | const y = {x};
|
||||
6 | return <Bar y={y} />;
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @enableNewMutationAliasingModel:false
|
||||
function Foo() {
|
||||
const x = () => {
|
||||
window.href = 'foo';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
function Component() {
|
||||
return <Foo />;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
function Component() {
|
||||
return <Foo />;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ function Component(props) {
|
||||
7 | return hasErrors;
|
||||
8 | }
|
||||
> 9 | return hasErrors();
|
||||
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$15 (9:9)
|
||||
| ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9)
|
||||
10 | }
|
||||
11 |
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @validateBlocklistedImports:["DangerousImport"]
|
||||
// @validateBlocklistedImports(DangerousImport)
|
||||
import {foo} from 'DangerousImport';
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
@@ -17,7 +17,7 @@ function useHook() {
|
||||
## Error
|
||||
|
||||
```
|
||||
1 | // @validateBlocklistedImports:["DangerousImport"]
|
||||
1 | // @validateBlocklistedImports(DangerousImport)
|
||||
> 2 | import {foo} from 'DangerousImport';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Bailing out due to blocklisted import. Import from module DangerousImport (2:2)
|
||||
3 | import {useIdentity} from 'shared-runtime';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @validateBlocklistedImports:["DangerousImport"]
|
||||
// @validateBlocklistedImports(DangerousImport)
|
||||
import {foo} from 'DangerousImport';
|
||||
import {useIdentity} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
@@ -43,7 +43,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer"
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer)
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer"
|
||||
// @compilationMode(infer)
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
|
||||
// @compilationMode(infer) @enableResetCacheOnSourceFileChanges
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
@@ -46,7 +46,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
|
||||
import { c as _c } from "react/compiler-runtime"; // @compilationMode(infer) @enableResetCacheOnSourceFileChanges
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ValidateMemoization } from "shared-runtime";
|
||||
|
||||
@@ -63,12 +63,12 @@ function unsafeUpdateConst() {
|
||||
function Component() {
|
||||
const $ = _c(3);
|
||||
if (
|
||||
$[0] !== "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7"
|
||||
$[0] !== "8d7015668f857996c3d895a7a90e3e16b8a791d5b9cd13f2c76e1c254aeedebb"
|
||||
) {
|
||||
for (let $i = 0; $i < 3; $i += 1) {
|
||||
$[$i] = Symbol.for("react.memo_cache_sentinel");
|
||||
}
|
||||
$[0] = "a585d27423c1181e7b6305ff909458183d284658c3c3d2e3764e1128be302fd7";
|
||||
$[0] = "8d7015668f857996c3d895a7a90e3e16b8a791d5b9cd13f2c76e1c254aeedebb";
|
||||
}
|
||||
useState(_temp);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @compilationMode:"infer" @enableResetCacheOnSourceFileChanges
|
||||
// @compilationMode(infer) @enableResetCacheOnSourceFileChanges
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {ValidateMemoization} from 'shared-runtime';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
## Input
|
||||
|
||||
```javascript
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation" @gating
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation) @gating
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
@@ -38,7 +38,7 @@ export const FIXTURE_ENTRYPOINT = {
|
||||
```javascript
|
||||
import { shouldInstrument, useRenderCounter } from "react-compiler-runtime";
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @enableEmitInstrumentForget @compilationMode:"annotation" @gating
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag"; // @enableEmitInstrumentForget @compilationMode(annotation) @gating
|
||||
const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
"use forget";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @enableEmitInstrumentForget @compilationMode:"annotation" @gating
|
||||
// @enableEmitInstrumentForget @compilationMode(annotation) @gating
|
||||
|
||||
function Bar(props) {
|
||||
'use forget';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user