Skip to content

Commit

Permalink
[compiler] Allow lets to be hoisted
Browse files Browse the repository at this point in the history
ghstack-source-id: 02f4698bd98705a855deb0d4bc30b9829afdddc0
Pull Request resolved: #30674
  • Loading branch information
mvitousek committed Aug 13, 2024
1 parent 0d7c12c commit b3b7c98
Show file tree
Hide file tree
Showing 19 changed files with 381 additions and 51 deletions.
16 changes: 13 additions & 3 deletions compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,11 @@ function lowerStatement(
loc: id.parentPath.node.loc ?? GeneratedSource,
});
continue;
} else if (binding.kind !== 'const' && binding.kind !== 'var') {
// Avoid double errors on var declarations, which we do not plan to support anyways
} else if (
binding.kind !== 'const' &&
binding.kind !== 'var' &&
binding.kind !== 'let'
) {
builder.errors.push({
severity: ErrorSeverity.Todo,
reason: 'Handle non-const declarations for hoisting',
Expand All @@ -463,10 +466,17 @@ function lowerStatement(
reactive: false,
loc: id.node.loc ?? GeneratedSource,
};
const kind =
// Avoid double errors on var declarations, which we do not plan to support anyways
binding.kind === 'const' || binding.kind === 'var'
? InstructionKind.HoistedConst
: binding.kind === 'let'
? InstructionKind.HoistedLet
: assertExhaustive(binding.kind, 'Unexpected binding kind');
lowerValueToTemporary(builder, {
kind: 'DeclareContext',
lvalue: {
kind: InstructionKind.HoistedConst,
kind,
place,
},
loc: id.node.loc ?? GeneratedSource,
Expand Down
8 changes: 7 additions & 1 deletion compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,9 @@ export enum InstructionKind {

// hoisted const declarations
HoistedConst = 'HoistedConst',

// hoisted const declarations
HoistedLet = 'HoistedLet',
}

function _staticInvariantInstructionValueHasLocation(
Expand Down Expand Up @@ -858,7 +861,10 @@ export type InstructionValue =
| {
kind: 'DeclareContext';
lvalue: {
kind: InstructionKind.Let | InstructionKind.HoistedConst;
kind:
| InstructionKind.Let
| InstructionKind.HoistedConst
| InstructionKind.HoistedLet;
place: Place;
};
loc: SourceLocation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,9 @@ export function printLValue(lval: LValue): string {
case InstructionKind.HoistedConst: {
return `HoistedConst ${lvalue}$`;
}
case InstructionKind.HoistedLet: {
return `HoistedLet ${lvalue}$`;
}
default: {
assertExhaustive(lval.kind, `Unexpected lvalue kind \`${lval.kind}\``);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,13 @@ function codegenTerminal(
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.HoistedLet:
CompilerError.invariant(false, {
reason: 'Unexpected HoistedLet variable in for..in collection',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
default:
assertExhaustive(
iterableItem.value.lvalue.kind,
Expand Down Expand Up @@ -1089,6 +1096,13 @@ function codegenTerminal(
loc: iterableItem.loc,
suggestions: null,
});
case InstructionKind.HoistedLet:
CompilerError.invariant(false, {
reason: 'Unexpected HoistedLet variable in for..of collection',
description: null,
loc: iterableItem.loc,
suggestions: null,
});
default:
assertExhaustive(
iterableItem.value.lvalue.kind,
Expand Down Expand Up @@ -1289,6 +1303,15 @@ function codegenInstructionNullable(
case InstructionKind.Catch: {
return t.emptyStatement();
}
case InstructionKind.HoistedLet: {
CompilerError.invariant(false, {
reason:
'Expected HoistedLet to have been pruned in PruneHoistedContexts',
description: null,
loc: instr.loc,
suggestions: null,
});
}
case InstructionKind.HoistedConst: {
CompilerError.invariant(false, {
reason:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import {
* original instruction kind.
*/
export function pruneHoistedContexts(fn: ReactiveFunction): void {
const hoistedIdentifiers: HoistedIdentifiers = new Set();
const hoistedIdentifiers: HoistedIdentifiers = new Map();
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
}

type HoistedIdentifiers = Set<DeclarationId>;
type HoistedIdentifiers = Map<DeclarationId, InstructionKind>;

class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
override transformInstruction(
Expand All @@ -39,14 +39,31 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedConst'
) {
state.add(instruction.value.lvalue.place.identifier.declarationId);
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Const,
);
return {kind: 'remove'};
}

if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedLet'
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
InstructionKind.Let,
);
return {kind: 'remove'};
}

if (
instruction.value.kind === 'StoreContext' &&
state.has(instruction.value.lvalue.place.identifier.declarationId)
) {
const kind = state.get(
instruction.value.lvalue.place.identifier.declarationId,
)!;
return {
kind: 'replace',
value: {
Expand All @@ -57,7 +74,7 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
...instruction.value,
lvalue: {
...instruction.value.lvalue,
kind: InstructionKind.Const,
kind,
},
type: null,
kind: 'StoreLocal',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ function getContextReassignment(
*/
contextVariables.add(value.lvalue.place.identifier.id);
}
const reassignment = reassigningFunctions.get(
value.value.identifier.id,
);
if (reassignment !== undefined) {
reassigningFunctions.set(
value.lvalue.place.identifier.id,
reassignment,
);
reassigningFunctions.set(lvalue.identifier.id, reassignment);
}
break;
}
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function Component() {
1 | function Component() {
2 | let callback = () => {
> 3 | callback = null;
| ^^^^^^^^^^^^^^^ Todo: Handle non-const declarations for hoisting. variable "callback" declared with let (3:3)
| ^^^^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `callback` cannot be reassigned after render (3:3)
4 | };
5 | return <div onClick={callback} />;
6 | }
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Component() {
1 | function Component() {
2 | let callback = () => {
> 3 | onClick = () => {};
| ^^^^^^^^^^^^^^^^^^ Todo: Handle non-const declarations for hoisting. variable "onClick" declared with let (3:3)
| ^^^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `onClick` cannot be reassigned after render (3:3)
4 | };
5 | let onClick;
6 |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

## Input

```javascript
function hoisting(cond) {
let items = [];
if (cond) {
let foo = () => {
items.push(bar());
};
let bar = () => true;
foo();
}
return items;
}

export const FIXTURE_ENTRYPOINT = {
fn: hoisting,
params: [true],
isComponent: false,
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime";
function hoisting(cond) {
const $ = _c(2);
let items;
if ($[0] !== cond) {
items = [];
if (cond) {
const foo = () => {
items.push(bar());
};

let bar = _temp;
foo();
}
$[0] = cond;
$[1] = items;
} else {
items = $[1];
}
return items;
}
function _temp() {
return true;
}

export const FIXTURE_ENTRYPOINT = {
fn: hoisting,
params: [true],
isComponent: false,
};

```
### Eval output
(kind: ok) [true]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function hoisting(cond) {
let items = [];
if (cond) {
let foo = () => {
items.push(bar());
};
let bar = () => true;
foo();
}
return items;
}

export const FIXTURE_ENTRYPOINT = {
fn: hoisting,
params: [true],
isComponent: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

## Input

```javascript
function hoisting() {
let qux = () => {
let result;
{
result = foo();
}
return result;
};
let foo = () => {
return bar + baz;
};
let bar = 3;
const baz = 2;
return qux(); // OK: called outside of TDZ
}

export const FIXTURE_ENTRYPOINT = {
fn: hoisting,
params: [],
isComponent: false,
};

```

## Code

```javascript
import { c as _c } from "react/compiler-runtime";
function hoisting() {
const $ = _c(1);
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
const qux = () => {
let result;

result = foo();
return result;
};

let foo = () => bar + baz;

let bar = 3;
const baz = 2;
t0 = qux();
$[0] = t0;
} else {
t0 = $[0];
}
return t0;
}

export const FIXTURE_ENTRYPOINT = {
fn: hoisting,
params: [],
isComponent: false,
};

```
### Eval output
(kind: ok) 5
Loading

0 comments on commit b3b7c98

Please sign in to comment.