main
ts 464 lines 14.6 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 '../CompilerError';
9 import {GeneratedSource} from '../HIR';
10 import {
11 DeclarationId,
12 Identifier,
13 InstructionId,
14 Place,
15 PrunedReactiveScopeBlock,
16 ReactiveFunction,
17 ReactiveScope,
18 ReactiveInstruction,
19 ReactiveScopeBlock,
20 ReactiveValue,
21 ScopeId,
22 SpreadPattern,
23 promoteTemporary,
24 promoteTemporaryJsxTag,
25 IdentifierId,
26 } from '../HIR/HIR';
27 import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
28 import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
29
30 /**
31 * Phase 2: Promote identifiers which are used in a place that requires a named variable.
32 */
33 class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
34 override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
35 for (const dep of scopeBlock.scope.dependencies) {
36 const {identifier} = dep;
37 if (identifier.name == null) {
38 promoteIdentifier(identifier, state);
39 }
40 }
41 /*
42 * This is technically optional. We could prune ReactiveScopes
43 * whose outputs are not used in another computation or return
44 * value.
45 * Many of our current test fixtures do not return a value, so
46 * it is better for now to promote (and memoize) every output.
47 */
48 for (const [, declaration] of scopeBlock.scope.declarations) {
49 if (declaration.identifier.name == null) {
50 promoteIdentifier(declaration.identifier, state);
51 }
52 }
53 this.traverseScope(scopeBlock, state);
54 }
55
56 override visitPrunedScope(
57 scopeBlock: PrunedReactiveScopeBlock,
58 state: State,
59 ): void {
60 for (const [, declaration] of scopeBlock.scope.declarations) {
61 if (
62 declaration.identifier.name == null &&
63 state.pruned.get(declaration.identifier.declarationId)
64 ?.usedOutsideScope === true
65 ) {
66 promoteIdentifier(declaration.identifier, state);
67 }
68 }
69 this.traversePrunedScope(scopeBlock, state);
70 }
71
72 override visitParam(place: Place, state: State): void {
73 if (place.identifier.name === null) {
74 promoteIdentifier(place.identifier, state);
75 }
76 }
77
78 override visitValue(
79 id: InstructionId,
80 value: ReactiveValue,
81 state: State,
82 ): void {
83 this.traverseValue(id, value, state);
84 if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') {
85 this.visitHirFunction(value.loweredFunc.func, state);
86 }
87 }
88
89 override visitReactiveFunctionValue(
90 _id: InstructionId,
91 _dependencies: Array<Place>,
92 fn: ReactiveFunction,
93 state: State,
94 ): void {
95 for (const operand of fn.params) {
96 const place = operand.kind === 'Identifier' ? operand : operand.place;
97 if (place.identifier.name === null) {
98 promoteIdentifier(place.identifier, state);
99 }
100 }
101 visitReactiveFunction(fn, this, state);
102 }
103 }
104
105 /**
106 * Phase 3: Now that identifiers which need promotion are promoted, find and promote
107 * all other Identifier instances of each promoted DeclarationId.
108 */
109 class PromoteAllInstancedOfPromotedTemporaries extends ReactiveFunctionVisitor<State> {
110 override visitPlace(_id: InstructionId, place: Place, state: State): void {
111 if (
112 place.identifier.name === null &&
113 state.promoted.has(place.identifier.declarationId)
114 ) {
115 promoteIdentifier(place.identifier, state);
116 }
117 }
118 override visitLValue(
119 _id: InstructionId,
120 _lvalue: Place,
121 _state: State,
122 ): void {
123 this.visitPlace(_id, _lvalue, _state);
124 }
125 traverseScopeIdentifiers(scope: ReactiveScope, state: State): void {
126 for (const [, decl] of scope.declarations) {
127 if (
128 decl.identifier.name === null &&
129 state.promoted.has(decl.identifier.declarationId)
130 ) {
131 promoteIdentifier(decl.identifier, state);
132 }
133 }
134 for (const dep of scope.dependencies) {
135 if (
136 dep.identifier.name === null &&
137 state.promoted.has(dep.identifier.declarationId)
138 ) {
139 promoteIdentifier(dep.identifier, state);
140 }
141 }
142 for (const reassignment of scope.reassignments) {
143 if (
144 reassignment.name === null &&
145 state.promoted.has(reassignment.declarationId)
146 ) {
147 promoteIdentifier(reassignment, state);
148 }
149 }
150 }
151 override visitScope(scope: ReactiveScopeBlock, state: State): void {
152 this.traverseScope(scope, state);
153 this.traverseScopeIdentifiers(scope.scope, state);
154 }
155 override visitPrunedScope(
156 scopeBlock: PrunedReactiveScopeBlock,
157 state: State,
158 ): void {
159 this.traversePrunedScope(scopeBlock, state);
160 this.traverseScopeIdentifiers(scopeBlock.scope, state);
161 }
162 override visitReactiveFunctionValue(
163 _id: InstructionId,
164 _dependencies: Array<Place>,
165 fn: ReactiveFunction,
166 state: State,
167 ): void {
168 visitReactiveFunction(fn, this, state);
169 }
170 }
171
172 type JsxExpressionTags = Set<DeclarationId>;
173 type State = {
174 tags: JsxExpressionTags;
175 promoted: Set<DeclarationId>;
176 pruned: Map<
177 DeclarationId,
178 {activeScopes: Array<ScopeId>; usedOutsideScope: boolean}
179 >; // true if referenced within another scope, false if only accessed outside of scopes
180 };
181
182 /**
183 * Phase 1: checks for pruned variables which need to be promoted, as well as
184 * usage of identifiers as jsx tags, which need to be promoted differently
185 */
186 class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
187 activeScopes: Array<ScopeId> = [];
188
189 override visitPlace(_id: InstructionId, place: Place, state: State): void {
190 if (
191 this.activeScopes.length !== 0 &&
192 state.pruned.has(place.identifier.declarationId)
193 ) {
194 const prunedPlace = state.pruned.get(place.identifier.declarationId)!;
195 if (prunedPlace.activeScopes.indexOf(this.activeScopes.at(-1)!) === -1) {
196 prunedPlace.usedOutsideScope = true;
197 }
198 }
199 }
200
201 override visitValue(
202 id: InstructionId,
203 value: ReactiveValue,
204 state: State,
205 ): void {
206 this.traverseValue(id, value, state);
207 if (value.kind === 'JsxExpression' && value.tag.kind === 'Identifier') {
208 state.tags.add(value.tag.identifier.declarationId);
209 }
210 }
211
212 override visitPrunedScope(
213 scopeBlock: PrunedReactiveScopeBlock,
214 state: State,
215 ): void {
216 for (const [_id, decl] of scopeBlock.scope.declarations) {
217 state.pruned.set(decl.identifier.declarationId, {
218 activeScopes: [...this.activeScopes],
219 usedOutsideScope: false,
220 });
221 }
222 this.visitBlock(scopeBlock.instructions, state);
223 }
224
225 override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
226 this.activeScopes.push(scopeBlock.scope.id);
227 this.traverseScope(scopeBlock, state);
228 this.activeScopes.pop();
229 }
230 }
231
232 type InterState = Map<IdentifierId, [Identifier, boolean]>;
233 class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
234 #promotable: State;
235 #consts: Set<IdentifierId> = new Set();
236 #globals: Set<IdentifierId> = new Set();
237
238 /*
239 * Unpromoted temporaries will be emitted at their use sites rather than as separate
240 * declarations. However, this causes errors if an interposing temporary has been
241 * promoted, or if an interposing instruction has had its lvalues deleted, because such
242 * temporaries will be emitted as separate statements, which can effectively cause
243 * code to be reordered, and when that code has side effects that changes program behavior.
244 * This visitor promotes temporarties that have such interposing instructions to preserve
245 * source ordering.
246 */
247 constructor(promotable: State, params: Array<Place | SpreadPattern>) {
248 super();
249 params.forEach(param => {
250 switch (param.kind) {
251 case 'Identifier':
252 this.#consts.add(param.identifier.id);
253 break;
254 case 'Spread':
255 this.#consts.add(param.place.identifier.id);
256 break;
257 }
258 });
259 this.#promotable = promotable;
260 }
261
262 override visitPlace(
263 _id: InstructionId,
264 place: Place,
265 state: InterState,
266 ): void {
267 const promo = state.get(place.identifier.id);
268 if (promo) {
269 const [identifier, needsPromotion] = promo;
270 if (
271 needsPromotion &&
272 identifier.name === null &&
273 !this.#consts.has(identifier.id)
274 ) {
275 /*
276 * If the identifier hasn't been promoted but is marked as needing
277 * promotion by the logic in `visitInstruction`, and we've seen a
278 * use of it after said marking, promote it
279 */
280 promoteIdentifier(identifier, this.#promotable);
281 }
282 }
283 }
284
285 override visitInstruction(
286 instruction: ReactiveInstruction,
287 state: InterState,
288 ): void {
289 for (const lval of eachInstructionValueLValue(instruction.value)) {
290 CompilerError.invariant(lval.identifier.name != null, {
291 reason:
292 'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
293 loc: instruction.loc,
294 });
295 }
296
297 switch (instruction.value.kind) {
298 case 'CallExpression':
299 case 'MethodCall':
300 case 'Await':
301 case 'PropertyStore':
302 case 'PropertyDelete':
303 case 'ComputedStore':
304 case 'ComputedDelete':
305 case 'PostfixUpdate':
306 case 'PrefixUpdate':
307 case 'StoreLocal':
308 case 'StoreContext':
309 case 'StoreGlobal':
310 case 'Destructure': {
311 let constStore = false;
312
313 if (
314 (instruction.value.kind === 'StoreContext' ||
315 instruction.value.kind === 'StoreLocal') &&
316 (instruction.value.lvalue.kind === 'Const' ||
317 instruction.value.lvalue.kind === 'HoistedConst')
318 ) {
319 /*
320 * If an identifier is const, we don't need to worry about it
321 * being mutated between being loaded and being used
322 */
323 this.#consts.add(instruction.value.lvalue.place.identifier.id);
324 constStore = true;
325 }
326 if (
327 instruction.value.kind === 'Destructure' &&
328 (instruction.value.lvalue.kind === 'Const' ||
329 instruction.value.lvalue.kind === 'HoistedConst')
330 ) {
331 [...eachPatternOperand(instruction.value.lvalue.pattern)].forEach(
332 ident => this.#consts.add(ident.identifier.id),
333 );
334 constStore = true;
335 }
336 if (instruction.value.kind === 'MethodCall') {
337 // Treat property of method call as constlike so we don't promote it.
338 this.#consts.add(instruction.value.property.identifier.id);
339 }
340
341 super.visitInstruction(instruction, state);
342 if (
343 !constStore &&
344 (instruction.lvalue == null ||
345 instruction.lvalue.identifier.name != null)
346 ) {
347 /*
348 * If we've stripped the lvalue or promoted the lvalue, then we will emit this instruction
349 * as a statement in codegen.
350 *
351 * If this instruction will be emitted directly as a statement rather than as a temporary
352 * during codegen, then it can interpose between the defs and the uses of other temporaries.
353 * Since this instruction could potentially mutate those defs, it's not safe to relocate
354 * the definition of those temporaries to after this instruction. Mark all those temporaries
355 * as needing promotion, but don't promote them until we actually see them being used.
356 */
357 for (const [key, [ident, _]] of state.entries()) {
358 state.set(key, [ident, true]);
359 }
360 }
361 if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
362 // Add this instruction's lvalue to the state, initially not marked as needing promotion
363 state.set(instruction.lvalue.identifier.id, [
364 instruction.lvalue.identifier,
365 false,
366 ]);
367 }
368 break;
369 }
370 case 'DeclareContext':
371 case 'DeclareLocal': {
372 if (
373 instruction.value.lvalue.kind === 'Const' ||
374 instruction.value.lvalue.kind === 'HoistedConst'
375 ) {
376 this.#consts.add(instruction.value.lvalue.place.identifier.id);
377 }
378 super.visitInstruction(instruction, state);
379 break;
380 }
381 case 'LoadContext':
382 case 'LoadLocal': {
383 if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
384 if (this.#consts.has(instruction.value.place.identifier.id)) {
385 this.#consts.add(instruction.lvalue.identifier.id);
386 }
387 state.set(instruction.lvalue.identifier.id, [
388 instruction.lvalue.identifier,
389 false,
390 ]);
391 }
392 super.visitInstruction(instruction, state);
393 break;
394 }
395 case 'PropertyLoad':
396 case 'ComputedLoad': {
397 if (instruction.lvalue) {
398 if (this.#globals.has(instruction.value.object.identifier.id)) {
399 this.#globals.add(instruction.lvalue.identifier.id);
400 this.#consts.add(instruction.lvalue.identifier.id);
401 }
402 if (instruction.lvalue.identifier.name === null) {
403 state.set(instruction.lvalue.identifier.id, [
404 instruction.lvalue.identifier,
405 false,
406 ]);
407 }
408 }
409 super.visitInstruction(instruction, state);
410 break;
411 }
412 case 'LoadGlobal': {
413 instruction.lvalue &&
414 this.#globals.add(instruction.lvalue.identifier.id);
415 super.visitInstruction(instruction, state);
416 break;
417 }
418 default: {
419 super.visitInstruction(instruction, state);
420 }
421 }
422 }
423 }
424
425 export function promoteUsedTemporaries(fn: ReactiveFunction): void {
426 const state: State = {
427 tags: new Set(),
428 promoted: new Set(),
429 pruned: new Map(),
430 };
431 visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
432 for (const operand of fn.params) {
433 const place = operand.kind === 'Identifier' ? operand : operand.place;
434 if (place.identifier.name === null) {
435 promoteIdentifier(place.identifier, state);
436 }
437 }
438 visitReactiveFunction(fn, new PromoteTemporaries(), state);
439
440 visitReactiveFunction(
441 fn,
442 new PromoteInterposedTemporaries(state, fn.params),
443 new Map(),
444 );
445 visitReactiveFunction(
446 fn,
447 new PromoteAllInstancedOfPromotedTemporaries(),
448 state,
449 );
450 }
451
452 function promoteIdentifier(identifier: Identifier, state: State): void {
453 CompilerError.invariant(identifier.name === null, {
454 reason:
455 'promoteTemporary: Expected to be called only for temporary variables',
456 loc: GeneratedSource,
457 });
458 if (state.tags.has(identifier.declarationId)) {
459 promoteTemporaryJsxTag(identifier);
460 } else {
461 promoteTemporary(identifier);
462 }
463 state.promoted.add(identifier.declarationId);
464 }