Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,10 @@ export function compileProgram(

const hasFireRewrite = compiledFns.some(c => c.compiledFn.hasFireRewrite);
if (environment.enableFire && hasFireRewrite) {
externalFunctions.push({source: 'react', importSpecifierName: 'useFire'});
externalFunctions.push({
source: getReactCompilerRuntimeModule(pass.opts),
importSpecifierName: 'useFire',
});
}
} catch (err) {
handleError(err, pass, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,14 @@ export function printSourceLocation(loc: SourceLocation): string {
}
}

export function printSourceLocationLine(loc: SourceLocation): string {
if (typeof loc === 'symbol') {
return 'generated';
} else {
return `${loc.start.line}:${loc.end.line}`;
}
}

export function printAliases(aliases: DisjointSet<Identifier>): string {
const aliasSets = aliases.buildSets();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/

import {CompilerError, CompilerErrorDetailOptions, ErrorSeverity} from '..';
import {
CompilerError,
CompilerErrorDetailOptions,
ErrorSeverity,
SourceLocation,
} from '..';
import {
CallExpression,
Effect,
Expand All @@ -28,14 +33,11 @@ import {
import {createTemporaryPlace, markInstructionIds} from '../HIR/HIRBuilder';
import {getOrInsertWith} from '../Utils/utils';
import {BuiltInFireId, DefaultNonmutatingHook} from '../HIR/ObjectShape';
import {eachInstructionOperand} from '../HIR/visitors';
import {printSourceLocationLine} from '../HIR/PrintHIR';

/*
* TODO(jmbrown):
* In this stack:
* - Assert no lingering fire calls
* - Ensure a fired function is not called regularly elsewhere in the same effect
*
* Future:
* - rewrite dep arrays
* - traverse object methods
* - method calls
Expand All @@ -47,6 +49,9 @@ const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
export function transformFire(fn: HIRFunction): void {
const context = new Context(fn.env);
replaceFireFunctions(fn, context);
if (!context.hasErrors()) {
ensureNoMoreFireUses(fn, context);
}
context.throwIfErrorsFound();
}

Expand Down Expand Up @@ -120,6 +125,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
}
rewriteInstrs.set(loadUseEffectInstrId, newInstrs);
}
ensureNoRemainingCalleeCaptures(
lambda.loweredFunc.func,
context,
capturedCallees,
);
}
}
} else if (
Expand Down Expand Up @@ -159,7 +169,10 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
}

const fireFunctionBinding =
context.getOrGenerateFireFunctionBinding(loadLocal.place);
context.getOrGenerateFireFunctionBinding(
loadLocal.place,
value.loc,
);

loadLocal.place = {...fireFunctionBinding};

Expand Down Expand Up @@ -320,6 +333,69 @@ function visitFunctionExpressionAndPropagateFireDependencies(
return calleesCapturedByFnExpression;
}

/*
* eachInstructionOperand is not sufficient for our cases because:
* 1. fire is a global, which will not appear
* 2. The HIR may be malformed, so can't rely on function deps and must
* traverse the whole function.
*/
function* eachReachablePlace(fn: HIRFunction): Iterable<Place> {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
yield* eachReachablePlace(instr.value.loweredFunc.func);
} else {
yield* eachInstructionOperand(instr);
}
}
}
}

function ensureNoRemainingCalleeCaptures(
fn: HIRFunction,
context: Context,
capturedCallees: FireCalleesToFireFunctionBinding,
): void {
for (const place of eachReachablePlace(fn)) {
const calleeInfo = capturedCallees.get(place.identifier.id);
if (calleeInfo != null) {
const calleeName =
calleeInfo.capturedCalleeIdentifier.name?.kind === 'named'
? calleeInfo.capturedCalleeIdentifier.name.value
: '<unknown>';
context.pushError({
loc: place.loc,
description: `All uses of ${calleeName} must be either used with a fire() call in \
this effect or not used with a fire() call at all. ${calleeName} was used with fire() on line \
${printSourceLocationLine(calleeInfo.fireLoc)} in this effect`,
severity: ErrorSeverity.InvalidReact,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
}

function ensureNoMoreFireUses(fn: HIRFunction, context: Context): void {
for (const place of eachReachablePlace(fn)) {
if (
place.identifier.type.kind === 'Function' &&
place.identifier.type.shapeId === BuiltInFireId
) {
context.pushError({
loc: place.identifier.loc,
description: 'Cannot use `fire` outside of a useEffect function',
severity: ErrorSeverity.Invariant,
reason: CANNOT_COMPILE_FIRE,
suggestions: null,
});
}
}
}

function makeLoadUseFireInstruction(env: Environment): Instruction {
const useFirePlace = createTemporaryPlace(env, GeneratedSource);
useFirePlace.effect = Effect.Read;
Expand Down Expand Up @@ -422,6 +498,7 @@ type FireCalleesToFireFunctionBinding = Map<
{
fireFunctionBinding: Place;
capturedCalleeIdentifier: Identifier;
fireLoc: SourceLocation;
}
>;

Expand Down Expand Up @@ -523,8 +600,10 @@ class Context {
getLoadLocalInstr(id: IdentifierId): LoadLocal | undefined {
return this.#loadLocals.get(id);
}

getOrGenerateFireFunctionBinding(callee: Place): Place {
getOrGenerateFireFunctionBinding(
callee: Place,
fireLoc: SourceLocation,
): Place {
const fireFunctionBinding = getOrInsertWith(
this.#fireCalleesToFireFunctions,
callee.identifier.id,
Expand All @@ -534,6 +613,7 @@ class Context {
this.#capturedCalleeIdentifierIds.set(callee.identifier.id, {
fireFunctionBinding,
capturedCalleeIdentifier: callee.identifier,
fireLoc,
});

return fireFunctionBinding;
Expand Down Expand Up @@ -575,8 +655,12 @@ class Context {
return this.#loadGlobalInstructionIds.get(id);
}

hasErrors(): boolean {
return this.#errors.hasErrors();
}

throwIfErrorsFound(): void {
if (this.#errors.hasErrors()) throw this.#errors;
if (this.hasErrors()) throw this.#errors;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function Component(props) {
## Code

```javascript
import { useFire } from "react";
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function Component(props) {
## Code

```javascript
import { useFire } from "react";
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

## Input

```javascript
// @enableFire
import {fire} from 'react';

function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
function nested() {
fire(foo(props));
foo(props);
}

nested();
});

return null;
}

```


## Error

```
9 | function nested() {
10 | fire(foo(props));
> 11 | foo(props);
| ^^^ 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)
12 | }
13 |
14 | nested();
```


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @enableFire
import {fire} from 'react';

function Component(props) {
const foo = props => {
console.log(props);
};
useEffect(() => {
function nested() {
fire(foo(props));
foo(props);
}

nested();
});

return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

## Input

```javascript
// @enableFire
import {fire, useCallback} from 'react';

function Component({props, bar}) {
const foo = () => {
console.log(props);
};
fire(foo(props));

useCallback(() => {
fire(foo(props));
}, [foo, props]);

return null;
}

```


## Error

```
6 | console.log(props);
7 | };
> 8 | fire(foo(props));
| ^^^^ Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (8:8)

Invariant: Cannot compile `fire`. Cannot use `fire` outside of a useEffect function (11:11)
9 |
10 | useCallback(() => {
11 | fire(foo(props));
```


Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @enableFire
import {fire, useCallback} from 'react';

function Component({props, bar}) {
const foo = () => {
console.log(props);
};
fire(foo(props));

useCallback(() => {
fire(foo(props));
}, [foo, props]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function Component(props) {
## Code

```javascript
import { useFire } from "react";
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function Component(props) {
## Code

```javascript
import { useFire } from "react";
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function Component({bar, baz}) {
## Code

```javascript
import { useFire } from "react";
import { useFire } from "react/compiler-runtime";
import { c as _c } from "react/compiler-runtime"; // @enableFire
import { fire } from "react";

Expand Down
Loading