[compiler][bugfix] Fix hoisting of let declarations (#32724)

(Found when compiling Meta React code)

Let variable declarations and reassignments are currently rewritten to
`StoreLocal <varName>` instructions, which each translates to a new
`const varName` declaration in codegen.

```js
// Example input
function useHook() {
  const getX = () => x;
  let x = CONSTANT1;
  if (cond) {
    x += CONSTANT2;
  }
  return <Stringify getX={getX} />
}

// Compiled output, prior to this PR
import { c as _c } from "react/compiler-runtime";
function useHook() {
  const $ = _c(1);
  let t0;
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
    const getX = () => x;
    let x = CONSTANT1;
    if (cond) {
      let x = x + CONSTANT2;
      x;
    }

    t0 = <Stringify getX={getX} />;
    $[0] = t0;
  } else {
    t0 = $[0];
  }
  return t0;
}
```

This also manifests as a babel internal error when replacing the
original function declaration with the compiler output. The below
compilation output fails with `Duplicate declaration "x" (This is an
error on an internal node. Probably an internal error.)`.
```js
// example input
let x = CONSTANT1;
if (cond) {
  x += CONSTANT2;
  x = CONSTANT3;
}

// current output
let x = CONSTANT1;
if (playheadDragState) {
  let x = x + CONSTANT2
  x;
  let x = CONSTANT3;
}
```
This commit is contained in:
mofeiZ
2025-03-24 14:30:17 -04:00
committed by GitHub
parent 42a57ea802
commit 254dc4d9f3
13 changed files with 378 additions and 27 deletions

View File

@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
DeclarationId,
InstructionKind,
@@ -27,7 +28,17 @@ export function pruneHoistedContexts(fn: ReactiveFunction): void {
visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
}
type HoistedIdentifiers = Map<DeclarationId, InstructionKind>;
const REWRITTEN_HOISTED_CONST: unique symbol = Symbol(
'REWRITTEN_HOISTED_CONST',
);
const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET');
type HoistedIdentifiers = Map<
DeclarationId,
| InstructionKind
| typeof REWRITTEN_HOISTED_CONST
| typeof REWRITTEN_HOISTED_LET
>;
class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
override transformInstruction(
@@ -35,6 +46,10 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
state: HoistedIdentifiers,
): Transformed<ReactiveStatement> {
this.visitInstruction(instruction, state);
/**
* Remove hoisted declarations to preserve TDZ
*/
if (
instruction.value.kind === 'DeclareContext' &&
instruction.value.lvalue.kind === 'HoistedConst'
@@ -68,31 +83,75 @@ class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
return {kind: 'remove'};
}
if (
instruction.value.kind === 'StoreContext' &&
state.has(instruction.value.lvalue.place.identifier.declarationId)
) {
if (instruction.value.kind === 'StoreContext') {
const kind = state.get(
instruction.value.lvalue.place.identifier.declarationId,
)!;
return {
kind: 'replace',
value: {
kind: 'instruction',
instruction: {
...instruction,
);
if (kind != null) {
CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, {
reason: 'Expected exactly one store to a hoisted const variable',
loc: instruction.loc,
});
if (
kind === InstructionKind.Const ||
kind === InstructionKind.Function
) {
state.set(
instruction.value.lvalue.place.identifier.declarationId,
REWRITTEN_HOISTED_CONST,
);
return {
kind: 'replace',
value: {
...instruction.value,
lvalue: {
...instruction.value.lvalue,
kind,
kind: 'instruction',
instruction: {
...instruction,
value: {
...instruction.value,
lvalue: {
...instruction.value.lvalue,
kind,
},
type: null,
kind: 'StoreLocal',
},
},
type: null,
kind: 'StoreLocal',
},
},
},
};
};
} else if (kind !== REWRITTEN_HOISTED_LET) {
/**
* Context variables declared with let may have reassignments. Only
* insert a `DeclareContext` for the first encountered `StoreContext`
* instruction.
*/
state.set(
instruction.value.lvalue.place.identifier.declarationId,
REWRITTEN_HOISTED_LET,
);
return {
kind: 'replace-many',
value: [
{
kind: 'instruction',
instruction: {
id: instruction.id,
lvalue: null,
value: {
kind: 'DeclareContext',
lvalue: {
kind: InstructionKind.Let,
place: {...instruction.value.lvalue.place},
},
loc: instruction.value.loc,
},
loc: instruction.loc,
},
},
{kind: 'instruction', instruction},
],
};
}
}
}
return {kind: 'keep'};

View File

@@ -0,0 +1,59 @@
## Input
```javascript
function Foo() {
const getX = () => x;
console.log(getX());
let x = 4;
x += 5;
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
function Foo() {
const $ = _c(2);
let getX;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
getX = () => x;
console.log(getX());
let x;
x = 4;
x = x + 5;
$[0] = getX;
} else {
getX = $[0];
}
x;
let t0;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t0 = <Stringify getX={getX} shouldInvokeFns={true} />;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};
```
### Eval output
(kind: exception) Cannot access 'x' before initialization

View File

@@ -0,0 +1,14 @@
function Foo() {
const getX = () => x;
console.log(getX());
let x = 4;
x += 5;
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [],
};

View File

@@ -36,7 +36,8 @@ function hoisting(cond) {
items.push(bar());
};
let bar = _temp;
let bar;
bar = _temp;
foo();
}
$[0] = cond;

View File

@@ -41,9 +41,11 @@ function hoisting() {
return result;
};
let foo = () => bar + baz;
let foo;
foo = () => bar + baz;
let bar = 3;
let bar;
bar = 3;
const baz = 2;
t0 = qux();
$[0] = t0;

View File

@@ -0,0 +1,67 @@
## Input
```javascript
import {CONST_NUMBER0, CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x = CONST_NUMBER0;
if (cond) {
x += CONST_NUMBER1;
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_NUMBER0, CONST_NUMBER1, Stringify } from "shared-runtime";
function useHook(t0) {
"use memo";
const $ = _c(2);
const { cond } = t0;
let t1;
if ($[0] !== cond) {
const getX = () => x;
let x;
x = CONST_NUMBER0;
if (cond) {
x = x + CONST_NUMBER1;
x;
}
t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
$[0] = cond;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
};
```
### Eval output
(kind: ok) <div>{"getX":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
<div>{"getX":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
<div>{"getX":{"kind":"Function","result":0},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,18 @@
import {CONST_NUMBER0, CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x = CONST_NUMBER0;
if (cond) {
x += CONST_NUMBER1;
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};

View File

@@ -0,0 +1,69 @@
## Input
```javascript
import {CONST_NUMBER0, CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x = CONST_NUMBER0;
if (cond) {
x += CONST_NUMBER1;
x = Math.min(x, 100);
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { CONST_NUMBER0, CONST_NUMBER1, Stringify } from "shared-runtime";
function useHook(t0) {
"use memo";
const $ = _c(2);
const { cond } = t0;
let t1;
if ($[0] !== cond) {
const getX = () => x;
let x;
x = CONST_NUMBER0;
if (cond) {
x = x + CONST_NUMBER1;
x;
x = Math.min(x, 100);
}
t1 = <Stringify getX={getX} shouldInvokeFns={true} />;
$[0] = cond;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }],
};
```
### Eval output
(kind: ok) <div>{"getX":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
<div>{"getX":{"kind":"Function","result":1},"shouldInvokeFns":true}</div>
<div>{"getX":{"kind":"Function","result":0},"shouldInvokeFns":true}</div>

View File

@@ -0,0 +1,19 @@
import {CONST_NUMBER0, CONST_NUMBER1, Stringify} from 'shared-runtime';
function useHook({cond}) {
'use memo';
const getX = () => x;
let x = CONST_NUMBER0;
if (cond) {
x += CONST_NUMBER1;
x = Math.min(x, 100);
}
return <Stringify getX={getX} shouldInvokeFns={true} />;
}
export const FIXTURE_ENTRYPOINT = {
fn: useHook,
params: [{cond: true}],
sequentialRenders: [{cond: true}, {cond: true}, {cond: false}],
};

View File

@@ -29,8 +29,10 @@ function hoisting() {
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
foo = () => bar + baz;
let bar = 3;
let baz = 2;
let bar;
bar = 3;
let baz;
baz = 2;
$[0] = foo;
} else {
foo = $[0];

View File

@@ -33,7 +33,8 @@ function component(a) {
m(x);
};
let x = { a };
let x;
x = { a };
m(x);
$[0] = a;
$[1] = y;

View File

@@ -2,6 +2,17 @@
## Input
```javascript
function Component1() {
const x = callback(10);
function callback(x) {
if (x == 0) {
return null;
}
return callback(x - 1);
}
return x;
}
function Component() {
function callback(x) {
if (x == 0) {
@@ -23,6 +34,24 @@ export const FIXTURE_ENTRYPOINT = {
```javascript
import { c as _c } from "react/compiler-runtime";
function Component1() {
const $ = _c(1);
let x;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
x = callback(10);
function callback(x_0) {
if (x_0 == 0) {
return null;
}
return callback(x_0 - 1);
}
$[0] = x;
} else {
x = $[0];
}
return x;
}
function Component() {
const $ = _c(1);
let t0;

View File

@@ -1,3 +1,14 @@
function Component1() {
const x = callback(10);
function callback(x) {
if (x == 0) {
return null;
}
return callback(x - 1);
}
return x;
}
function Component() {
function callback(x) {
if (x == 0) {