Skip to content

Commit 09019ba

Browse files
committed
[compiler][rfc] Simple retry for fire
Start of a simple retry pipeline. Would love feedback on how we implement this to be extensible to other compiler non-memoization features (e.g. inlineJSX)
1 parent e5a2062 commit 09019ba

25 files changed

+764
-90
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ function runWithEnvironment(
162162
if (
163163
!env.config.enablePreserveExistingManualUseMemo &&
164164
!env.config.disableMemoizationForDebugging &&
165-
!env.config.enableChangeDetectionForDebugging
165+
!env.config.enableChangeDetectionForDebugging &&
166+
!env.config.enableMinimalTransformsForRetry
166167
) {
167168
dropManualMemoization(hir);
168169
log({kind: 'hir', name: 'DropManualMemoization', value: hir});
@@ -279,8 +280,10 @@ function runWithEnvironment(
279280
value: hir,
280281
});
281282

282-
inferReactiveScopeVariables(hir);
283-
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
283+
if (!env.config.enableMinimalTransformsForRetry) {
284+
inferReactiveScopeVariables(hir);
285+
log({kind: 'hir', name: 'InferReactiveScopeVariables', value: hir});
286+
}
284287

285288
const fbtOperands = memoizeFbtAndMacroOperandsInSameScope(hir);
286289
log({

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

Lines changed: 92 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,35 @@ export function compileProgram(
333333
queue.push({kind: 'original', fn, fnType});
334334
};
335335

336+
const runMinimalCompilePipeline = (
337+
fn: NodePath<
338+
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
339+
>,
340+
fnType: ReactFunctionType,
341+
) => {
342+
const retryEnvironment = {
343+
...environment,
344+
validateHooksUsage: false,
345+
validateRefAccessDuringRender: false,
346+
validateNoSetStateInRender: false,
347+
validateNoSetStateInPassiveEffects: false,
348+
validateNoJSXInTryStatements: false,
349+
validateMemoizedEffectDependencies: false,
350+
validateNoCapitalizedCalls: null,
351+
validateBlocklistedImports: null,
352+
enableMinimalTransformsForRetry: true,
353+
};
354+
return compileFn(
355+
fn,
356+
retryEnvironment,
357+
fnType,
358+
useMemoCacheIdentifier.name,
359+
pass.opts.logger,
360+
pass.filename,
361+
pass.code,
362+
);
363+
};
364+
336365
// Main traversal to compile with Forget
337366
program.traverse(
338367
{
@@ -382,66 +411,81 @@ export function compileProgram(
382411
);
383412
}
384413

385-
let compiledFn: CodegenFunction;
386-
try {
387-
/**
388-
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
389-
* Program node itself. We need to figure out whether an eslint suppression range
390-
* applies to this function first.
391-
*/
392-
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
393-
suppressions,
394-
fn,
395-
);
396-
if (suppressionsInFunction.length > 0) {
397-
const lintError = suppressionsToCompilerError(suppressionsInFunction);
398-
if (optOutDirectives.length > 0) {
399-
logError(lintError, pass, fn.node.loc ?? null);
400-
} else {
401-
handleError(lintError, pass, fn.node.loc ?? null);
402-
}
403-
return null;
414+
/**
415+
* Note that Babel does not attach comment nodes to nodes; they are dangling off of the
416+
* Program node itself. We need to figure out whether an eslint suppression range
417+
* applies to this function first.
418+
*/
419+
const suppressionsInFunction = filterSuppressionsThatAffectFunction(
420+
suppressions,
421+
fn,
422+
);
423+
let compileResult:
424+
| {kind: 'compile'; compiledFn: CodegenFunction}
425+
| {kind: 'error'; error: unknown};
426+
if (suppressionsInFunction.length > 0) {
427+
compileResult = {
428+
kind: 'error',
429+
error: suppressionsToCompilerError(suppressionsInFunction),
430+
};
431+
} else {
432+
try {
433+
compileResult = {
434+
kind: 'compile',
435+
compiledFn: compileFn(
436+
fn,
437+
environment,
438+
fnType,
439+
useMemoCacheIdentifier.name,
440+
pass.opts.logger,
441+
pass.filename,
442+
pass.code,
443+
),
444+
};
445+
} catch (err) {
446+
compileResult = {kind: 'error', error: err};
404447
}
405-
406-
compiledFn = compileFn(
407-
fn,
408-
environment,
409-
fnType,
410-
useMemoCacheIdentifier.name,
411-
pass.opts.logger,
412-
pass.filename,
413-
pass.code,
414-
);
415-
pass.opts.logger?.logEvent(pass.filename, {
416-
kind: 'CompileSuccess',
417-
fnLoc: fn.node.loc ?? null,
418-
fnName: compiledFn.id?.name ?? null,
419-
memoSlots: compiledFn.memoSlotsUsed,
420-
memoBlocks: compiledFn.memoBlocks,
421-
memoValues: compiledFn.memoValues,
422-
prunedMemoBlocks: compiledFn.prunedMemoBlocks,
423-
prunedMemoValues: compiledFn.prunedMemoValues,
424-
});
425-
} catch (err) {
448+
}
449+
// If non-memoization features are enabled, retry regardless of error kind
450+
if (compileResult.kind === 'error' && environment.enableFire) {
451+
try {
452+
compileResult = {
453+
kind: 'compile',
454+
compiledFn: runMinimalCompilePipeline(fn, fnType),
455+
};
456+
} catch (err) {
457+
compileResult = {kind: 'error', error: err};
458+
}
459+
}
460+
if (compileResult.kind === 'error') {
426461
/**
427462
* If an opt out directive is present, log only instead of throwing and don't mark as
428463
* containing a critical error.
429464
*/
430-
if (fn.node.body.type === 'BlockStatement') {
431-
if (optOutDirectives.length > 0) {
432-
logError(err, pass, fn.node.loc ?? null);
433-
return null;
434-
}
465+
if (optOutDirectives.length > 0) {
466+
logError(compileResult.error, pass, fn.node.loc ?? null);
467+
} else {
468+
handleError(compileResult.error, pass, fn.node.loc ?? null);
435469
}
436-
handleError(err, pass, fn.node.loc ?? null);
437470
return null;
438471
}
439472

473+
pass.opts.logger?.logEvent(pass.filename, {
474+
kind: 'CompileSuccess',
475+
fnLoc: fn.node.loc ?? null,
476+
fnName: compileResult.compiledFn.id?.name ?? null,
477+
memoSlots: compileResult.compiledFn.memoSlotsUsed,
478+
memoBlocks: compileResult.compiledFn.memoBlocks,
479+
memoValues: compileResult.compiledFn.memoValues,
480+
prunedMemoBlocks: compileResult.compiledFn.prunedMemoBlocks,
481+
prunedMemoValues: compileResult.compiledFn.prunedMemoValues,
482+
});
483+
440484
/**
441485
* Always compile functions with opt in directives.
442486
*/
443487
if (optInDirectives.length > 0) {
444-
return compiledFn;
488+
return compileResult.compiledFn;
445489
} else if (pass.opts.compilationMode === 'annotation') {
446490
/**
447491
* No opt-in directive in annotation mode, so don't insert the compiled function.
@@ -467,7 +511,7 @@ export function compileProgram(
467511
}
468512

469513
if (!pass.opts.noEmit) {
470-
return compiledFn;
514+
return compileResult.compiledFn;
471515
}
472516
return null;
473517
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,8 @@ const EnvironmentConfigSchema = z.object({
552552
*/
553553
disableMemoizationForDebugging: z.boolean().default(false),
554554

555+
enableMinimalTransformsForRetry: z.boolean().default(false),
556+
555557
/**
556558
* When true, rather using memoized values, the compiler will always re-compute
557559
* values, and then use a heuristic to compare the memoized value to the newly

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export default function inferReferenceEffects(
241241

242242
if (options.isFunctionExpression) {
243243
fn.effects = functionEffects;
244-
} else {
244+
} else if (!fn.env.config.enableMinimalTransformsForRetry) {
245245
raiseFunctionEffectErrors(functionEffects);
246246
}
247247
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableFire
6+
import {fire} from 'react';
7+
8+
function Component({props, bar}) {
9+
const foo = () => {
10+
console.log(props);
11+
};
12+
useEffect(() => {
13+
fire(foo(props), bar);
14+
fire(...foo);
15+
fire(bar);
16+
fire(props.foo());
17+
});
18+
19+
return null;
20+
}
21+
22+
```
23+
24+
25+
## Error
26+
27+
```
28+
7 | };
29+
8 | useEffect(() => {
30+
> 9 | fire(foo(props), bar);
31+
| ^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received multiple arguments (9:9)
32+
33+
InvalidReact: Cannot compile `fire`. fire() can only take in a single call expression as an argument but received a spread argument (10:10)
34+
35+
InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (11:11)
36+
37+
InvalidReact: Cannot compile `fire`. `fire()` can only receive a function call such as `fire(fn(a,b)). Method calls and other expressions are not allowed (12:12)
38+
10 | fire(...foo);
39+
11 | fire(bar);
40+
12 | fire(props.foo());
41+
```
42+
43+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @enableFire
2+
import {fire} from 'react';
3+
4+
function Component({props, bar}) {
5+
const foo = () => {
6+
console.log(props);
7+
};
8+
useEffect(() => {
9+
fire(foo(props), bar);
10+
fire(...foo);
11+
fire(bar);
12+
fire(props.foo());
13+
});
14+
15+
return null;
16+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @validateNoCapitalizedCalls @enableFire
6+
import {fire} from 'react';
7+
const CapitalizedCall = require('shared-runtime').sum;
8+
9+
function Component({prop1, bar}) {
10+
const foo = () => {
11+
console.log(prop1);
12+
};
13+
useEffect(() => {
14+
fire(foo(prop1));
15+
fire(foo());
16+
fire(bar());
17+
});
18+
19+
return CapitalizedCall();
20+
}
21+
22+
```
23+
24+
## Code
25+
26+
```javascript
27+
import { useFire } from "react/compiler-runtime"; // @validateNoCapitalizedCalls @enableFire
28+
import { fire } from "react";
29+
const CapitalizedCall = require("shared-runtime").sum;
30+
31+
function Component(t0) {
32+
const { prop1, bar } = t0;
33+
const foo = () => {
34+
console.log(prop1);
35+
};
36+
const t1 = useFire(foo);
37+
const t2 = useFire(bar);
38+
39+
useEffect(() => {
40+
t1(prop1);
41+
t1();
42+
t2();
43+
});
44+
return CapitalizedCall();
45+
}
46+
47+
```
48+
49+
### Eval output
50+
(kind: exception) Fixture not implemented
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// @validateNoCapitalizedCalls @enableFire
2+
import {fire} from 'react';
3+
const CapitalizedCall = require('shared-runtime').sum;
4+
5+
function Component({prop1, bar}) {
6+
const foo = () => {
7+
console.log(prop1);
8+
};
9+
useEffect(() => {
10+
fire(foo(prop1));
11+
fire(foo());
12+
fire(bar());
13+
});
14+
15+
return CapitalizedCall();
16+
}

0 commit comments

Comments
 (0)