@samitouri / QOS-React-2 / commits / 45a720f7c7

[compile] Error on fire outside of effects and ensure correct compilation, correct import (#31798)

Traverse the compiled functions to ensure there are no lingering fires and that all fire calls are inside an effect lambda. Also corrects the import to import from the compiler runtime instead --

Jordan Brown committed Dec 20, 2024 at 16:55 UTC 45a720f7c7ff98e22fb299b50fef90fe319081a7
12 files changed +221 -16
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+4 -1
@@ -567,7 +567,10 @@ export function compileProgram(
567
568 const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite);
569 if (environment.enableFire && hasFireRewrite) {
570 - externalFunctions.push({source: 'react', importSpecifierName: 'useFire'});
570 + externalFunctions.push({
571 + source: getReactCompilerRuntimeModule(pass.opts),
572 + importSpecifierName: 'useFire',
573 + });
574 }
575 } catch (err) {
576 handleError(err, pass, null);
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+8
@@ -897,6 +897,14 @@ export function printSourceLocation(loc: SourceLocation): string {
897 }
898 }
899
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 +
908 export function printAliases(aliases: DisjointSet<Identifier>): string {
909 const aliasSets = aliases.buildSets();
910
compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts
+94 -10
@@ -5,7 +5,12 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {CompilerError, CompilerErrorDetailOptions, ErrorSeverity} from '..';
8 +import {
9 + CompilerError,
10 + CompilerErrorDetailOptions,
11 + ErrorSeverity,
12 + SourceLocation,
13 +} from '..';
14 import {
15 CallExpression,
16 Effect,
@@ -28,14 +33,11 @@ import {
33 import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
34 import {getOrInsertWith} from '../Utils/utils';
35 import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
36 +import {eachInstructionOperand} from '../HIR/visitors';
37 +import {printSourceLocationLine} from '../HIR/PrintHIR';
38
39 /*
40 * 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:
41 * - rewrite dep arrays
42 * - traverse object methods
43 * - method calls
@@ -47,6 +49,9 @@ const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
49 export function transformFire(fn: HIRFunction): void {
50 const context = new Context(fn.env);
51 replaceFireFunctions(fn, context);
52 + if (!context.hasErrors()) {
53 + ensureNoMoreFireUses(fn, context);
54 + }
55 context.throwIfErrorsFound();
56 }
57
@@ -120,6 +125,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
125 }
126 rewriteInstrs.set(loadUseEffectInstrId, newInstrs);
127 }
128 + ensureNoRemainingCalleeCaptures(
129 + lambda.loweredFunc.func,
130 + context,
131 + capturedCallees,
132 + );
133 }
134 }
135 } else if (
@@ -159,7 +169,10 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
169 }
170
171 const fireFunctionBinding =
162 - context.getOrGenerateFireFunctionBinding(loadLocal.place);
172 + context.getOrGenerateFireFunctionBinding(
173 + loadLocal.place,
174 + value.loc,
175 + );
176
177 loadLocal.place = {...fireFunctionBinding};
178
@@ -320,6 +333,69 @@ function visitFunctionExpressionAndPropagateFireDependencies(
333 return calleesCapturedByFnExpression;
334 }
335
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 +
399 function makeLoadUseFireInstruction(env: Environment): Instruction {
400 const useFirePlace = createTemporaryPlace(env, GeneratedSource);
401 useFirePlace.effect = Effect.Read;
@@ -422,6 +498,7 @@ type FireCalleesToFireFunctionBinding = Map<
498 {
499 fireFunctionBinding: Place;
500 capturedCalleeIdentifier: Identifier;
501 + fireLoc: SourceLocation;
502 }
503 >;
504
@@ -523,8 +600,10 @@ class Context {
600 getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined {
601 return this.#loadLocals.get(id);
602 }
526 -
527 - getOrGenerateFireFunctionBinding(callee: Place): Place {
603 + getOrGenerateFireFunctionBinding(
604 + callee: Place,
605 + fireLoc: SourceLocation,
606 + ): Place {
607 const fireFunctionBinding = getOrInsertWith(
608 this.#fireCalleesToFireFunctions,
609 callee.identifier.id,
@@ -534,6 +613,7 @@ class Context {
613 this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
614 fireFunctionBinding,
615 capturedCalleeIdentifier: callee.identifier,
616 + fireLoc,
617 });
618
619 return fireFunctionBinding;
@@ -575,8 +655,12 @@ class Context {
655 return this.#loadGlobalInstructionIds.get(id);
656 }
657
658 + hasErrors(): boolean {
659 + return this.#errors.hasErrors();
660 + }
661 +
662 throwIfErrorsFound(): void {
579 - if (this.#errors.hasErrors()) throw this.#errors;
663 + if (this.hasErrors()) throw this.#errors;
664 }
665 }
666
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/basic.expect.md
+1 -1
@@ -21,7 +21,7 @@ function Component(props) {
21 ## Code
22
23 ```javascript
24 -import { useFire } from "react";
24 +import { useFire } from "react/compiler-runtime";
25 import { c as _c } from "react/compiler-runtime"; // @enableFire
26 import { fire } from "react";
27
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/deep-scope.expect.md
+1 -1
@@ -30,7 +30,7 @@ function Component(props) {
30 ## Code
31
32 ```javascript
33 -import { useFire } from "react";
33 +import { useFire } from "react/compiler-runtime";
34 import { c as _c } from "react/compiler-runtime"; // @enableFire
35 import { fire } from "react";
36
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.expect.md new
+39
@@ -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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-mix-fire-and-no-fire.js new
+18
@@ -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 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.expect.md new
+38
@@ -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 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/error.invalid-outside-effect.js new
+15
@@ -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 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/multiple-scope.expect.md
+1 -1
@@ -29,7 +29,7 @@ function Component(props) {
29 ## Code
30
31 ```javascript
32 -import { useFire } from "react";
32 +import { useFire } from "react/compiler-runtime";
33 import { c as _c } from "react/compiler-runtime"; // @enableFire
34 import { fire } from "react";
35
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/repeated-calls.expect.md
+1 -1
@@ -22,7 +22,7 @@ function Component(props) {
22 ## Code
23
24 ```javascript
25 -import { useFire } from "react";
25 +import { useFire } from "react/compiler-runtime";
26 import { c as _c } from "react/compiler-runtime"; // @enableFire
27 import { fire } from "react";
28
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/shared-hook-calls.expect.md
+1 -1
@@ -26,7 +26,7 @@ function Component({bar, baz}) {
26 ## Code
27
28 ```javascript
29 -import { useFire } from "react";
29 +import { useFire } from "react/compiler-runtime";
30 import { c as _c } from "react/compiler-runtime"; // @enableFire
31 import { fire } from "react";
32