main
ts 666 lines 18.3 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 {
9 HIRFunction,
10 InstructionId,
11 Place,
12 PrunedReactiveScopeBlock,
13 ReactiveBlock,
14 ReactiveFunction,
15 ReactiveInstruction,
16 ReactiveScopeBlock,
17 ReactiveStatement,
18 ReactiveTerminal,
19 ReactiveTerminalStatement,
20 ReactiveValue,
21 } from '../HIR/HIR';
22 import {
23 eachInstructionLValue,
24 eachInstructionValueOperand,
25 eachTerminalOperand,
26 } from '../HIR/visitors';
27 import {assertExhaustive} from '../Utils/utils';
28
29 export function visitReactiveFunction<TState>(
30 fn: ReactiveFunction,
31 visitor: ReactiveFunctionVisitor<TState>,
32 state: TState,
33 ): void {
34 visitor.visitBlock(fn.body, state);
35 }
36
37 export class ReactiveFunctionVisitor<TState = void> {
38 visitID(_id: InstructionId, _state: TState): void {}
39 visitParam(_place: Place, _state: TState): void {}
40 visitLValue(_id: InstructionId, _lvalue: Place, _state: TState): void {}
41 visitPlace(_id: InstructionId, _place: Place, _state: TState): void {}
42 visitReactiveFunctionValue(
43 _id: InstructionId,
44 _dependencies: Array<Place>,
45 _fn: ReactiveFunction,
46 _state: TState,
47 ): void {}
48
49 visitValue(id: InstructionId, value: ReactiveValue, state: TState): void {
50 this.traverseValue(id, value, state);
51 }
52 traverseValue(id: InstructionId, value: ReactiveValue, state: TState): void {
53 switch (value.kind) {
54 case 'OptionalExpression': {
55 this.visitValue(id, value.value, state);
56 break;
57 }
58 case 'LogicalExpression': {
59 this.visitValue(id, value.left, state);
60 this.visitValue(id, value.right, state);
61 break;
62 }
63 case 'ConditionalExpression': {
64 this.visitValue(id, value.test, state);
65 this.visitValue(id, value.consequent, state);
66 this.visitValue(id, value.alternate, state);
67 break;
68 }
69 case 'SequenceExpression': {
70 for (const instr of value.instructions) {
71 this.visitInstruction(instr, state);
72 }
73 this.visitValue(value.id, value.value, state);
74 break;
75 }
76 default: {
77 for (const place of eachInstructionValueOperand(value)) {
78 this.visitPlace(id, place, state);
79 }
80 }
81 }
82 }
83
84 visitInstruction(instruction: ReactiveInstruction, state: TState): void {
85 this.traverseInstruction(instruction, state);
86 }
87 traverseInstruction(instruction: ReactiveInstruction, state: TState): void {
88 this.visitID(instruction.id, state);
89 for (const operand of eachInstructionLValue(instruction)) {
90 this.visitLValue(instruction.id, operand, state);
91 }
92 this.visitValue(instruction.id, instruction.value, state);
93 }
94
95 visitTerminal(stmt: ReactiveTerminalStatement, state: TState): void {
96 this.traverseTerminal(stmt, state);
97 }
98 traverseTerminal(stmt: ReactiveTerminalStatement, state: TState): void {
99 const {terminal} = stmt;
100 if (terminal.id !== null) {
101 this.visitID(terminal.id, state);
102 }
103 switch (terminal.kind) {
104 case 'break':
105 case 'continue': {
106 break;
107 }
108 case 'return': {
109 this.visitPlace(terminal.id, terminal.value, state);
110 break;
111 }
112 case 'throw': {
113 this.visitPlace(terminal.id, terminal.value, state);
114 break;
115 }
116 case 'for': {
117 this.visitValue(terminal.id, terminal.init, state);
118 this.visitValue(terminal.id, terminal.test, state);
119 this.visitBlock(terminal.loop, state);
120 if (terminal.update !== null) {
121 this.visitValue(terminal.id, terminal.update, state);
122 }
123 break;
124 }
125 case 'for-of': {
126 this.visitValue(terminal.id, terminal.init, state);
127 this.visitValue(terminal.id, terminal.test, state);
128 this.visitBlock(terminal.loop, state);
129 break;
130 }
131 case 'for-in': {
132 this.visitValue(terminal.id, terminal.init, state);
133 this.visitBlock(terminal.loop, state);
134 break;
135 }
136 case 'do-while': {
137 this.visitBlock(terminal.loop, state);
138 this.visitValue(terminal.id, terminal.test, state);
139 break;
140 }
141 case 'while': {
142 this.visitValue(terminal.id, terminal.test, state);
143 this.visitBlock(terminal.loop, state);
144 break;
145 }
146 case 'if': {
147 this.visitPlace(terminal.id, terminal.test, state);
148 this.visitBlock(terminal.consequent, state);
149 if (terminal.alternate !== null) {
150 this.visitBlock(terminal.alternate, state);
151 }
152 break;
153 }
154 case 'switch': {
155 this.visitPlace(terminal.id, terminal.test, state);
156 for (const case_ of terminal.cases) {
157 if (case_.test !== null) {
158 this.visitPlace(terminal.id, case_.test, state);
159 }
160 if (case_.block !== undefined) {
161 this.visitBlock(case_.block, state);
162 }
163 }
164 break;
165 }
166 case 'label': {
167 this.visitBlock(terminal.block, state);
168 break;
169 }
170 case 'try': {
171 this.visitBlock(terminal.block, state);
172 this.visitBlock(terminal.handler, state);
173 break;
174 }
175 default: {
176 assertExhaustive(
177 terminal,
178 `Unexpected terminal kind \`${(terminal as any).kind}\``,
179 );
180 }
181 }
182 }
183
184 visitScope(scope: ReactiveScopeBlock, state: TState): void {
185 this.traverseScope(scope, state);
186 }
187 traverseScope(scope: ReactiveScopeBlock, state: TState): void {
188 this.visitBlock(scope.instructions, state);
189 }
190
191 visitPrunedScope(scopeBlock: PrunedReactiveScopeBlock, state: TState): void {
192 this.traversePrunedScope(scopeBlock, state);
193 }
194 traversePrunedScope(
195 scopeBlock: PrunedReactiveScopeBlock,
196 state: TState,
197 ): void {
198 this.visitBlock(scopeBlock.instructions, state);
199 }
200
201 visitBlock(block: ReactiveBlock, state: TState): void {
202 this.traverseBlock(block, state);
203 }
204 traverseBlock(block: ReactiveBlock, state: TState): void {
205 for (const instr of block) {
206 switch (instr.kind) {
207 case 'instruction': {
208 this.visitInstruction(instr.instruction, state);
209 break;
210 }
211 case 'scope': {
212 this.visitScope(instr, state);
213 break;
214 }
215 case 'pruned-scope': {
216 this.visitPrunedScope(instr, state);
217 break;
218 }
219 case 'terminal': {
220 this.visitTerminal(instr, state);
221 break;
222 }
223 default: {
224 assertExhaustive(
225 instr,
226 `Unexpected instruction kind \`${(instr as any).kind}\``,
227 );
228 }
229 }
230 }
231 }
232
233 visitHirFunction(fn: HIRFunction, state: TState): void {
234 for (const param of fn.params) {
235 const place = param.kind === 'Identifier' ? param : param.place;
236 this.visitParam(place, state);
237 }
238 for (const [, block] of fn.body.blocks) {
239 for (const instr of block.instructions) {
240 this.visitInstruction(instr, state);
241 if (
242 instr.value.kind === 'FunctionExpression' ||
243 instr.value.kind === 'ObjectMethod'
244 ) {
245 this.visitHirFunction(instr.value.loweredFunc.func, state);
246 }
247 }
248 for (const operand of eachTerminalOperand(block.terminal)) {
249 this.visitPlace(block.terminal.id, operand, state);
250 }
251 }
252 }
253 }
254
255 export type TransformedValue =
256 | {kind: 'keep'}
257 | {kind: 'replace'; value: ReactiveValue};
258
259 export type Transformed<T> =
260 | {kind: 'remove'}
261 | {kind: 'keep'}
262 | {kind: 'replace'; value: T}
263 | {kind: 'replace-many'; value: Array<T>};
264
265 export class ReactiveFunctionTransform<
266 TState = void,
267 > extends ReactiveFunctionVisitor<TState> {
268 override traverseBlock(block: ReactiveBlock, state: TState): void {
269 let nextBlock: ReactiveBlock | null = null;
270 for (let i = 0; i < block.length; i++) {
271 const instr = block[i]!;
272 let transformed: Transformed<ReactiveStatement>;
273 switch (instr.kind) {
274 case 'instruction': {
275 transformed = this.transformInstruction(instr.instruction, state);
276 break;
277 }
278 case 'scope': {
279 transformed = this.transformScope(instr, state);
280 break;
281 }
282 case 'pruned-scope': {
283 transformed = this.transformPrunedScope(instr, state);
284 break;
285 }
286 case 'terminal': {
287 transformed = this.transformTerminal(instr, state);
288 break;
289 }
290 default: {
291 assertExhaustive(
292 instr,
293 `Unexpected instruction kind \`${(instr as any).kind}\``,
294 );
295 }
296 }
297 switch (transformed.kind) {
298 case 'keep': {
299 if (nextBlock !== null) {
300 nextBlock.push(instr);
301 }
302 break;
303 }
304 case 'remove': {
305 if (nextBlock === null) {
306 nextBlock = block.slice(0, i);
307 }
308 break;
309 }
310 case 'replace': {
311 nextBlock ??= block.slice(0, i);
312 nextBlock.push(transformed.value);
313 break;
314 }
315 case 'replace-many': {
316 nextBlock ??= block.slice(0, i);
317 nextBlock.push(...transformed.value);
318 break;
319 }
320 }
321 }
322 if (nextBlock !== null) {
323 block.length = 0;
324 block.push(...nextBlock);
325 }
326 }
327
328 transformInstruction(
329 instruction: ReactiveInstruction,
330 state: TState,
331 ): Transformed<ReactiveStatement> {
332 this.visitInstruction(instruction, state);
333 return {kind: 'keep'};
334 }
335
336 transformTerminal(
337 stmt: ReactiveTerminalStatement,
338 state: TState,
339 ): Transformed<ReactiveStatement> {
340 this.visitTerminal(stmt, state);
341 return {kind: 'keep'};
342 }
343
344 transformScope(
345 scope: ReactiveScopeBlock,
346 state: TState,
347 ): Transformed<ReactiveStatement> {
348 this.visitScope(scope, state);
349 return {kind: 'keep'};
350 }
351
352 transformPrunedScope(
353 scope: PrunedReactiveScopeBlock,
354 state: TState,
355 ): Transformed<ReactiveStatement> {
356 this.visitPrunedScope(scope, state);
357 return {kind: 'keep'};
358 }
359
360 transformValue(
361 id: InstructionId,
362 value: ReactiveValue,
363 state: TState,
364 ): TransformedValue {
365 this.visitValue(id, value, state);
366 return {kind: 'keep'};
367 }
368
369 transformReactiveFunctionValue(
370 id: InstructionId,
371 dependencies: Array<Place>,
372 fn: ReactiveFunction,
373 state: TState,
374 ): {kind: 'keep'} | {kind: 'replace'; value: ReactiveFunction} {
375 this.visitReactiveFunctionValue(id, dependencies, fn, state);
376 return {kind: 'keep'};
377 }
378
379 override traverseValue(
380 id: InstructionId,
381 value: ReactiveValue,
382 state: TState,
383 ): void {
384 switch (value.kind) {
385 case 'OptionalExpression': {
386 const nextValue = this.transformValue(id, value.value, state);
387 if (nextValue.kind === 'replace') {
388 value.value = nextValue.value;
389 }
390 break;
391 }
392 case 'LogicalExpression': {
393 const left = this.transformValue(id, value.left, state);
394 if (left.kind === 'replace') {
395 value.left = left.value;
396 }
397 const right = this.transformValue(id, value.right, state);
398 if (right.kind === 'replace') {
399 value.right = right.value;
400 }
401 break;
402 }
403 case 'ConditionalExpression': {
404 const test = this.transformValue(id, value.test, state);
405 if (test.kind === 'replace') {
406 value.test = test.value;
407 }
408 const consequent = this.transformValue(id, value.consequent, state);
409 if (consequent.kind === 'replace') {
410 value.consequent = consequent.value;
411 }
412 const alternate = this.transformValue(id, value.alternate, state);
413 if (alternate.kind === 'replace') {
414 value.alternate = alternate.value;
415 }
416 break;
417 }
418 case 'SequenceExpression': {
419 for (const instr of value.instructions) {
420 this.visitInstruction(instr, state);
421 }
422 const nextValue = this.transformValue(value.id, value.value, state);
423 if (nextValue.kind === 'replace') {
424 value.value = nextValue.value;
425 }
426 break;
427 }
428 default: {
429 for (const place of eachInstructionValueOperand(value)) {
430 this.visitPlace(id, place, state);
431 }
432 }
433 }
434 }
435
436 override traverseInstruction(
437 instruction: ReactiveInstruction,
438 state: TState,
439 ): void {
440 this.visitID(instruction.id, state);
441 for (const operand of eachInstructionLValue(instruction)) {
442 this.visitLValue(instruction.id, operand, state);
443 }
444 const nextValue = this.transformValue(
445 instruction.id,
446 instruction.value,
447 state,
448 );
449 if (nextValue.kind === 'replace') {
450 instruction.value = nextValue.value;
451 }
452 }
453
454 override traverseTerminal(
455 stmt: ReactiveTerminalStatement,
456 state: TState,
457 ): void {
458 const {terminal} = stmt;
459 if (terminal.id !== null) {
460 this.visitID(terminal.id, state);
461 }
462 switch (terminal.kind) {
463 case 'break':
464 case 'continue': {
465 break;
466 }
467 case 'return': {
468 this.visitPlace(terminal.id, terminal.value, state);
469 break;
470 }
471 case 'throw': {
472 this.visitPlace(terminal.id, terminal.value, state);
473 break;
474 }
475 case 'for': {
476 const init = this.transformValue(terminal.id, terminal.init, state);
477 if (init.kind === 'replace') {
478 terminal.init = init.value;
479 }
480 const test = this.transformValue(terminal.id, terminal.test, state);
481 if (test.kind === 'replace') {
482 terminal.test = test.value;
483 }
484 if (terminal.update !== null) {
485 const update = this.transformValue(
486 terminal.id,
487 terminal.update,
488 state,
489 );
490 if (update.kind === 'replace') {
491 terminal.update = update.value;
492 }
493 }
494 this.visitBlock(terminal.loop, state);
495 break;
496 }
497 case 'for-of': {
498 const init = this.transformValue(terminal.id, terminal.init, state);
499 if (init.kind === 'replace') {
500 terminal.init = init.value;
501 }
502 const test = this.transformValue(terminal.id, terminal.test, state);
503 if (test.kind === 'replace') {
504 terminal.test = test.value;
505 }
506 this.visitBlock(terminal.loop, state);
507 break;
508 }
509 case 'for-in': {
510 const init = this.transformValue(terminal.id, terminal.init, state);
511 if (init.kind === 'replace') {
512 terminal.init = init.value;
513 }
514 this.visitBlock(terminal.loop, state);
515 break;
516 }
517 case 'do-while': {
518 this.visitBlock(terminal.loop, state);
519 const test = this.transformValue(terminal.id, terminal.test, state);
520 if (test.kind === 'replace') {
521 terminal.test = test.value;
522 }
523 break;
524 }
525 case 'while': {
526 const test = this.transformValue(terminal.id, terminal.test, state);
527 if (test.kind === 'replace') {
528 terminal.test = test.value;
529 }
530 this.visitBlock(terminal.loop, state);
531 break;
532 }
533 case 'if': {
534 this.visitPlace(terminal.id, terminal.test, state);
535 this.visitBlock(terminal.consequent, state);
536 if (terminal.alternate !== null) {
537 this.visitBlock(terminal.alternate, state);
538 }
539 break;
540 }
541 case 'switch': {
542 this.visitPlace(terminal.id, terminal.test, state);
543 for (const case_ of terminal.cases) {
544 if (case_.test !== null) {
545 this.visitPlace(terminal.id, case_.test, state);
546 }
547 if (case_.block !== undefined) {
548 this.visitBlock(case_.block, state);
549 }
550 }
551 break;
552 }
553 case 'label': {
554 this.visitBlock(terminal.block, state);
555 break;
556 }
557 case 'try': {
558 this.visitBlock(terminal.block, state);
559 if (terminal.handlerBinding !== null) {
560 this.visitPlace(terminal.id, terminal.handlerBinding, state);
561 }
562 this.visitBlock(terminal.handler, state);
563 break;
564 }
565 default: {
566 assertExhaustive(
567 terminal,
568 `Unexpected terminal kind \`${(terminal as any).kind}\``,
569 );
570 }
571 }
572 }
573 }
574
575 export function* eachReactiveValueOperand(
576 instrValue: ReactiveValue,
577 ): Iterable<Place> {
578 switch (instrValue.kind) {
579 case 'OptionalExpression': {
580 yield* eachReactiveValueOperand(instrValue.value);
581 break;
582 }
583 case 'LogicalExpression': {
584 yield* eachReactiveValueOperand(instrValue.left);
585 yield* eachReactiveValueOperand(instrValue.right);
586 break;
587 }
588 case 'SequenceExpression': {
589 for (const instr of instrValue.instructions) {
590 yield* eachReactiveValueOperand(instr.value);
591 }
592 yield* eachReactiveValueOperand(instrValue.value);
593 break;
594 }
595 case 'ConditionalExpression': {
596 yield* eachReactiveValueOperand(instrValue.test);
597 yield* eachReactiveValueOperand(instrValue.consequent);
598 yield* eachReactiveValueOperand(instrValue.alternate);
599 break;
600 }
601 default: {
602 yield* eachInstructionValueOperand(instrValue);
603 }
604 }
605 }
606
607 export function mapTerminalBlocks(
608 terminal: ReactiveTerminal,
609 fn: (block: ReactiveBlock) => ReactiveBlock,
610 ): void {
611 switch (terminal.kind) {
612 case 'break':
613 case 'continue':
614 case 'return':
615 case 'throw': {
616 break;
617 }
618 case 'for': {
619 terminal.loop = fn(terminal.loop);
620 break;
621 }
622 case 'for-of': {
623 terminal.loop = fn(terminal.loop);
624 break;
625 }
626 case 'for-in': {
627 terminal.loop = fn(terminal.loop);
628 break;
629 }
630 case 'do-while':
631 case 'while': {
632 terminal.loop = fn(terminal.loop);
633 break;
634 }
635 case 'if': {
636 terminal.consequent = fn(terminal.consequent);
637 if (terminal.alternate !== null) {
638 terminal.alternate = fn(terminal.alternate);
639 }
640 break;
641 }
642 case 'switch': {
643 for (const case_ of terminal.cases) {
644 if (case_.block !== undefined) {
645 case_.block = fn(case_.block);
646 }
647 }
648 break;
649 }
650 case 'label': {
651 terminal.block = fn(terminal.block);
652 break;
653 }
654 case 'try': {
655 terminal.block = fn(terminal.block);
656 terminal.handler = fn(terminal.handler);
657 break;
658 }
659 default: {
660 assertExhaustive(
661 terminal,
662 `Unexpected terminal kind \`${(terminal as any).kind}\``,
663 );
664 }
665 }
666 }