Skip to content

Commit c8d53fc

Browse files
committed
[compile] Error on fire outside of effects and ensure correct compilation
Traverse the compiled functions to ensure there are no lingering fires and that all fire calls are inside an effect lambda. --
1 parent 5795b48 commit c8d53fc

File tree

6 files changed

+210
-8
lines changed

6 files changed

+210
-8
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,14 @@ export function printSourceLocation(loc: SourceLocation): string {
897897
}
898898
}
899899

900+
export function printSourceLocationLine(loc: SourceLocation): string {
901+
if (typeof loc === 'symbol') {
902+
return 'generated';
903+
} else {
904+
return `${loc.start.line}:${loc.end.line}`;
905+
}
906+
}
907+
900908
export function printAliases(aliases: DisjointSet<Identifier>): string {
901909
const aliasSets = aliases.buildSets();
902910

compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {CompilerError, CompilerErrorDetailOptions, ErrorSeverity} from '..';
8+
import {
9+
CompilerError,
10+
CompilerErrorDetailOptions,
11+
ErrorSeverity,
12+
SourceLocation,
13+
} from '..';
914
import {
1015
CallExpression,
1116
Effect,
@@ -28,14 +33,11 @@ import {
2833
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
2934
import {getOrInsertWith} from '../Utils/utils';
3035
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
36+
import {eachInstructionOperand} from '../HIR/visitors';
37+
import {printSourceLocationLine} from '../HIR/PrintHIR';
3138

3239
/*
3340
* TODO(jmbrown):
34-
* In this stack:
35-
* - Assert no lingering fire calls
36-
* - Ensure a fired function is not called regularly elsewhere in the same effect
37-
*
38-
* Future:
3941
* - rewrite dep arrays
4042
* - traverse object methods
4143
* - method calls
@@ -187,6 +189,7 @@ type FireCalleesToFireFunctionBinding = Map<
187189
{
188190
fireFunctionBinding: Place;
189191
capturedCalleeIdentifier: Identifier;
192+
fireLoc: SourceLocation;
190193
}
191194
>;
192195

@@ -295,7 +298,11 @@ class Context {
295298
return this.#loadLocals.get(id);
296299
}
297300

298-
getOrGenerateFireFunctionBinding(callee: Place, env: Environment): Place {
301+
getOrGenerateFireFunctionBinding(
302+
callee: Place,
303+
env: Environment,
304+
fireLoc: SourceLocation,
305+
): Place {
299306
const fireFunctionBinding = getOrInsertWith(
300307
this.#fireCalleesToFireFunctions,
301308
callee.identifier.id,
@@ -305,6 +312,7 @@ class Context {
305312
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
306313
fireFunctionBinding,
307314
capturedCalleeIdentifier: callee.identifier,
315+
fireLoc,
308316
});
309317

310318
return fireFunctionBinding;
@@ -346,8 +354,12 @@ class Context {
346354
return this.#loadGlobalInstructionIds.get(id);
347355
}
348356

357+
hasErrors(): boolean {
358+
return this.#errors.hasErrors();
359+
}
360+
349361
throwIfErrorsFound(): void {
350-
if (this.#errors.hasErrors()) throw this.#errors;
362+
if (this.hasErrors()) throw this.#errors;
351363
}
352364
}
353365

@@ -510,6 +522,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
510522
rewriteInstrs.set(loadUseEffectInstrId, newInstrs);
511523
}
512524
}
525+
ensureNoRemainingCalleeCaptures(
526+
lambda.loweredFunc.func,
527+
context,
528+
capturedCallees,
529+
);
513530
}
514531
} else if (
515532
value.kind === 'CallExpression' &&
@@ -551,6 +568,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
551568
context.getOrGenerateFireFunctionBinding(
552569
{...loadLocal.place},
553570
fn.env,
571+
value.loc,
554572
);
555573

556574
loadLocal.place = {...fireFunctionBinding};
@@ -626,8 +644,74 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
626644
}
627645
}
628646

647+
/*
648+
* eachInstructionOperand is not sufficient for our cases because:
649+
* 1. fire is a global, which will not appear
650+
* 2. The HIR may be malformed, so can't rely on function deps and must
651+
* traverse the whole function.
652+
*/
653+
function* eachReachablePlace(fn: HIRFunction): Iterable<Place> {
654+
for (const [, block] of fn.body.blocks) {
655+
for (const instr of block.instructions) {
656+
if (
657+
instr.value.kind === 'FunctionExpression' ||
658+
instr.value.kind === 'ObjectMethod'
659+
) {
660+
yield* eachReachablePlace(instr.value.loweredFunc.func);
661+
} else {
662+
yield* eachInstructionOperand(instr);
663+
}
664+
}
665+
}
666+
}
667+
668+
function ensureNoRemainingCalleeCaptures(
669+
fn: HIRFunction,
670+
context: Context,
671+
capturedCallees: FireCalleesToFireFunctionBinding,
672+
): void {
673+
for (const place of eachReachablePlace(fn)) {
674+
const calleeInfo = capturedCallees.get(place.identifier.id);
675+
if (calleeInfo != null) {
676+
const calleeName =
677+
calleeInfo.capturedCalleeIdentifier.name?.kind === 'named'
678+
? calleeInfo.capturedCalleeIdentifier.name.value
679+
: '<unknown>';
680+
context.pushError({
681+
loc: place.loc,
682+
description: `All uses of ${calleeName} must be either used with a fire() call in \
683+
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
684+
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
685+
severity: ErrorSeverity.InvalidReact,
686+
reason: CANNOT_COMPILE_FIRE,
687+
suggestions: null,
688+
});
689+
}
690+
}
691+
}
692+
693+
function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
694+
for (const place of eachReachablePlace(fn)) {
695+
if (
696+
place.identifier.type.kind === 'Function' &&
697+
place.identifier.type.shapeId === BuiltInFireId
698+
) {
699+
context.pushError({
700+
loc: place.identifier.loc,
701+
description: 'Cannot use `fire` outside of a useEffect function',
702+
severity: ErrorSeverity.Invariant,
703+
reason: CANNOT_COMPILE_FIRE,
704+
suggestions: null,
705+
});
706+
}
707+
}
708+
}
709+
629710
export function transformFire(fn: HIRFunction): void {
630711
const context = new Context();
631712
replaceFireFunctions(fn, context);
713+
if (!context.hasErrors()) {
714+
ensureNoMoreFireUses(fn, context);
715+
}
632716
context.throwIfErrorsFound();
633717
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableFire
6+
import {fire} from 'react';
7+
8+
function Component(props) {
9+
const foo = props => {
10+
console.log(props);
11+
};
12+
useEffect(() => {
13+
function nested() {
14+
fire(foo(props));
15+
foo(props);
16+
}
17+
18+
nested();
19+
});
20+
21+
return null;
22+
}
23+
24+
```
25+
26+
27+
## Error
28+
29+
```
30+
9 | function nested() {
31+
10 | fire(foo(props));
32+
> 11 | foo(props);
33+
| ^^^ InvalidReact: Cannot compile `fire`. All uses of foo must be either used with a fire() call in this effect or not used with a fire() call at all. foo was used with fire() on line 10:10 in this effect (11:11)
34+
12 | }
35+
13 |
36+
14 | nested();
37+
```
38+
39+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @enableFire
2+
import {fire} from 'react';
3+
4+
function Component(props) {
5+
const foo = props => {
6+
console.log(props);
7+
};
8+
useEffect(() => {
9+
function nested() {
10+
fire(foo(props));
11+
foo(props);
12+
}
13+
14+
nested();
15+
});
16+
17+
return null;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableFire
6+
import {fire, useCallback} from 'react';
7+
8+
function Component({props, bar}) {
9+
const foo = () => {
10+
console.log(props);
11+
};
12+
fire(foo(props));
13+
14+
useCallback(() => {
15+
fire(foo(props));
16+
}, [foo, props]);
17+
18+
return null;
19+
}
20+
21+
```
22+
23+
24+
## Error
25+
26+
```
27+
6 | console.log(props);
28+
7 | };
29+
> 8 | fire(foo(props));
30+
| ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8)
31+
32+
Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11)
33+
9 |
34+
10 | useCallback(() => {
35+
11 | fire(foo(props));
36+
```
37+
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// @enableFire
2+
import {fire, useCallback} from 'react';
3+
4+
function Component({props, bar}) {
5+
const foo = () => {
6+
console.log(props);
7+
};
8+
fire(foo(props));
9+
10+
useCallback(() => {
11+
fire(foo(props));
12+
}, [foo, props]);
13+
14+
return null;
15+
}

0 commit comments

Comments
 (0)