main
ts 170 lines 5.22 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {CompilerError} from '..';
9 import {
10 convertHoistedLValueKind,
11 IdentifierId,
12 InstructionId,
13 InstructionKind,
14 Place,
15 ReactiveFunction,
16 ReactiveInstruction,
17 ReactiveScopeBlock,
18 ReactiveStatement,
19 } from '../HIR';
20 import {empty, Stack} from '../Utils/Stack';
21 import {
22 ReactiveFunctionTransform,
23 Transformed,
24 visitReactiveFunction,
25 } from './visitors';
26
27 /*
28 * Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
29 * original instruction kind.
30 *
31 * Also detects and bails out on context variables which are:
32 * - function declarations, which are hoisted by JS engines to the nearest block scope
33 * - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
34 * - declared
35 *
36 * This is because React Compiler converts a `function foo()` function declaration to
37 * 1. a `let foo;` declaration before reactive memo blocks
38 * 2. a `foo = function foo() {}` assignment within the block
39 *
40 * This means references before the assignment are invalid (see fixture
41 * error.todo-functiondecl-hoisting)
42 */
43 export function pruneHoistedContexts(fn: ReactiveFunction): void {
44 visitReactiveFunction(fn, new Visitor(), {
45 activeScopes: empty(),
46 uninitialized: new Map(),
47 });
48 }
49
50 type VisitorState = {
51 activeScopes: Stack<Set<IdentifierId>>;
52 uninitialized: Map<
53 IdentifierId,
54 | {
55 kind: 'unknown-kind';
56 }
57 | {
58 kind: 'func';
59 definition: Place | null;
60 }
61 >;
62 };
63
64 class Visitor extends ReactiveFunctionTransform<VisitorState> {
65 override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void {
66 state.activeScopes = state.activeScopes.push(
67 new Set(scope.scope.declarations.keys()),
68 );
69 /**
70 * Add declared but not initialized / assigned variables. This may include
71 * function declarations that escape the memo block.
72 */
73 for (const decl of scope.scope.declarations.values()) {
74 state.uninitialized.set(decl.identifier.id, {kind: 'unknown-kind'});
75 }
76 this.traverseScope(scope, state);
77 state.activeScopes.pop();
78 for (const decl of scope.scope.declarations.values()) {
79 state.uninitialized.delete(decl.identifier.id);
80 }
81 }
82 override visitPlace(
83 _id: InstructionId,
84 place: Place,
85 state: VisitorState,
86 ): void {
87 const maybeHoistedFn = state.uninitialized.get(place.identifier.id);
88 if (
89 maybeHoistedFn?.kind === 'func' &&
90 maybeHoistedFn.definition !== place
91 ) {
92 CompilerError.throwTodo({
93 reason: '[PruneHoistedContexts] Rewrite hoisted function references',
94 loc: place.loc,
95 });
96 }
97 }
98 override transformInstruction(
99 instruction: ReactiveInstruction,
100 state: VisitorState,
101 ): Transformed<ReactiveStatement> {
102 /**
103 * Remove hoisted declarations to preserve TDZ
104 */
105 if (instruction.value.kind === 'DeclareContext') {
106 const maybeNonHoisted = convertHoistedLValueKind(
107 instruction.value.lvalue.kind,
108 );
109 if (maybeNonHoisted != null) {
110 if (
111 maybeNonHoisted === InstructionKind.Function &&
112 state.uninitialized.has(instruction.value.lvalue.place.identifier.id)
113 ) {
114 state.uninitialized.set(
115 instruction.value.lvalue.place.identifier.id,
116 {
117 kind: 'func',
118 definition: null,
119 },
120 );
121 }
122 return {kind: 'remove'};
123 }
124 }
125 if (
126 instruction.value.kind === 'StoreContext' &&
127 instruction.value.lvalue.kind !== InstructionKind.Reassign
128 ) {
129 /**
130 * Rewrite StoreContexts let/const that will be pre-declared in
131 * codegen to reassignments.
132 */
133 const lvalueId = instruction.value.lvalue.place.identifier.id;
134 const isDeclaredByScope = state.activeScopes.find(scope =>
135 scope.has(lvalueId),
136 );
137 if (isDeclaredByScope) {
138 if (
139 instruction.value.lvalue.kind === InstructionKind.Let ||
140 instruction.value.lvalue.kind === InstructionKind.Const
141 ) {
142 instruction.value.lvalue.kind = InstructionKind.Reassign;
143 } else if (instruction.value.lvalue.kind === InstructionKind.Function) {
144 const maybeHoistedFn = state.uninitialized.get(lvalueId);
145 if (maybeHoistedFn != null) {
146 CompilerError.invariant(maybeHoistedFn.kind === 'func', {
147 reason: '[PruneHoistedContexts] Unexpected hoisted function',
148 loc: instruction.loc,
149 });
150 maybeHoistedFn.definition = instruction.value.lvalue.place;
151 /**
152 * References to hoisted functions are now "safe" as variable assignments
153 * have finished.
154 */
155 state.uninitialized.delete(lvalueId);
156 }
157 } else {
158 CompilerError.throwTodo({
159 reason: '[PruneHoistedContexts] Unexpected kind',
160 description: `(${instruction.value.lvalue.kind})`,
161 loc: instruction.loc,
162 });
163 }
164 }
165 }
166
167 this.visitInstruction(instruction, state);
168 return {kind: 'keep'};
169 }
170 }