Support hoisting const variable declarations
This PR adds preliminary support for hoisting const variable declarations. We do this via BuildHIR when lowering top level statements in a BlockStatement, by first checking which bindings are in scope to be hoistable if referenced before they are declared. The declarations are then hoisted to their earliest point where they are referenced (ie the top level statement just before) as context variables. Later, prior to codegen, we restore the original source by removing the DeclareContexts and transforming their associated StoreContexts back. Support for hoisting other kinds of declarations will come in future PRs!
Lauren Tan committed
Sep 21, 2023 at 17:00 UTC
1ce952e687b7d2031bf3d6aa69f917506bc2c2cb
24 files changed
+650
-66
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
+8
@@ -46,6 +46,7 @@ import {
46
promoteUsedTemporaries,
47
propagateScopeDependencies,
48
pruneAllReactiveScopes,
49
+ pruneHoistedContexts,
50
pruneNonEscapingScopes,
51
pruneNonReactiveDependencies,
52
pruneUnusedLValues,
@@ -299,6 +300,13 @@ export function* run(
300
value: reactiveFunction,
301
});
302
303
+ pruneHoistedContexts(reactiveFunction);
304
+ yield log({
305
+ kind: "reactive",
306
+ name: "PruneHoistedContexts",
307
+ value: reactiveFunction,
308
+ });
309
+
310
const ast = codegenReactiveFunction(reactiveFunction).unwrap();
311
yield log({ kind: "ast", name: "Codegen", value: ast });
312
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+108
-4
@@ -5,7 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import { NodePath, Scope } from "@babel/traverse";
8
+import { Binding, NodePath, Scope } from "@babel/traverse";
9
import * as t from "@babel/types";
10
import { Expression } from "@babel/types";
11
import invariant from "invariant";
@@ -298,7 +298,108 @@ function lowerStatement(
298
}
299
case "BlockStatement": {
300
const stmt = stmtPath as NodePath<t.BlockStatement>;
301
- stmt.get("body").forEach((s) => lowerStatement(builder, s));
301
+ const statements = stmt.get("body");
302
+ const hoistableBindings: Set<Binding> = new Set();
303
+
304
+ const recordDeclaration = (lval: NodePath<t.LVal>): void => {
305
+ // TODO: support other kinds of declarations that might need to be hoisted
306
+ switch (lval.type) {
307
+ case "Identifier": {
308
+ const lv = lval as NodePath<t.Identifier>;
309
+ const binding = stmt.scope.getBinding(lv.node.name);
310
+ if (binding != null) {
311
+ hoistableBindings.delete(binding);
312
+ }
313
+ break;
314
+ }
315
+ }
316
+ };
317
+
318
+ for (const [, binding] of Object.entries(stmt.scope.bindings)) {
319
+ // TODO: support other kinds of bindings
320
+ if (binding.kind === "const") {
321
+ if (
322
+ binding.path.isVariableDeclarator() &&
323
+ binding.path.get("id").isIdentifier()
324
+ ) {
325
+ hoistableBindings.add(binding);
326
+ }
327
+ }
328
+ }
329
+
330
+ for (const s of statements) {
331
+ const hoistableIdentifiers = new Set<NodePath<t.Identifier>>();
332
+ // After visiting the declaration, hoisting is no longer required
333
+ // TODO: support other kinds of declarations
334
+ if (s.isVariableDeclaration()) {
335
+ for (const decl of s.get("declarations")) {
336
+ recordDeclaration(decl.get("id"));
337
+ }
338
+ }
339
+
340
+ // If we see a hoistable identifier before its declaration, it should be hoisted just
341
+ // before the statement that references it
342
+ s.traverse({
343
+ Identifier(id: NodePath<t.Identifier>) {
344
+ const binding = stmt.scope.getBinding(id.node.name);
345
+ if (binding != null && hoistableBindings.has(binding)) {
346
+ if (
347
+ id.parentPath.isVariableDeclarator() ||
348
+ // don't hoist MemberExpr `property`s, only their `object`
349
+ (id.parentPath.isMemberExpression() &&
350
+ id.parentPath.get("property") === id &&
351
+ id.parentPath.node.computed === false)
352
+ ) {
353
+ return;
354
+ }
355
+ hoistableIdentifiers.add(id);
356
+ }
357
+ },
358
+ });
359
+
360
+ // Hoist declarations that need it to the earliest point where they are needed
361
+ for (const id of hoistableIdentifiers) {
362
+ const binding = stmt.scope.getBinding(id.node.name);
363
+ CompilerError.invariant(binding != null, {
364
+ reason: "Expected to find binding for hoisted identifier",
365
+ description: `Could not find a binding for ${id.node.name}`,
366
+ suggestions: null,
367
+ loc: id.node.loc ?? GeneratedSource,
368
+ });
369
+ if (builder.environment.isHoistedIdentifier(binding.identifier)) {
370
+ // Already hoisted
371
+ continue;
372
+ }
373
+ if (!binding.path.isVariableDeclarator()) {
374
+ builder.errors.push({
375
+ severity: ErrorSeverity.Todo,
376
+ reason: "Unsupported declaration type for hoisting",
377
+ description: `${id.parentPath.type}`,
378
+ suggestions: null,
379
+ loc: id.parentPath.node.loc ?? GeneratedSource,
380
+ });
381
+ continue;
382
+ }
383
+ const identifier = builder.resolveIdentifier(id)!;
384
+ const place: Place = {
385
+ effect: Effect.Unknown,
386
+ identifier,
387
+ kind: "Identifier",
388
+ loc: id.node.loc ?? GeneratedSource,
389
+ };
390
+ lowerValueToTemporary(builder, {
391
+ kind: "DeclareContext",
392
+ lvalue: {
393
+ kind: InstructionKind.HoistedConst,
394
+ place,
395
+ },
396
+ loc: id.node.loc ?? GeneratedSource,
397
+ });
398
+ builder.environment.addHoistedIdentifier(binding.identifier);
399
+ }
400
+ lowerStatement(builder, s);
401
+ }
402
+
403
return;
404
}
405
case "BreakStatement": {
@@ -2894,13 +2995,16 @@ function lowerAssignment(
2995
node: lvalue.node,
2996
};
2997
}
2998
+ const isHoistedIdentifier = builder.environment.isHoistedIdentifier(
2999
+ lvalue.node
3000
+ );
3001
3002
let temporary;
3003
if (builder.isContextIdentifier(lvalue)) {
2900
- if (kind !== InstructionKind.Reassign) {
3004
+ if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
3005
if (kind === InstructionKind.Const) {
3006
builder.errors.push({
2903
- reason: `Invalid declaration kind (const), this variable is reassigned later`,
3007
+ reason: `[lowerAssignment] Invalid declaration kind (const), this variable is reassigned later`,
3008
severity: ErrorSeverity.InvalidJS,
3009
loc: lvalue.node.loc ?? null,
3010
suggestions: null,
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+12
@@ -235,6 +235,7 @@ export class Environment {
235
enableForest: boolean;
236
237
#contextIdentifiers: Set<t.Identifier>;
238
+ #hoistedIdentifiers: Set<t.Identifier>;
239
240
constructor(
241
config: EnvironmentConfig | null,
@@ -289,6 +290,7 @@ export class Environment {
290
this.enableForest = config?.enableForest ?? false;
291
292
this.#contextIdentifiers = contextIdentifiers;
293
+ this.#hoistedIdentifiers = new Set();
294
}
295
296
get nextIdentifierId(): IdentifierId {
@@ -298,10 +300,15 @@ export class Environment {
300
get nextBlockId(): BlockId {
301
return makeBlockId(this.#nextBlock++);
302
}
303
+
304
isContextIdentifier(node: t.Identifier): boolean {
305
return this.#contextIdentifiers.has(node);
306
}
307
308
+ isHoistedIdentifier(node: t.Identifier): boolean {
309
+ return this.#hoistedIdentifiers.has(node);
310
+ }
311
+
312
getGlobalDeclaration(name: string): Global | null {
313
let resolvedGlobal: Global | null = this.#globals.get(name) ?? null;
314
if (resolvedGlobal === null) {
@@ -356,6 +363,11 @@ export class Environment {
363
}
364
return null;
365
}
366
+
367
+ addHoistedIdentifier(node: t.Identifier): void {
368
+ this.#contextIdentifiers.add(node);
369
+ this.#hoistedIdentifiers.add(node);
370
+ }
371
}
372
373
// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+7
-1
@@ -586,6 +586,11 @@ export enum InstructionKind {
586
* catch clause binding
587
*/
588
Catch = "Catch",
589
+
590
+ /**
591
+ * hoisted const declarations
592
+ */
593
+ HoistedConst = "HoistedConst",
594
}
595
596
function _staticInvariantInstructionValueHasLocation(
@@ -641,6 +646,7 @@ export type CallExpression = {
646
*
647
* Operands are therefore always a Place.
648
*/
649
+
650
export type InstructionValue =
651
| {
652
kind: "LoadLocal";
@@ -660,7 +666,7 @@ export type InstructionValue =
666
| {
667
kind: "DeclareContext";
668
lvalue: {
663
- kind: InstructionKind.Let;
669
+ kind: InstructionKind.Let | InstructionKind.HoistedConst;
670
place: Place;
671
};
672
loc: SourceLocation;
compiler/packages/babel-plugin-react-forget/src/HIR/PrintHIR.ts
+3
@@ -602,6 +602,9 @@ export function printLValue(lval: LValue): string {
602
case InstructionKind.Catch: {
603
return `Catch ${lvalue}`;
604
}
605
+ case InstructionKind.HoistedConst: {
606
+ return `HoistedConst ${lvalue}$`;
607
+ }
608
default: {
609
assertExhaustive(lval.kind, `Unexpected lvalue kind '${lval.kind}'`);
610
}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+16
@@ -467,6 +467,13 @@ function codegenTerminal(
467
loc: iterableItem.loc,
468
suggestions: null,
469
});
470
+ case InstructionKind.HoistedConst:
471
+ CompilerError.invariant(false, {
472
+ reason: "Unexpected HoistedConst variable in for-of collection",
473
+ description: null,
474
+ loc: iterableItem.loc,
475
+ suggestions: null,
476
+ });
477
default:
478
assertExhaustive(
479
iterableItem.value.lvalue.kind,
@@ -671,6 +678,15 @@ function codegenInstructionNullable(
678
case InstructionKind.Catch: {
679
return t.emptyStatement();
680
}
681
+ case InstructionKind.HoistedConst: {
682
+ CompilerError.invariant(false, {
683
+ reason:
684
+ "Expected HoistedConsts to have been pruned in PruneHoistedContexts",
685
+ description: null,
686
+ loc: instr.loc,
687
+ suggestions: null,
688
+ });
689
+ }
690
default: {
691
assertExhaustive(kind, `Unexpected instruction kind '${kind}'`);
692
}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/PruneHoistedContexts.ts
new
+64
@@ -0,0 +1,64 @@
1
+import {
2
+ Identifier,
3
+ InstructionKind,
4
+ ReactiveFunction,
5
+ ReactiveInstruction,
6
+ ReactiveStatement,
7
+} from "../HIR";
8
+import {
9
+ ReactiveFunctionTransform,
10
+ Transformed,
11
+ visitReactiveFunction,
12
+} from "./visitors";
13
+
14
+/**
15
+ * Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
16
+ * original instruction kind.
17
+ */
18
+export function pruneHoistedContexts(fn: ReactiveFunction): void {
19
+ const hoistedIdentifiers: HoistedIdentifiers = new Set();
20
+ visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers);
21
+}
22
+
23
+type HoistedIdentifiers = Set<Identifier>;
24
+
25
+class Visitor extends ReactiveFunctionTransform<HoistedIdentifiers> {
26
+ override transformInstruction(
27
+ instruction: ReactiveInstruction,
28
+ state: HoistedIdentifiers
29
+ ): Transformed<ReactiveStatement> {
30
+ this.visitInstruction(instruction, state);
31
+ if (
32
+ instruction.value.kind === "DeclareContext" &&
33
+ instruction.value.lvalue.kind === "HoistedConst"
34
+ ) {
35
+ state.add(instruction.value.lvalue.place.identifier);
36
+ return { kind: "remove" };
37
+ }
38
+
39
+ if (
40
+ instruction.value.kind === "StoreContext" &&
41
+ state.has(instruction.value.lvalue.place.identifier)
42
+ ) {
43
+ return {
44
+ kind: "replace",
45
+ value: {
46
+ kind: "instruction",
47
+ instruction: {
48
+ ...instruction,
49
+ value: {
50
+ ...instruction.value,
51
+ lvalue: {
52
+ ...instruction.value.lvalue,
53
+ kind: InstructionKind.Const,
54
+ },
55
+ kind: "StoreLocal",
56
+ },
57
+ },
58
+ },
59
+ };
60
+ }
61
+
62
+ return { kind: "keep" };
63
+ }
64
+}
compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/index.ts
+1
@@ -22,6 +22,7 @@ export { printReactiveFunction } from "./PrintReactiveFunction";
22
export { promoteUsedTemporaries } from "./PromoteUsedTemporaries";
23
export { propagateScopeDependencies } from "./PropagateScopeDependencies";
24
export { pruneAllReactiveScopes } from "./PruneAllReactiveScopes";
25
+export { pruneHoistedContexts } from "./PruneHoistedContexts";
26
export { pruneNonEscapingScopes } from "./PruneNonEscapingScopes";
27
export { pruneNonReactiveDependencies } from "./PruneNonReactiveDependencies";
28
export { pruneTemporaryLValues as pruneUnusedLValues } from "./PruneTemporaryLValues";
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.expect.md
renamed
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.hoisting-simple-function-declaration.js
renamed
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-hoisting-simple-function-expression.expect.md
deleted
-31
@@ -1,31 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function hoisting() {
6
- const foo = () => {
7
- return bar();
8
- };
9
- const bar = () => {
10
- return 1;
11
- };
12
-
13
- return foo(); // OK: bar's value is only accessed outside of its TDZ
14
-}
15
-
16
-export const FIXTURE_ENTRYPOINT = {
17
- fn: hoisting,
18
- params: [],
19
- isComponent: false,
20
-};
21
-
22
-```
23
-
24
-
25
-## Error
26
-
27
-```
28
-[ReactForget] Todo: EnterSSA: Expected identifier to be defined before being used. Identifier bar$0 is undefined (5:7)
29
-```
30
-
31
-
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-hoisting-simple-let-const-declaration.expect.md
deleted
-29
@@ -1,29 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-function hoisting() {
6
- const foo = () => {
7
- return bar + baz;
8
- };
9
- let bar = 3;
10
- const baz = 2;
11
- return foo(); // OK: called outside of TDZ for bar/baz
12
-}
13
-
14
-export const FIXTURE_ENTRYPOINT = {
15
- fn: hoisting,
16
- params: [],
17
- isComponent: false,
18
-};
19
-
20
-```
21
-
22
-
23
-## Error
24
-
25
-```
26
-[ReactForget] Todo: EnterSSA: Expected identifier to be defined before being used. Identifier bar$0 is undefined (5:5)
27
-```
28
-
29
-
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-computed-member-expression.expect.md
new
+67
@@ -0,0 +1,67 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function hoisting() {
6
+ function onClick(x) {
7
+ return x + bar["baz"];
8
+ }
9
+ function onClick2(x) {
10
+ return x + bar[baz];
11
+ }
12
+ const baz = "baz";
13
+ const bar = { baz: 1 };
14
+
15
+ return <Button onClick={onClick} onClick2={onClick2} />;
16
+}
17
+
18
+export const FIXTURE_ENTRYPOINT = {
19
+ fn: hoisting,
20
+ params: [],
21
+ isComponent: false,
22
+};
23
+
24
+```
25
+
26
+## Code
27
+
28
+```javascript
29
+import { unstable_useMemoCache as useMemoCache } from "react";
30
+function hoisting() {
31
+ const $ = useMemoCache(3);
32
+ let onClick;
33
+ let onClick2;
34
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35
+ onClick = function onClick(x) {
36
+ return x + bar.baz;
37
+ };
38
+ onClick2 = function onClick2(x_0) {
39
+ return x_0 + bar[baz];
40
+ };
41
+
42
+ const baz = "baz";
43
+ const bar = { baz: 1 };
44
+ $[0] = onClick;
45
+ $[1] = onClick2;
46
+ } else {
47
+ onClick = $[0];
48
+ onClick2 = $[1];
49
+ }
50
+ let t0;
51
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
52
+ t0 = <Button onClick={onClick} onClick2={onClick2} />;
53
+ $[2] = t0;
54
+ } else {
55
+ t0 = $[2];
56
+ }
57
+ return t0;
58
+}
59
+
60
+export const FIXTURE_ENTRYPOINT = {
61
+ fn: hoisting,
62
+ params: [],
63
+ isComponent: false,
64
+};
65
+
66
+```
67
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-computed-member-expression.js
new
+18
@@ -0,0 +1,18 @@
1
+function hoisting() {
2
+ function onClick(x) {
3
+ return x + bar["baz"];
4
+ }
5
+ function onClick2(x) {
6
+ return x + bar[baz];
7
+ }
8
+ const baz = "baz";
9
+ const bar = { baz: 1 };
10
+
11
+ return <Button onClick={onClick} onClick2={onClick2} />;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: hoisting,
16
+ params: [],
17
+ isComponent: false,
18
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-member-expression.expect.md
new
+56
@@ -0,0 +1,56 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function hoisting() {
6
+ function onClick(x) {
7
+ return x + bar.baz;
8
+ }
9
+ const bar = { baz: 1 };
10
+
11
+ return <Button onClick={onClick} />;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: hoisting,
16
+ params: [],
17
+ isComponent: false,
18
+};
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+import { unstable_useMemoCache as useMemoCache } from "react";
26
+function hoisting() {
27
+ const $ = useMemoCache(2);
28
+ let onClick;
29
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30
+ onClick = function onClick(x) {
31
+ return x + bar.baz;
32
+ };
33
+
34
+ const bar = { baz: 1 };
35
+ $[0] = onClick;
36
+ } else {
37
+ onClick = $[0];
38
+ }
39
+ let t0;
40
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
41
+ t0 = <Button onClick={onClick} />;
42
+ $[1] = t0;
43
+ } else {
44
+ t0 = $[1];
45
+ }
46
+ return t0;
47
+}
48
+
49
+export const FIXTURE_ENTRYPOINT = {
50
+ fn: hoisting,
51
+ params: [],
52
+ isComponent: false,
53
+};
54
+
55
+```
56
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-member-expression.js
new
+14
@@ -0,0 +1,14 @@
1
+function hoisting() {
2
+ function onClick(x) {
3
+ return x + bar.baz;
4
+ }
5
+ const bar = { baz: 1 };
6
+
7
+ return <Button onClick={onClick} />;
8
+}
9
+
10
+export const FIXTURE_ENTRYPOINT = {
11
+ fn: hoisting,
12
+ params: [],
13
+ isComponent: false,
14
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-nested-const-declaration-2.expect.md
new
+64
@@ -0,0 +1,64 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function hoisting(cond) {
6
+ let items = [];
7
+ if (cond) {
8
+ const foo = () => {
9
+ items.push(bar());
10
+ };
11
+ const bar = () => true;
12
+ foo();
13
+ }
14
+ return items;
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: hoisting,
19
+ params: [true],
20
+ isComponent: false,
21
+};
22
+
23
+```
24
+
25
+## Code
26
+
27
+```javascript
28
+import { unstable_useMemoCache as useMemoCache } from "react";
29
+function hoisting(cond) {
30
+ const $ = useMemoCache(3);
31
+ const c_0 = $[0] !== cond;
32
+ let items;
33
+ if (c_0) {
34
+ items = [];
35
+ if (cond) {
36
+ const foo = () => {
37
+ items.push(bar());
38
+ };
39
+ let t0;
40
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
41
+ t0 = () => true;
42
+ $[2] = t0;
43
+ } else {
44
+ t0 = $[2];
45
+ }
46
+ const bar = t0;
47
+ foo();
48
+ }
49
+ $[0] = cond;
50
+ $[1] = items;
51
+ } else {
52
+ items = $[1];
53
+ }
54
+ return items;
55
+}
56
+
57
+export const FIXTURE_ENTRYPOINT = {
58
+ fn: hoisting,
59
+ params: [true],
60
+ isComponent: false,
61
+};
62
+
63
+```
64
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-nested-const-declaration-2.js
new
+17
@@ -0,0 +1,17 @@
1
+function hoisting(cond) {
2
+ let items = [];
3
+ if (cond) {
4
+ const foo = () => {
5
+ items.push(bar());
6
+ };
7
+ const bar = () => true;
8
+ foo();
9
+ }
10
+ return items;
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: hoisting,
15
+ params: [true],
16
+ isComponent: false,
17
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-nested-const-declaration.expect.md
new
+61
@@ -0,0 +1,61 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function hoisting() {
6
+ const qux = () => {
7
+ let result;
8
+ {
9
+ result = foo();
10
+ }
11
+ return result;
12
+ };
13
+ const foo = () => {
14
+ return bar + baz;
15
+ };
16
+ const bar = 3;
17
+ const baz = 2;
18
+ return qux(); // OK: called outside of TDZ
19
+}
20
+
21
+export const FIXTURE_ENTRYPOINT = {
22
+ fn: hoisting,
23
+ params: [],
24
+ isComponent: false,
25
+};
26
+
27
+```
28
+
29
+## Code
30
+
31
+```javascript
32
+import { unstable_useMemoCache as useMemoCache } from "react";
33
+function hoisting() {
34
+ const $ = useMemoCache(1);
35
+ let t0;
36
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
37
+ const qux = () => {
38
+ const result = foo();
39
+ return result;
40
+ };
41
+
42
+ const foo = () => bar + baz;
43
+
44
+ const bar = 3;
45
+ const baz = 2;
46
+ t0 = qux();
47
+ $[0] = t0;
48
+ } else {
49
+ t0 = $[0];
50
+ }
51
+ return t0;
52
+}
53
+
54
+export const FIXTURE_ENTRYPOINT = {
55
+ fn: hoisting,
56
+ params: [],
57
+ isComponent: false,
58
+};
59
+
60
+```
61
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-nested-const-declaration.js
new
+21
@@ -0,0 +1,21 @@
1
+function hoisting() {
2
+ const qux = () => {
3
+ let result;
4
+ {
5
+ result = foo();
6
+ }
7
+ return result;
8
+ };
9
+ const foo = () => {
10
+ return bar + baz;
11
+ };
12
+ const bar = 3;
13
+ const baz = 2;
14
+ return qux(); // OK: called outside of TDZ
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: hoisting,
19
+ params: [],
20
+ isComponent: false,
21
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-simple-const-declaration.expect.md
new
+55
@@ -0,0 +1,55 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function hoisting() {
6
+ const foo = () => {
7
+ return bar + baz;
8
+ };
9
+ const bar = 3;
10
+ const baz = 2;
11
+ return foo(); // OK: called outside of TDZ for bar/baz
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: hoisting,
16
+ params: [],
17
+ isComponent: false,
18
+};
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+import { unstable_useMemoCache as useMemoCache } from "react";
26
+function hoisting() {
27
+ const $ = useMemoCache(2);
28
+ let foo;
29
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30
+ foo = () => bar + baz;
31
+
32
+ const bar = 3;
33
+ const baz = 2;
34
+ $[0] = foo;
35
+ } else {
36
+ foo = $[0];
37
+ }
38
+ let t0;
39
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
40
+ t0 = foo();
41
+ $[1] = t0;
42
+ } else {
43
+ t0 = $[1];
44
+ }
45
+ return t0;
46
+}
47
+
48
+export const FIXTURE_ENTRYPOINT = {
49
+ fn: hoisting,
50
+ params: [],
51
+ isComponent: false,
52
+};
53
+
54
+```
55
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-simple-const-declaration.js
renamed
+1
-1
@@ -2,7 +2,7 @@ function hoisting() {
2
const foo = () => {
3
return bar + baz;
4
};
5
- let bar = 3;
5
+ const bar = 3;
6
const baz = 2;
7
return foo(); // OK: called outside of TDZ for bar/baz
8
}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/hoisting-simple-function-expression.expect.md
new
+57
@@ -0,0 +1,57 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function hoisting() {
6
+ const foo = () => {
7
+ return bar();
8
+ };
9
+ const bar = () => {
10
+ return 1;
11
+ };
12
+
13
+ return foo(); // OK: bar's value is only accessed outside of its TDZ
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: hoisting,
18
+ params: [],
19
+ isComponent: false,
20
+};
21
+
22
+```
23
+
24
+## Code
25
+
26
+```javascript
27
+import { unstable_useMemoCache as useMemoCache } from "react";
28
+function hoisting() {
29
+ const $ = useMemoCache(2);
30
+ let t1;
31
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
32
+ const foo = () => bar();
33
+ let t0;
34
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
35
+ t0 = () => 1;
36
+ $[1] = t0;
37
+ } else {
38
+ t0 = $[1];
39
+ }
40
+ const bar = t0;
41
+
42
+ t1 = foo();
43
+ $[0] = t1;
44
+ } else {
45
+ t1 = $[0];
46
+ }
47
+ return t1;
48
+}
49
+
50
+export const FIXTURE_ENTRYPOINT = {
51
+ fn: hoisting,
52
+ params: [],
53
+ isComponent: false,
54
+};
55
+
56
+```
57
+
\ No newline at end of file