[compiler] clean up retry pipeline: fireRetry flag -> compileMode

Removes `EnvironmentConfig.enableMinimalTransformsForRetry` in favor of `run` parameters. This is a minimal difference but lets us explicitly opt out certain compiler passes based on mode parameters, instead of environment configurations

Retry flags don't really make sense to have in `EnvironmentConfig` anyways as the config is user-facing API, while retrying is a compiler implementation detail.

(per @josephsavona's feedback https://github.com/facebook/react/pull/32164#issuecomment-2608616479)
> Re the "hacky" framing of this in the PR title: I think this is fine. I can see having something like a compilation or output mode that we use when running the pipeline. Rather than changing environment settings when we re-run, various passes could take effect based on the combination of the mode + env flags. The modes might be:
>
> * Full: transform, validate, memoize. This is the default today.
> * Transform: Along the lines of the backup mode in this PR. Only applies transforms that do not require following the rules of React, like `fire()`.
> * Validate: This could be used for ESLint.
This commit is contained in:
Mofei Zhang
2025-03-13 19:42:49 -04:00
parent d92e5713be
commit 4e9bdc25fd
18 changed files with 115 additions and 93 deletions
@@ -181,6 +181,7 @@ function ReactForgetFunctionTransform() {
fn,
forgetOptions,
'Other',
'all_features',
'_c',
null,
null,
@@ -24,6 +24,7 @@ import {
pruneUnusedLabelsHIR,
} from '../HIR';
import {
CompilerMode,
Environment,
EnvironmentConfig,
ReactFunctionType,
@@ -100,6 +101,7 @@ import {outlineJSX} from '../Optimization/OutlineJsx';
import {optimizePropsMethodCalls} from '../Optimization/OptimizePropsMethodCalls';
import {transformFire} from '../Transform';
import {validateNoImpureFunctionsInRender} from '../Validation/ValiateNoImpureFunctionsInRender';
import {CompilerError} from '..';
export type CompilerPipelineValue =
| {kind: 'ast'; name: string; value: CodegenFunction}
@@ -113,6 +115,7 @@ function run(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
useMemoCacheIdentifier: string,
logger: Logger | null,
filename: string | null,
@@ -122,6 +125,7 @@ function run(
const env = new Environment(
func.scope,
fnType,
mode,
config,
contextIdentifiers,
logger,
@@ -160,10 +164,10 @@ function runWithEnvironment(
validateUseMemo(hir);
if (
env.isInferredMemoEnabled &&
!env.config.enablePreserveExistingManualUseMemo &&
!env.config.disableMemoizationForDebugging &&
!env.config.enableChangeDetectionForDebugging &&
!env.config.enableMinimalTransformsForRetry
!env.config.enableChangeDetectionForDebugging
) {
dropManualMemoization(hir);
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
@@ -196,8 +200,13 @@ function runWithEnvironment(
inferTypes(hir);
log({kind: 'hir', name: 'InferTypes', value: hir});
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
if (env.isInferredMemoEnabled) {
if (env.config.validateHooksUsage) {
validateHooksUsage(hir);
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
}
}
if (env.config.enableFire) {
@@ -205,10 +214,6 @@ function runWithEnvironment(
log({kind: 'hir', name: 'TransformFire', value: hir});
}
if (env.config.validateNoCapitalizedCalls) {
validateNoCapitalizedCalls(hir);
}
if (env.config.lowerContextAccess) {
lowerContextAccess(hir, env.config.lowerContextAccess);
}
@@ -219,7 +224,12 @@ function runWithEnvironment(
analyseFunctions(hir);
log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
inferReferenceEffects(hir);
const fnEffectErrors = inferReferenceEffects(hir);
if (env.isInferredMemoEnabled) {
if (fnEffectErrors.length > 0) {
CompilerError.throw(fnEffectErrors[0]);
}
}
log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
validateLocalsNotReassignedAfterRender(hir);
@@ -239,28 +249,30 @@ function runWithEnvironment(
inferMutableRanges(hir);
log({kind: 'hir', name: 'InferMutableRanges', value: hir});
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.isInferredMemoEnabled) {
if (env.config.assertValidMutableRanges) {
assertValidMutableRanges(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
}
if (env.config.validateRefAccessDuringRender) {
validateNoRefAccessInRender(hir);
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
}
if (env.config.validateNoSetStateInRender) {
validateNoSetStateInRender(hir);
}
if (env.config.validateNoSetStateInPassiveEffects) {
validateNoSetStateInPassiveEffects(hir);
}
if (env.config.validateNoSetStateInPassiveEffects) {
validateNoSetStateInPassiveEffects(hir);
}
if (env.config.validateNoJSXInTryStatements) {
validateNoJSXInTryStatement(hir);
}
if (env.config.validateNoJSXInTryStatements) {
validateNoJSXInTryStatement(hir);
}
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir);
if (env.config.validateNoImpureFunctionsInRender) {
validateNoImpureFunctionsInRender(hir);
}
}
inferReactivePlaces(hir);
@@ -280,7 +292,12 @@ function runWithEnvironment(
value: hir,
});
if (!env.config.enableMinimalTransformsForRetry) {
if (env.isInferredMemoEnabled) {
/**
* Only create reactive scopes (which directly map to generated memo blocks)
* if inferred memoization is enabled. This makes all later passes which
* transform reactive-scope labeled instructions no-ops.
*/
inferReactiveScopeVariables(hir);
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
}
@@ -529,6 +546,7 @@ export function compileFn(
>,
config: EnvironmentConfig,
fnType: ReactFunctionType,
mode: CompilerMode,
useMemoCacheIdentifier: string,
logger: Logger | null,
filename: string | null,
@@ -538,6 +556,7 @@ export function compileFn(
func,
config,
fnType,
mode,
useMemoCacheIdentifier,
logger,
filename,
@@ -16,7 +16,6 @@ import {
EnvironmentConfig,
ExternalFunction,
ReactFunctionType,
MINIMAL_RETRY_CONFIG,
} from '../HIR/Environment';
import {CodegenFunction} from '../ReactiveScopes';
import {isComponentDeclaration} from '../Utils/ComponentDeclaration';
@@ -407,6 +406,7 @@ export function compileProgram(
fn,
environment,
fnType,
'all_features',
useMemoCacheIdentifier.name,
pass.opts.logger,
pass.filename,
@@ -417,28 +417,7 @@ export function compileProgram(
compileResult = {kind: 'error', error: err};
}
}
// If non-memoization features are enabled, retry regardless of error kind
if (compileResult.kind === 'error' && environment.enableFire) {
try {
compileResult = {
kind: 'compile',
compiledFn: compileFn(
fn,
{
...environment,
...MINIMAL_RETRY_CONFIG,
},
fnType,
useMemoCacheIdentifier.name,
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
@@ -449,7 +428,28 @@ export function compileProgram(
} else {
handleError(compileResult.error, pass, fn.node.loc ?? null);
}
return null;
// If non-memoization features are enabled, retry regardless of error kind
if (!environment.enableFire) {
return null;
}
try {
compileResult = {
kind: 'compile',
compiledFn: compileFn(
fn,
environment,
fnType,
'no_inferred_memo',
useMemoCacheIdentifier.name,
pass.opts.logger,
pass.filename,
pass.code,
),
};
} catch (err) {
// TODO: we might want to log error here, but this will also result in duplicate logging
return null;
}
}
pass.opts.logger?.logEvent(pass.filename, {
@@ -96,6 +96,8 @@ export const MacroSchema = z.union([
z.tuple([z.string(), z.array(MacroMethodSchema)]),
]);
export type CompilerMode = 'all_features' | 'no_inferred_memo';
export type Macro = z.infer<typeof MacroSchema>;
export type MacroMethod = z.infer<typeof MacroMethodSchema>;
@@ -550,8 +552,6 @@ const EnvironmentConfigSchema = z.object({
*/
disableMemoizationForDebugging: z.boolean().default(false),
enableMinimalTransformsForRetry: z.boolean().default(false),
/**
* When true, rather using memoized values, the compiler will always re-compute
* values, and then use a heuristic to compare the memoized value to the newly
@@ -626,17 +626,6 @@ const EnvironmentConfigSchema = z.object({
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
export const MINIMAL_RETRY_CONFIG: PartialEnvironmentConfig = {
validateHooksUsage: false,
validateRefAccessDuringRender: false,
validateNoSetStateInRender: false,
validateNoSetStateInPassiveEffects: false,
validateNoJSXInTryStatements: false,
validateMemoizedEffectDependencies: false,
validateNoCapitalizedCalls: null,
validateBlocklistedImports: null,
enableMinimalTransformsForRetry: true,
};
/**
* For test fixtures and playground only.
*
@@ -851,6 +840,7 @@ export class Environment {
code: string | null;
config: EnvironmentConfig;
fnType: ReactFunctionType;
compilerMode: CompilerMode;
useMemoCacheIdentifier: string;
hasLoweredContextAccess: boolean;
hasFireRewrite: boolean;
@@ -861,6 +851,7 @@ export class Environment {
constructor(
scope: BabelScope,
fnType: ReactFunctionType,
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
logger: Logger | null,
@@ -870,6 +861,7 @@ export class Environment {
) {
this.#scope = scope;
this.fnType = fnType;
this.compilerMode = compilerMode;
this.config = config;
this.filename = filename;
this.code = code;
@@ -924,6 +916,10 @@ export class Environment {
this.#hoistedIdentifiers = new Set();
}
get isInferredMemoEnabled(): boolean {
return this.compilerMode !== 'no_inferred_memo';
}
get nextIdentifierId(): IdentifierId {
return makeIdentifierId(this.#nextIdentifer++);
}
@@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError, ErrorSeverity, ValueKind} from '..';
import {
CompilerError,
CompilerErrorDetailOptions,
ErrorSeverity,
ValueKind,
} from '..';
import {
AbstractValue,
BasicBlock,
@@ -290,21 +295,21 @@ export function inferTerminalFunctionEffects(
return functionEffects;
}
export function raiseFunctionEffectErrors(
export function transformFunctionEffectErrors(
functionEffects: Array<FunctionEffect>,
): void {
functionEffects.forEach(eff => {
): Array<CompilerErrorDetailOptions> {
return functionEffects.map(eff => {
switch (eff.kind) {
case 'ReactMutation':
case 'GlobalMutation': {
CompilerError.throw(eff.error);
return eff.error;
}
case 'ContextMutation': {
CompilerError.throw({
return {
severity: ErrorSeverity.Invariant,
reason: `Unexpected ContextMutation in top-level function effects`,
loc: eff.loc,
});
};
}
default:
assertExhaustive(
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '../CompilerError';
import {CompilerError, CompilerErrorDetailOptions} from '../CompilerError';
import {Environment} from '../HIR';
import {
AbstractValue,
@@ -49,7 +49,7 @@ import {assertExhaustive} from '../Utils/utils';
import {
inferTerminalFunctionEffects,
inferInstructionFunctionEffects,
raiseFunctionEffectErrors,
transformFunctionEffectErrors,
} from './InferFunctionEffects';
const UndefinedValue: InstructionValue = {
@@ -103,7 +103,7 @@ const UndefinedValue: InstructionValue = {
export default function inferReferenceEffects(
fn: HIRFunction,
options: {isFunctionExpression: boolean} = {isFunctionExpression: false},
): void {
): Array<CompilerErrorDetailOptions> {
/*
* Initial state contains function params
* TODO: include module declarations here as well
@@ -241,8 +241,9 @@ export default function inferReferenceEffects(
if (options.isFunctionExpression) {
fn.effects = functionEffects;
} else if (!fn.env.config.enableMinimalTransformsForRetry) {
raiseFunctionEffectErrors(functionEffects);
return [];
} else {
return transformFunctionEffectErrors(functionEffects);
}
}
@@ -2,7 +2,7 @@
## Input
```javascript
// @validateNoCapitalizedCalls @enableFire
// @validateNoCapitalizedCalls @enableFire @panicThreshold(none)
import {fire} from 'react';
const CapitalizedCall = require('shared-runtime').sum;
@@ -24,7 +24,7 @@ function Component({prop1, bar}) {
## Code
```javascript
import { useFire } from "react/compiler-runtime"; // @validateNoCapitalizedCalls @enableFire
import { useFire } from "react/compiler-runtime"; // @validateNoCapitalizedCalls @enableFire @panicThreshold(none)
import { fire } from "react";
const CapitalizedCall = require("shared-runtime").sum;
@@ -1,4 +1,4 @@
// @validateNoCapitalizedCalls @enableFire
// @validateNoCapitalizedCalls @enableFire @panicThreshold(none)
import {fire} from 'react';
const CapitalizedCall = require('shared-runtime').sum;
@@ -2,7 +2,7 @@
## Input
```javascript
// @enableFire
// @enableFire @panicThreshold(none)
import {useRef} from 'react';
function Component({props, bar}) {
@@ -26,7 +26,7 @@ function Component({props, bar}) {
## Code
```javascript
import { useFire } from "react/compiler-runtime"; // @enableFire
import { useFire } from "react/compiler-runtime"; // @enableFire @panicThreshold(none)
import { useRef } from "react";
function Component(t0) {
@@ -1,4 +1,4 @@
// @enableFire
// @enableFire @panicThreshold(none)
import {useRef} from 'react';
function Component({props, bar}) {
@@ -2,7 +2,7 @@
## Input
```javascript
// @validatePreserveExistingMemoizationGuarantees @enableFire
// @validatePreserveExistingMemoizationGuarantees @enableFire @panicThreshold(none)
import {fire} from 'react';
import {sum} from 'shared-runtime';
@@ -24,7 +24,7 @@ function Component({prop1, bar}) {
## Code
```javascript
import { useFire } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableFire
import { useFire } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableFire @panicThreshold(none)
import { fire } from "react";
import { sum } from "shared-runtime";
@@ -1,4 +1,4 @@
// @validatePreserveExistingMemoizationGuarantees @enableFire
// @validatePreserveExistingMemoizationGuarantees @enableFire @panicThreshold(none)
import {fire} from 'react';
import {sum} from 'shared-runtime';
@@ -2,7 +2,7 @@
## Input
```javascript
// @enableFire
// @enableFire @panicThreshold(none)
import {fire} from 'react';
function Component({prop1}) {
@@ -20,7 +20,7 @@ function Component({prop1}) {
## Code
```javascript
import { useFire } from "react/compiler-runtime"; // @enableFire
import { useFire } from "react/compiler-runtime"; // @enableFire @panicThreshold(none)
import { fire } from "react";
function Component(t0) {
@@ -1,4 +1,4 @@
// @enableFire
// @enableFire @panicThreshold(none)
import {fire} from 'react';
function Component({prop1}) {
@@ -2,7 +2,7 @@
## Input
```javascript
// @flow @enableFire
// @flow @enableFire @panicThreshold(none)
import {fire} from 'react';
import {print} from 'shared-runtime';
@@ -1,4 +1,4 @@
// @flow @enableFire
// @flow @enableFire @panicThreshold(none)
import {fire} from 'react';
import {print} from 'shared-runtime';
@@ -2,7 +2,7 @@
## Input
```javascript
// @enableFire
// @enableFire @panicThreshold(none)
import {fire, useEffect} from 'react';
import {Stringify} from 'shared-runtime';
@@ -29,7 +29,7 @@ function Component(props) {
## Code
```javascript
import { useFire } from "react/compiler-runtime"; // @enableFire
import { useFire } from "react/compiler-runtime"; // @enableFire @panicThreshold(none)
import { fire, useEffect } from "react";
import { Stringify } from "shared-runtime";
@@ -1,4 +1,4 @@
// @enableFire
// @enableFire @panicThreshold(none)
import {fire, useEffect} from 'react';
import {Stringify} from 'shared-runtime';