Skip to content

Commit 4b58c67

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 ab27231 commit 4b58c67

File tree

6 files changed

+212
-10
lines changed

6 files changed

+212
-10
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: 94 additions & 10 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
@@ -47,6 +49,9 @@ const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
4749
export function transformFire(fn: HIRFunction): void {
4850
const context = new Context(fn.env);
4951
replaceFireFunctions(fn, context);
52+
if (!context.hasErrors()) {
53+
ensureNoMoreFireUses(fn, context);
54+
}
5055
context.throwIfErrorsFound();
5156
}
5257

@@ -120,6 +125,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
120125
}
121126
rewriteInstrs.set(loadUseEffectInstrId, newInstrs);
122127
}
128+
ensureNoRemainingCalleeCaptures(
129+
lambda.loweredFunc.func,
130+
context,
131+
capturedCallees,
132+
);
123133
}
124134
}
125135
} else if (
@@ -159,7 +169,10 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
159169
}
160170

161171
const fireFunctionBinding =
162-
context.getOrGenerateFireFunctionBinding(loadLocal.place);
172+
context.getOrGenerateFireFunctionBinding(
173+
loadLocal.place,
174+
value.loc,
175+
);
163176

164177
loadLocal.place = {...fireFunctionBinding};
165178

@@ -320,6 +333,69 @@ function visitFunctionExpressionAndPropagateFireDependencies(
320333
return calleesCapturedByFnExpression;
321334
}
322335

336+
/*
337+
* eachInstructionOperand is not sufficient for our cases because:
338+
* 1. fire is a global, which will not appear
339+
* 2. The HIR may be malformed, so can't rely on function deps and must
340+
* traverse the whole function.
341+
*/
342+
function* eachReachablePlace(fn: HIRFunction): Iterable<Place> {
343+
for (const [, block] of fn.body.blocks) {
344+
for (const instr of block.instructions) {
345+
if (
346+
instr.value.kind === 'FunctionExpression' ||
347+
instr.value.kind === 'ObjectMethod'
348+
) {
349+
yield* eachReachablePlace(instr.value.loweredFunc.func);
350+
} else {
351+
yield* eachInstructionOperand(instr);
352+
}
353+
}
354+
}
355+
}
356+
357+
function ensureNoRemainingCalleeCaptures(
358+
fn: HIRFunction,
359+
context: Context,
360+
capturedCallees: FireCalleesToFireFunctionBinding,
361+
): void {
362+
for (const place of eachReachablePlace(fn)) {
363+
const calleeInfo = capturedCallees.get(place.identifier.id);
364+
if (calleeInfo != null) {
365+
const calleeName =
366+
calleeInfo.capturedCalleeIdentifier.name?.kind === 'named'
367+
? calleeInfo.capturedCalleeIdentifier.name.value
368+
: '<unknown>';
369+
context.pushError({
370+
loc: place.loc,
371+
description: `All uses of ${calleeName} must be either used with a fire() call in \
372+
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
373+
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
374+
severity: ErrorSeverity.InvalidReact,
375+
reason: CANNOT_COMPILE_FIRE,
376+
suggestions: null,
377+
});
378+
}
379+
}
380+
}
381+
382+
function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
383+
for (const place of eachReachablePlace(fn)) {
384+
if (
385+
place.identifier.type.kind === 'Function' &&
386+
place.identifier.type.shapeId === BuiltInFireId
387+
) {
388+
context.pushError({
389+
loc: place.identifier.loc,
390+
description: 'Cannot use `fire` outside of a useEffect function',
391+
severity: ErrorSeverity.Invariant,
392+
reason: CANNOT_COMPILE_FIRE,
393+
suggestions: null,
394+
});
395+
}
396+
}
397+
}
398+
323399
function makeLoadUseFireInstruction(env: Environment): Instruction {
324400
const useFirePlace = createTemporaryPlace(env, GeneratedSource);
325401
useFirePlace.effect = Effect.Read;
@@ -422,6 +498,7 @@ type FireCalleesToFireFunctionBinding = Map<
422498
{
423499
fireFunctionBinding: Place;
424500
capturedCalleeIdentifier: Identifier;
501+
fireLoc: SourceLocation;
425502
}
426503
>;
427504

@@ -523,8 +600,10 @@ class Context {
523600
getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined {
524601
return this.#loadLocals.get(id);
525602
}
526-
527-
getOrGenerateFireFunctionBinding(callee: Place): Place {
603+
getOrGenerateFireFunctionBinding(
604+
callee: Place,
605+
fireLoc: SourceLocation,
606+
): Place {
528607
const fireFunctionBinding = getOrInsertWith(
529608
this.#fireCalleesToFireFunctions,
530609
callee.identifier.id,
@@ -534,6 +613,7 @@ class Context {
534613
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
535614
fireFunctionBinding,
536615
capturedCalleeIdentifier: callee.identifier,
616+
fireLoc,
537617
});
538618

539619
return fireFunctionBinding;
@@ -575,8 +655,12 @@ class Context {
575655
return this.#loadGlobalInstructionIds.get(id);
576656
}
577657

658+
hasErrors(): boolean {
659+
return this.#errors.hasErrors();
660+
}
661+
578662
throwIfErrorsFound(): void {
579-
if (this.#errors.hasErrors()) throw this.#errors;
663+
if (this.hasErrors()) throw this.#errors;
580664
}
581665
}
582666

Lines changed: 39 additions & 0 deletions
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+
Lines changed: 18 additions & 0 deletions
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+
}
Lines changed: 38 additions & 0 deletions
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+
Lines changed: 15 additions & 0 deletions
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)