Skip to content

Commit 1a204bc

Browse files
committed
[compiler] Deprecate noEmit, add outputMode
This deprecates the `noEmit: boolean` flag and adds `outputMode: 'client' | 'client-no-memo' | 'ssr' | 'lint'` as the replacement. OutputMode defaults to null and takes precedence if specified, otherwise we use 'client' mode for noEmit=false and 'lint' mode for noEmit=true. Key points: * Retrying failed compilation switches from 'client' mode to 'client-no-memo' * Validations are enabled behind Environment.proto.shouldEnableValidations, enabled for all modes except 'client-no-memo'. Similar for dropping manual memoization. * OptimizeSSR is now gated by the outputMode==='ssr', not a feature flag * Creation of reactive scopes, and related codegen logic, is now gated by outputMode==='client'
1 parent 5fa5200 commit 1a204bc

30 files changed

+497
-217
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,25 @@ export type PluginOptions = Partial<{
102102

103103
panicThreshold: PanicThresholdOptions;
104104

105-
/*
105+
/**
106+
* @deprecated
107+
*
106108
* When enabled, Forget will continue statically analyzing and linting code, but skip over codegen
107109
* passes.
108110
*
111+
* NOTE: ignored if `outputMode` is specified
112+
*
109113
* Defaults to false
110114
*/
111115
noEmit: boolean;
112116

117+
/**
118+
* If specified, overrides `noEmit` and controls the output mode of the compiler.
119+
*
120+
* Defaults to null
121+
*/
122+
outputMode: CompilerOutputMode | null;
123+
113124
/*
114125
* Determines the strategy for determining which functions to compile. Note that regardless of
115126
* which mode is enabled, a component can be opted out by adding the string literal
@@ -212,6 +223,19 @@ const CompilationModeSchema = z.enum([
212223

213224
export type CompilationMode = z.infer<typeof CompilationModeSchema>;
214225

226+
const CompilerOutputModeSchema = z.enum([
227+
// Build optimized for SSR, with client features removed
228+
'ssr',
229+
// Build optimized for the client, with auto memoization
230+
'client',
231+
// Build optimized for the client without auto memo
232+
'client-no-memo',
233+
// Lint mode, the output is unused but validations should run
234+
'lint',
235+
]);
236+
237+
export type CompilerOutputMode = z.infer<typeof CompilerOutputModeSchema>;
238+
215239
/**
216240
* Represents 'events' that may occur during compilation. Events are only
217241
* recorded when a logger is set (through the config).
@@ -293,6 +317,7 @@ export const defaultOptions: ParsedPluginOptions = {
293317
logger: null,
294318
gating: null,
295319
noEmit: false,
320+
outputMode: null,
296321
dynamicGating: null,
297322
eslintSuppressionRules: null,
298323
flowSuppressions: true,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import {NodePath} from '@babel/traverse';
99
import * as t from '@babel/types';
1010
import prettyFormat from 'pretty-format';
11-
import {Logger, ProgramContext} from '.';
11+
import {CompilerOutputMode, Logger, ProgramContext} from '.';
1212
import {
1313
HIRFunction,
1414
ReactiveFunction,
@@ -24,7 +24,6 @@ import {
2424
pruneUnusedLabelsHIR,
2525
} from '../HIR';
2626
import {
27-
CompilerMode,
2827
Environment,
2928
EnvironmentConfig,
3029
ReactFunctionType,
@@ -120,7 +119,7 @@ function run(
120119
>,
121120
config: EnvironmentConfig,
122121
fnType: ReactFunctionType,
123-
mode: CompilerMode,
122+
mode: CompilerOutputMode,
124123
programContext: ProgramContext,
125124
logger: Logger | null,
126125
filename: string | null,
@@ -170,7 +169,7 @@ function runWithEnvironment(
170169
validateUseMemo(hir).unwrap();
171170

172171
if (
173-
env.isInferredMemoEnabled &&
172+
env.shouldDropManualMemoization &&
174173
!env.config.enablePreserveExistingManualUseMemo &&
175174
!env.config.disableMemoizationForDebugging &&
176175
!env.config.enableChangeDetectionForDebugging
@@ -206,7 +205,7 @@ function runWithEnvironment(
206205
inferTypes(hir);
207206
log({kind: 'hir', name: 'InferTypes', value: hir});
208207

209-
if (env.isInferredMemoEnabled) {
208+
if (env.shouldEnableValidations) {
210209
if (env.config.validateHooksUsage) {
211210
validateHooksUsage(hir).unwrap();
212211
}
@@ -232,13 +231,13 @@ function runWithEnvironment(
232231

233232
const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
234233
log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
235-
if (env.isInferredMemoEnabled) {
234+
if (env.shouldEnableValidations) {
236235
if (mutabilityAliasingErrors.isErr()) {
237236
throw mutabilityAliasingErrors.unwrapErr();
238237
}
239238
}
240239

241-
if (env.config.enableOptimizeForSSR) {
240+
if (env.outputMode === 'ssr') {
242241
optimizeForSSR(hir);
243242
log({kind: 'hir', name: 'OptimizeForSSR', value: hir});
244243
}
@@ -259,14 +258,14 @@ function runWithEnvironment(
259258
isFunctionExpression: false,
260259
});
261260
log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
262-
if (env.isInferredMemoEnabled) {
261+
if (env.shouldEnableValidations) {
263262
if (mutabilityAliasingRangeErrors.isErr()) {
264263
throw mutabilityAliasingRangeErrors.unwrapErr();
265264
}
266265
validateLocalsNotReassignedAfterRender(hir);
267266
}
268267

269-
if (env.isInferredMemoEnabled) {
268+
if (env.shouldEnableValidations) {
270269
if (env.config.assertValidMutableRanges) {
271270
assertValidMutableRanges(hir);
272271
}
@@ -310,20 +309,17 @@ function runWithEnvironment(
310309
value: hir,
311310
});
312311

313-
if (env.isInferredMemoEnabled) {
314-
if (env.config.validateStaticComponents) {
315-
env.logErrors(validateStaticComponents(hir));
316-
}
317-
312+
if (env.shouldEnableValidations && env.config.validateStaticComponents) {
313+
env.logErrors(validateStaticComponents(hir));
314+
}
315+
if (env.outputMode === 'client') {
318316
/**
319317
* Only create reactive scopes (which directly map to generated memo blocks)
320318
* if inferred memoization is enabled. This makes all later passes which
321319
* transform reactive-scope labeled instructions no-ops.
322320
*/
323-
if (!env.config.enableOptimizeForSSR) {
324-
inferReactiveScopeVariables(hir);
325-
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
326-
}
321+
inferReactiveScopeVariables(hir);
322+
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
327323
}
328324

329325
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
@@ -588,7 +584,7 @@ export function compileFn(
588584
>,
589585
config: EnvironmentConfig,
590586
fnType: ReactFunctionType,
591-
mode: CompilerMode,
587+
mode: CompilerOutputMode,
592588
programContext: ProgramContext,
593589
logger: Logger | null,
594590
filename: string | null,

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
validateRestrictedImports,
2525
} from './Imports';
2626
import {
27+
CompilerOutputMode,
2728
CompilerReactTarget,
2829
ParsedPluginOptions,
2930
PluginOptions,
@@ -421,9 +422,17 @@ export function compileProgram(
421422
);
422423
const compiledFns: Array<CompileResult> = [];
423424

425+
// outputMode takes precedence if specified
426+
const outputMode: CompilerOutputMode =
427+
pass.opts.outputMode ?? (pass.opts.noEmit ? 'lint' : 'client');
424428
while (queue.length !== 0) {
425429
const current = queue.shift()!;
426-
const compiled = processFn(current.fn, current.fnType, programContext);
430+
const compiled = processFn(
431+
current.fn,
432+
current.fnType,
433+
programContext,
434+
outputMode,
435+
);
427436

428437
if (compiled != null) {
429438
for (const outlined of compiled.outlined) {
@@ -581,6 +590,7 @@ function processFn(
581590
fn: BabelFn,
582591
fnType: ReactFunctionType,
583592
programContext: ProgramContext,
593+
outputMode: CompilerOutputMode,
584594
): null | CodegenFunction {
585595
let directives: {
586596
optIn: t.Directive | null;
@@ -616,18 +626,27 @@ function processFn(
616626
}
617627

618628
let compiledFn: CodegenFunction;
619-
const compileResult = tryCompileFunction(fn, fnType, programContext);
629+
const compileResult = tryCompileFunction(
630+
fn,
631+
fnType,
632+
programContext,
633+
outputMode,
634+
);
620635
if (compileResult.kind === 'error') {
621636
if (directives.optOut != null) {
622637
logError(compileResult.error, programContext, fn.node.loc ?? null);
623638
} else {
624639
handleError(compileResult.error, programContext, fn.node.loc ?? null);
625640
}
626-
const retryResult = retryCompileFunction(fn, fnType, programContext);
627-
if (retryResult == null) {
641+
if (outputMode === 'client') {
642+
const retryResult = retryCompileFunction(fn, fnType, programContext);
643+
if (retryResult == null) {
644+
return null;
645+
}
646+
compiledFn = retryResult;
647+
} else {
628648
return null;
629649
}
630-
compiledFn = retryResult;
631650
} else {
632651
compiledFn = compileResult.compiledFn;
633652
}
@@ -663,7 +682,7 @@ function processFn(
663682

664683
if (programContext.hasModuleScopeOptOut) {
665684
return null;
666-
} else if (programContext.opts.noEmit) {
685+
} else if (programContext.opts.outputMode === 'lint') {
667686
/**
668687
* inferEffectDependencies + noEmit is currently only used for linting. In
669688
* this mode, add source locations for where the compiler *can* infer effect
@@ -693,6 +712,7 @@ function tryCompileFunction(
693712
fn: BabelFn,
694713
fnType: ReactFunctionType,
695714
programContext: ProgramContext,
715+
outputMode: CompilerOutputMode,
696716
):
697717
| {kind: 'compile'; compiledFn: CodegenFunction}
698718
| {kind: 'error'; error: unknown} {
@@ -719,7 +739,7 @@ function tryCompileFunction(
719739
fn,
720740
programContext.opts.environment,
721741
fnType,
722-
'all_features',
742+
outputMode,
723743
programContext,
724744
programContext.opts.logger,
725745
programContext.filename,
@@ -757,7 +777,7 @@ function retryCompileFunction(
757777
fn,
758778
environment,
759779
fnType,
760-
'no_inferred_memo',
780+
'client-no-memo',
761781
programContext,
762782
programContext.opts.logger,
763783
programContext.filename,

compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as t from '@babel/types';
99
import {ZodError, z} from 'zod/v4';
1010
import {fromZodError} from 'zod-validation-error/v4';
1111
import {CompilerError} from '../CompilerError';
12-
import {Logger, ProgramContext} from '../Entrypoint';
12+
import {CompilerOutputMode, Logger, ProgramContext} from '../Entrypoint';
1313
import {Err, Ok, Result} from '../Utils/Result';
1414
import {
1515
DEFAULT_GLOBALS,
@@ -51,6 +51,7 @@ import {Scope as BabelScope, NodePath} from '@babel/traverse';
5151
import {TypeSchema} from './TypeSchema';
5252
import {FlowTypeEnv} from '../Flood/Types';
5353
import {defaultModuleTypeProvider} from './DefaultModuleTypeProvider';
54+
import {assertExhaustive} from '../Utils/utils';
5455

5556
export const ReactElementSymbolSchema = z.object({
5657
elementSymbol: z.union([
@@ -677,8 +678,6 @@ export const EnvironmentConfigSchema = z.object({
677678
* from refs need to be stored in state during mount.
678679
*/
679680
enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
680-
681-
enableOptimizeForSSR: z.boolean().default(false),
682681
});
683682

684683
export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
@@ -718,7 +717,7 @@ export class Environment {
718717
code: string | null;
719718
config: EnvironmentConfig;
720719
fnType: ReactFunctionType;
721-
compilerMode: CompilerMode;
720+
outputMode: CompilerOutputMode;
722721
programContext: ProgramContext;
723722
hasFireRewrite: boolean;
724723
hasInferredEffect: boolean;
@@ -733,7 +732,7 @@ export class Environment {
733732
constructor(
734733
scope: BabelScope,
735734
fnType: ReactFunctionType,
736-
compilerMode: CompilerMode,
735+
outputMode: CompilerOutputMode,
737736
config: EnvironmentConfig,
738737
contextIdentifiers: Set<t.Identifier>,
739738
parentFunction: NodePath<t.Function>, // the outermost function being compiled
@@ -744,7 +743,7 @@ export class Environment {
744743
) {
745744
this.#scope = scope;
746745
this.fnType = fnType;
747-
this.compilerMode = compilerMode;
746+
this.outputMode = outputMode;
748747
this.config = config;
749748
this.filename = filename;
750749
this.code = code;
@@ -840,8 +839,45 @@ export class Environment {
840839
return this.#flowTypeEnvironment;
841840
}
842841

843-
get isInferredMemoEnabled(): boolean {
844-
return this.compilerMode !== 'no_inferred_memo';
842+
get shouldDropManualMemoization(): boolean {
843+
switch (this.outputMode) {
844+
case 'lint': {
845+
// linting drops to be more compatible with compiler analysis
846+
return true;
847+
}
848+
case 'client':
849+
case 'ssr': {
850+
return true;
851+
}
852+
case 'client-no-memo': {
853+
return false;
854+
}
855+
default: {
856+
assertExhaustive(
857+
this.outputMode,
858+
`Unexpected output mode '${this.outputMode}'`,
859+
);
860+
}
861+
}
862+
}
863+
864+
get shouldEnableValidations(): boolean {
865+
switch (this.outputMode) {
866+
case 'client':
867+
case 'lint':
868+
case 'ssr': {
869+
return true;
870+
}
871+
case 'client-no-memo': {
872+
return false;
873+
}
874+
default: {
875+
assertExhaustive(
876+
this.outputMode,
877+
`Unexpected output mode '${this.outputMode}'`,
878+
);
879+
}
880+
}
845881
}
846882

847883
get nextIdentifierId(): IdentifierId {

compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2451,7 +2451,10 @@ function computeEffectsForLegacySignature(
24512451
}),
24522452
});
24532453
}
2454-
if (signature.knownIncompatible != null && state.env.isInferredMemoEnabled) {
2454+
if (
2455+
signature.knownIncompatible != null &&
2456+
state.env.shouldEnableValidations
2457+
) {
24552458
const errors = new CompilerError();
24562459
errors.pushDiagnostic(
24572460
CompilerDiagnostic.create({

compiler/packages/babel-plugin-react-compiler/src/Optimization/DeadCodeElimination.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
319319
}
320320
case 'CallExpression':
321321
case 'MethodCall': {
322-
if (state.env.config.enableOptimizeForSSR) {
322+
if (state.env.outputMode === 'ssr') {
323323
const calleee =
324324
value.kind === 'CallExpression' ? value.callee : value.property;
325325
const hookKind = getHookKind(state.env, calleee.identifier);

0 commit comments

Comments
 (0)