main
ts 955 lines 27.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 {Binding, NodePath} from '@babel/traverse';
9 import * as t from '@babel/types';
10 import {
11 CompilerError,
12 CompilerDiagnostic,
13 CompilerErrorDetail,
14 ErrorCategory,
15 } from '../CompilerError';
16 import {Environment} from './Environment';
17 import {
18 BasicBlock,
19 BlockId,
20 BlockKind,
21 Effect,
22 GeneratedSource,
23 GotoVariant,
24 HIR,
25 Identifier,
26 IdentifierId,
27 Instruction,
28 Place,
29 SourceLocation,
30 Terminal,
31 VariableBinding,
32 makeBlockId,
33 makeDeclarationId,
34 makeIdentifierName,
35 makeInstructionId,
36 makeTemporaryIdentifier,
37 makeType,
38 } from './HIR';
39 import {printInstruction} from './PrintHIR';
40 import {
41 eachTerminalSuccessor,
42 mapTerminalSuccessors,
43 terminalFallthrough,
44 } from './visitors';
45
46 /*
47 * *******************************************************************************************
48 * *******************************************************************************************
49 * ************************************* Lowering to HIR *************************************
50 * *******************************************************************************************
51 * *******************************************************************************************
52 */
53
54 // A work-in-progress block that does not yet have a terminator
55 export type WipBlock = {
56 id: BlockId;
57 instructions: Array<Instruction>;
58 kind: BlockKind;
59 };
60
61 type Scope = LoopScope | LabelScope | SwitchScope;
62
63 type LoopScope = {
64 kind: 'loop';
65 label: string | null;
66 continueBlock: BlockId;
67 breakBlock: BlockId;
68 };
69
70 type SwitchScope = {
71 kind: 'switch';
72 breakBlock: BlockId;
73 label: string | null;
74 };
75
76 type LabelScope = {
77 kind: 'label';
78 label: string;
79 breakBlock: BlockId;
80 };
81
82 function newBlock(id: BlockId, kind: BlockKind): WipBlock {
83 return {id, kind, instructions: []};
84 }
85
86 export type Bindings = Map<
87 string,
88 {node: t.Identifier; identifier: Identifier}
89 >;
90
91 /*
92 * Determines how instructions should be constructed in order to preserve
93 * exception semantics
94 */
95 export type ExceptionsMode =
96 /*
97 * Mode used for code not covered by explicit exception handling, any
98 * errors are assumed to be thrown out of the function
99 */
100 | {kind: 'ThrowExceptions'}
101 /*
102 * Mode used for code that *is* covered by explicit exception handling
103 * (ie try/catch), which requires modeling the possibility of control
104 * flow to the exception handler.
105 */
106 | {kind: 'CatchExceptions'; handler: BlockId};
107
108 // Helper class for constructing a CFG
109 export default class HIRBuilder {
110 #completed: Map<BlockId, BasicBlock> = new Map();
111 #current: WipBlock;
112 #entry: BlockId;
113 #scopes: Array<Scope> = [];
114 #context: Map<t.Identifier, SourceLocation>;
115 #bindings: Bindings;
116 #env: Environment;
117 #exceptionHandlerStack: Array<BlockId> = [];
118 /**
119 * Traversal context: counts the number of `fbt` tag parents
120 * of the current babel node.
121 */
122 fbtDepth: number = 0;
123
124 get nextIdentifierId(): IdentifierId {
125 return this.#env.nextIdentifierId;
126 }
127
128 get context(): Map<t.Identifier, SourceLocation> {
129 return this.#context;
130 }
131
132 get bindings(): Bindings {
133 return this.#bindings;
134 }
135
136 get environment(): Environment {
137 return this.#env;
138 }
139
140 constructor(
141 env: Environment,
142 options?: {
143 bindings?: Bindings | null;
144 context?: Map<t.Identifier, SourceLocation>;
145 entryBlockKind?: BlockKind;
146 },
147 ) {
148 this.#env = env;
149 this.#bindings = options?.bindings ?? new Map();
150 this.#context = options?.context ?? new Map();
151 this.#entry = makeBlockId(env.nextBlockId);
152 this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
153 }
154
155 recordError(error: CompilerDiagnostic | CompilerErrorDetail): void {
156 this.#env.recordError(error);
157 }
158
159 currentBlockKind(): BlockKind {
160 return this.#current.kind;
161 }
162
163 // Push a statement or expression onto the current block
164 push(instruction: Instruction): void {
165 this.#current.instructions.push(instruction);
166 const exceptionHandler = this.#exceptionHandlerStack.at(-1);
167 if (exceptionHandler !== undefined) {
168 const continuationBlock = this.reserve(this.currentBlockKind());
169 this.terminateWithContinuation(
170 {
171 kind: 'maybe-throw',
172 continuation: continuationBlock.id,
173 handler: exceptionHandler,
174 id: makeInstructionId(0),
175 loc: instruction.loc,
176 effects: null,
177 },
178 continuationBlock,
179 );
180 }
181 }
182
183 enterTryCatch(handler: BlockId, fn: () => void): void {
184 this.#exceptionHandlerStack.push(handler);
185 fn();
186 this.#exceptionHandlerStack.pop();
187 }
188
189 resolveThrowHandler(): BlockId | null {
190 const handler = this.#exceptionHandlerStack.at(-1);
191 return handler ?? null;
192 }
193
194 makeTemporary(loc: SourceLocation): Identifier {
195 const id = this.nextIdentifierId;
196 return makeTemporaryIdentifier(id, loc);
197 }
198
199 #resolveBabelBinding(
200 path: NodePath<t.Identifier | t.JSXIdentifier>,
201 ): Binding | null {
202 const originalName = path.node.name;
203 const binding = path.scope.getBinding(originalName);
204 if (binding == null) {
205 return null;
206 }
207 return binding;
208 }
209
210 /*
211 * Maps an Identifier (or JSX identifier) Babel node to an internal `Identifier`
212 * which represents the variable being referenced, according to the JS scoping rules.
213 *
214 * Because Forget does not preserve _all_ block scopes in the input (only those that
215 * happen to occur from control flow), this resolution ensures that different variables
216 * with the same name are mapped to a unique name. Concretely, this function maintains
217 * the invariant that all references to a given variable will return an `Identifier`
218 * with the same (unique for the function) `name` and `id`.
219 *
220 * Example:
221 *
222 * ```javascript
223 * function foo() {
224 * const x = 0;
225 * {
226 * const x = 1;
227 * }
228 * return x;
229 * }
230 * ```
231 *
232 * The above converts as follows:
233 *
234 * ```
235 * Const Identifier { name: 'x', id: 0 } = Primitive { value: 0 };
236 * Const Identifier { name: 'x_0', id: 1 } = Primitive { value: 1 };
237 * Return Identifier { name: 'x', id: 0};
238 * ```
239 */
240 resolveIdentifier(
241 path: NodePath<t.Identifier | t.JSXIdentifier>,
242 ): VariableBinding {
243 const originalName = path.node.name;
244 const babelBinding = this.#resolveBabelBinding(path);
245 if (babelBinding == null) {
246 return {kind: 'Global', name: originalName};
247 }
248
249 // Check if the binding is from module scope
250 const outerBinding =
251 this.#env.parentFunction.scope.parent.getBinding(originalName);
252 if (babelBinding === outerBinding) {
253 const path = babelBinding.path;
254 if (path.isImportDefaultSpecifier()) {
255 const importDeclaration =
256 path.parentPath as NodePath<t.ImportDeclaration>;
257 return {
258 kind: 'ImportDefault',
259 name: originalName,
260 module: importDeclaration.node.source.value,
261 };
262 } else if (path.isImportSpecifier()) {
263 const importDeclaration =
264 path.parentPath as NodePath<t.ImportDeclaration>;
265 return {
266 kind: 'ImportSpecifier',
267 name: originalName,
268 module: importDeclaration.node.source.value,
269 imported:
270 path.node.imported.type === 'Identifier'
271 ? path.node.imported.name
272 : path.node.imported.value,
273 };
274 } else if (path.isImportNamespaceSpecifier()) {
275 const importDeclaration =
276 path.parentPath as NodePath<t.ImportDeclaration>;
277 return {
278 kind: 'ImportNamespace',
279 name: originalName,
280 module: importDeclaration.node.source.value,
281 };
282 } else {
283 return {
284 kind: 'ModuleLocal',
285 name: originalName,
286 };
287 }
288 }
289
290 const resolvedBinding = this.resolveBinding(babelBinding.identifier);
291 if (resolvedBinding.name && resolvedBinding.name.value !== originalName) {
292 babelBinding.scope.rename(originalName, resolvedBinding.name.value);
293 }
294 return {
295 kind: 'Identifier',
296 identifier: resolvedBinding,
297 bindingKind: babelBinding.kind,
298 };
299 }
300
301 isContextIdentifier(path: NodePath<t.Identifier | t.JSXIdentifier>): boolean {
302 const binding = this.#resolveBabelBinding(path);
303 if (binding) {
304 // Check if the binding is from module scope, if so return null
305 const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
306 path.node.name,
307 );
308 if (binding === outerBinding) {
309 return false;
310 }
311 return this.#env.isContextIdentifier(binding.identifier);
312 } else {
313 return false;
314 }
315 }
316
317 resolveBinding(node: t.Identifier): Identifier {
318 if (node.name === 'fbt') {
319 this.recordError(
320 new CompilerErrorDetail({
321 category: ErrorCategory.Todo,
322 reason: 'Support local variables named `fbt`',
323 description:
324 'Local variables named `fbt` may conflict with the fbt plugin and are not yet supported',
325 loc: node.loc ?? GeneratedSource,
326 suggestions: null,
327 }),
328 );
329 }
330 if (node.name === 'this') {
331 this.recordError(
332 new CompilerErrorDetail({
333 category: ErrorCategory.UnsupportedSyntax,
334 reason: '`this` is not supported syntax',
335 description:
336 'React Compiler does not support compiling functions that use `this`',
337 loc: node.loc ?? GeneratedSource,
338 suggestions: null,
339 }),
340 );
341 }
342 const originalName = node.name;
343 let name = originalName;
344 let index = 0;
345 while (true) {
346 const mapping = this.#bindings.get(name);
347 if (mapping === undefined) {
348 const id = this.nextIdentifierId;
349 const identifier: Identifier = {
350 id,
351 declarationId: makeDeclarationId(id),
352 name: makeIdentifierName(name),
353 mutableRange: {
354 start: makeInstructionId(0),
355 end: makeInstructionId(0),
356 },
357 scope: null,
358 type: makeType(),
359 loc: node.loc ?? GeneratedSource,
360 };
361 this.#env.programContext.addNewReference(name);
362 this.#bindings.set(name, {node, identifier});
363 return identifier;
364 } else if (mapping.node === node) {
365 return mapping.identifier;
366 } else {
367 name = `${originalName}_${index++}`;
368 }
369 }
370 }
371
372 // Construct a final CFG from this context
373 build(): HIR {
374 let ir: HIR = {
375 blocks: this.#completed,
376 entry: this.#entry,
377 };
378 const rpoBlocks = getReversePostorderedBlocks(ir);
379 for (const [id, block] of ir.blocks) {
380 if (
381 !rpoBlocks.has(id) &&
382 block.instructions.some(
383 instr => instr.value.kind === 'FunctionExpression',
384 )
385 ) {
386 this.recordError(
387 new CompilerErrorDetail({
388 reason: `Support functions with unreachable code that may contain hoisted declarations`,
389 loc: block.instructions[0]?.loc ?? block.terminal.loc,
390 description: null,
391 suggestions: null,
392 category: ErrorCategory.Todo,
393 }),
394 );
395 }
396 }
397 ir.blocks = rpoBlocks;
398
399 removeUnreachableForUpdates(ir);
400 removeDeadDoWhileStatements(ir);
401 removeUnnecessaryTryCatch(ir);
402 markInstructionIds(ir);
403 markPredecessors(ir);
404
405 return ir;
406 }
407
408 // Terminate the current block w the given terminal, and start a new block
409 terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
410 const {id: blockId, kind, instructions} = this.#current;
411 this.#completed.set(blockId, {
412 kind,
413 id: blockId,
414 instructions,
415 terminal,
416 preds: new Set(),
417 phis: new Set(),
418 });
419 if (nextBlockKind) {
420 const nextId = this.#env.nextBlockId;
421 this.#current = newBlock(nextId, nextBlockKind);
422 }
423 return blockId;
424 }
425
426 /*
427 * Terminate the current block w the given terminal, and set the previously
428 * reserved block as the new current block
429 */
430 terminateWithContinuation(terminal: Terminal, continuation: WipBlock): void {
431 const {id: blockId, kind, instructions} = this.#current;
432 this.#completed.set(blockId, {
433 kind: kind,
434 id: blockId,
435 instructions,
436 terminal: terminal,
437 preds: new Set(),
438 phis: new Set(),
439 });
440 this.#current = continuation;
441 }
442
443 /*
444 * Reserve a block so that it can be referenced prior to construction.
445 * Make this the current block with `terminateWithContinuation()` or
446 * call `complete()` to save it without setting it as the current block.
447 */
448 reserve(kind: BlockKind): WipBlock {
449 return newBlock(makeBlockId(this.#env.nextBlockId), kind);
450 }
451
452 // Save a previously reserved block as completed
453 complete(block: WipBlock, terminal: Terminal): void {
454 const {id: blockId, kind, instructions} = block;
455 this.#completed.set(blockId, {
456 kind,
457 id: blockId,
458 instructions,
459 terminal,
460 preds: new Set(),
461 phis: new Set(),
462 });
463 }
464
465 /*
466 * Sets the given wip block as the current block, executes the provided callback to populate the block
467 * up to its terminal, and then resets the previous actively block.
468 */
469 enterReserved(wip: WipBlock, fn: () => Terminal): void {
470 const current = this.#current;
471 this.#current = wip;
472 const terminal = fn();
473 const {id: blockId, kind, instructions} = this.#current;
474 this.#completed.set(blockId, {
475 kind,
476 id: blockId,
477 instructions,
478 terminal,
479 preds: new Set(),
480 phis: new Set(),
481 });
482 this.#current = current;
483 }
484
485 /*
486 * Create a new block and execute the provided callback with the new block
487 * set as the current, resetting to the previously active block upon exit.
488 * The lambda must return a terminal node, which is used to terminate the
489 * newly constructed block.
490 */
491 enter(nextBlockKind: BlockKind, fn: (blockId: BlockId) => Terminal): BlockId {
492 const wip = this.reserve(nextBlockKind);
493 this.enterReserved(wip, () => {
494 return fn(wip.id);
495 });
496 return wip.id;
497 }
498
499 label<T>(label: string, breakBlock: BlockId, fn: () => T): T {
500 this.#scopes.push({
501 kind: 'label',
502 breakBlock,
503 label,
504 });
505 const value = fn();
506 const last = this.#scopes.pop();
507 CompilerError.invariant(
508 last != null &&
509 last.kind === 'label' &&
510 last.label === label &&
511 last.breakBlock === breakBlock,
512 {
513 reason: 'Mismatched label',
514 loc: GeneratedSource,
515 },
516 );
517 return value;
518 }
519
520 switch<T>(label: string | null, breakBlock: BlockId, fn: () => T): T {
521 this.#scopes.push({
522 kind: 'switch',
523 breakBlock,
524 label,
525 });
526 const value = fn();
527 const last = this.#scopes.pop();
528 CompilerError.invariant(
529 last != null &&
530 last.kind === 'switch' &&
531 last.label === label &&
532 last.breakBlock === breakBlock,
533 {
534 reason: 'Mismatched label',
535 loc: GeneratedSource,
536 },
537 );
538 return value;
539 }
540
541 /*
542 * Executes the provided lambda inside a scope in which the provided loop
543 * information is cached for lookup with `lookupBreak()` and `lookupContinue()`
544 */
545 loop<T>(
546 label: string | null,
547 // block of the loop body. "continue" jumps here.
548 continueBlock: BlockId,
549 // block following the loop. "break" jumps here.
550 breakBlock: BlockId,
551 fn: () => T,
552 ): T {
553 this.#scopes.push({
554 kind: 'loop',
555 label,
556 continueBlock,
557 breakBlock,
558 });
559 const value = fn();
560 const last = this.#scopes.pop();
561 CompilerError.invariant(
562 last != null &&
563 last.kind === 'loop' &&
564 last.label === label &&
565 last.continueBlock === continueBlock &&
566 last.breakBlock === breakBlock,
567 {
568 reason: 'Mismatched loops',
569 loc: GeneratedSource,
570 },
571 );
572 return value;
573 }
574
575 /*
576 * Lookup the block target for a break statement, based on loops and switch statements
577 * in scope. Throws if there is no available location to break.
578 */
579 lookupBreak(label: string | null): BlockId {
580 for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {
581 const scope = this.#scopes[ii];
582 if (
583 (label === null &&
584 (scope.kind === 'loop' || scope.kind === 'switch')) ||
585 label === scope.label
586 ) {
587 return scope.breakBlock;
588 }
589 }
590 CompilerError.invariant(false, {
591 reason: 'Expected a loop or switch to be in scope',
592 loc: GeneratedSource,
593 });
594 }
595
596 /*
597 * Lookup the block target for a continue statement, based on loops
598 * in scope. Throws if there is no available location to continue, or if the given
599 * label does not correspond to a loop (this should also be validated at parse time).
600 */
601 lookupContinue(label: string | null): BlockId {
602 for (let ii = this.#scopes.length - 1; ii >= 0; ii--) {
603 const scope = this.#scopes[ii];
604 if (scope.kind === 'loop') {
605 if (label === null || label === scope.label) {
606 return scope.continueBlock;
607 }
608 } else if (label !== null && scope.label === label) {
609 CompilerError.invariant(false, {
610 reason: 'Continue may only refer to a labeled loop',
611 loc: GeneratedSource,
612 });
613 }
614 }
615 CompilerError.invariant(false, {
616 reason: 'Expected a loop to be in scope',
617 loc: GeneratedSource,
618 });
619 }
620 }
621
622 // Helper to shrink a CFG eliminate jump-only blocks.
623 function _shrink(func: HIR): void {
624 const gotos = new Map();
625 /*
626 * Given a target block for some terminator, resolves the ideal block that should be
627 * targeted instead. This transitively resolves any blocks that are simple indirections
628 * (empty blocks that terminate in a goto).
629 */
630 function resolveBlockTarget(blockId: BlockId): BlockId {
631 let target = gotos.get(blockId) ?? null;
632 if (target !== null) {
633 return target;
634 }
635 const block = func.blocks.get(blockId);
636 CompilerError.invariant(block != null, {
637 reason: `expected block ${blockId} to exist`,
638 loc: GeneratedSource,
639 });
640 target = getTargetIfIndirection(block);
641 if (target !== null) {
642 // the target might also be a simple goto, recurse
643 target = resolveBlockTarget(target) ?? target;
644 gotos.set(blockId, target);
645 return target;
646 } else {
647 // If the block wasn't an indirection, return the original input.
648 return blockId;
649 }
650 }
651
652 const queue = [func.entry];
653 const reachable = new Set<BlockId>();
654 while (queue.length !== 0) {
655 const blockId = queue.shift()!;
656 if (reachable.has(blockId)) {
657 continue;
658 }
659 reachable.add(blockId);
660 const block = func.blocks.get(blockId)!;
661 block.terminal = mapTerminalSuccessors(block.terminal, prevTarget => {
662 const target = resolveBlockTarget(prevTarget);
663 queue.push(target);
664 return target;
665 });
666 }
667 for (const [blockId] of func.blocks) {
668 if (!reachable.has(blockId)) {
669 func.blocks.delete(blockId);
670 }
671 }
672 }
673
674 export function removeUnreachableForUpdates(fn: HIR): void {
675 for (const [, block] of fn.blocks) {
676 if (
677 block.terminal.kind === 'for' &&
678 block.terminal.update !== null &&
679 !fn.blocks.has(block.terminal.update)
680 ) {
681 block.terminal.update = null;
682 }
683 }
684 }
685
686 export function removeDeadDoWhileStatements(func: HIR): void {
687 const visited: Set<BlockId> = new Set();
688 for (const [_, block] of func.blocks) {
689 visited.add(block.id);
690 }
691
692 /*
693 * If the test condition of a DoWhile is unreachable, the terminal is effectively deadcode and we
694 * can just inline the loop body. We replace the terminal with a goto to the loop block and
695 * MergeConsecutiveBlocks figures out how to merge as appropriate.
696 */
697 for (const [_, block] of func.blocks) {
698 if (block.terminal.kind === 'do-while') {
699 if (!visited.has(block.terminal.test)) {
700 block.terminal = {
701 kind: 'goto',
702 block: block.terminal.loop,
703 variant: GotoVariant.Break,
704 id: block.terminal.id,
705 loc: block.terminal.loc,
706 };
707 }
708 }
709 }
710 }
711
712 /*
713 * Converts the graph to reverse-postorder, with predecessor blocks appearing
714 * before successors except in the case of back edges (ie loops).
715 */
716 export function reversePostorderBlocks(func: HIR): void {
717 const rpoBlocks = getReversePostorderedBlocks(func);
718 func.blocks = rpoBlocks;
719 }
720
721 /**
722 * Returns a mapping of BlockId => BasicBlock where the insertion order of the map
723 * has blocks in reverse-postorder, with predecessor blocks appearing before successors
724 * except in the case of back edges (ie loops). Note that not all blocks in the input
725 * may be in the output: blocks will be removed in the case of unreachable code in
726 * the input.
727 */
728 function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
729 const visited: Set<BlockId> = new Set();
730 const used: Set<BlockId> = new Set();
731 const usedFallthroughs: Set<BlockId> = new Set();
732 const postorder: Array<BlockId> = [];
733 function visit(blockId: BlockId, isUsed: boolean): void {
734 const wasUsed = used.has(blockId);
735 const wasVisited = visited.has(blockId);
736 visited.add(blockId);
737 if (isUsed) {
738 used.add(blockId);
739 }
740 if (wasVisited && (wasUsed || !isUsed)) {
741 return;
742 }
743
744 /*
745 * Note that we visit successors in reverse order. This ensures that when we
746 * reverse the list at the end, that "sibling" edges appear in-order. For example,
747 * ```
748 * // bb0
749 * let x;
750 * if (c) {
751 * // bb1
752 * x = 1;
753 * } else {
754 * // bb2
755 * x = 2;
756 * }
757 * // bb3
758 * x;
759 * ```
760 *
761 * We want the output to be bb0, bb1, bb2, bb3 just to line up with the original
762 * program order for visual debugging. By visiting the successors in reverse order
763 * (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
764 */
765 const block = func.blocks.get(blockId)!;
766 CompilerError.invariant(block != null, {
767 reason: '[HIRBuilder] Unexpected null block',
768 description: `expected block ${blockId} to exist`,
769 loc: GeneratedSource,
770 });
771 const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
772 const fallthrough = terminalFallthrough(block.terminal);
773
774 /**
775 * Fallthrough blocks are only used to record original program block structure. If the
776 * fallthrough is actually reachable, it will be reached through terminal successors.
777 * To retain program structure, we visit fallthrough blocks first (marking them as not
778 * actually used yet) to ensure their block IDs emitted in the correct order.
779 */
780 if (fallthrough != null) {
781 if (isUsed) {
782 usedFallthroughs.add(fallthrough);
783 }
784 visit(fallthrough, false);
785 }
786 for (const successor of successors) {
787 visit(successor, isUsed);
788 }
789
790 if (!wasVisited) {
791 postorder.push(blockId);
792 }
793 }
794 visit(func.entry, true);
795 const blocks = new Map<BlockId, BasicBlock>();
796 for (const blockId of postorder.reverse()) {
797 const block = func.blocks.get(blockId)!;
798 if (used.has(blockId)) {
799 blocks.set(blockId, func.blocks.get(blockId)!);
800 } else if (usedFallthroughs.has(blockId)) {
801 blocks.set(blockId, {
802 ...block,
803 instructions: [],
804 terminal: {
805 kind: 'unreachable',
806 id: block.terminal.id,
807 loc: block.terminal.loc,
808 },
809 });
810 }
811 // otherwise this block is unreachable
812 }
813
814 return blocks;
815 }
816
817 export function markInstructionIds(func: HIR): void {
818 let id = 0;
819 const visited = new Set<Instruction>();
820 for (const [_, block] of func.blocks) {
821 for (const instr of block.instructions) {
822 CompilerError.invariant(!visited.has(instr), {
823 reason: `${printInstruction(instr)} already visited!`,
824 loc: instr.loc,
825 });
826 visited.add(instr);
827 instr.id = makeInstructionId(++id);
828 }
829 block.terminal.id = makeInstructionId(++id);
830 }
831 }
832
833 export function markPredecessors(func: HIR): void {
834 for (const [, block] of func.blocks) {
835 block.preds.clear();
836 }
837 const visited: Set<BlockId> = new Set();
838 function visit(blockId: BlockId, prevBlock: BasicBlock | null): void {
839 const block = func.blocks.get(blockId)!;
840 if (block == null) {
841 return;
842 }
843 CompilerError.invariant(block != null, {
844 reason: 'unexpected missing block',
845 description: `block ${blockId}`,
846 loc: GeneratedSource,
847 });
848 if (prevBlock) {
849 block.preds.add(prevBlock.id);
850 }
851
852 if (visited.has(blockId)) {
853 return;
854 }
855 visited.add(blockId);
856
857 const {terminal} = block;
858
859 for (const successor of eachTerminalSuccessor(terminal)) {
860 visit(successor, block);
861 }
862 }
863 visit(func.entry, null);
864 }
865
866 /*
867 * If the given block is a simple indirection — empty terminated with a goto(break) —
868 * returns the block being pointed to. Otherwise returns null.
869 */
870 function getTargetIfIndirection(block: BasicBlock): number | null {
871 return block.instructions.length === 0 &&
872 block.terminal.kind === 'goto' &&
873 block.terminal.variant === GotoVariant.Break
874 ? block.terminal.block
875 : null;
876 }
877
878 /*
879 * Finds try terminals where the handler is unreachable, and converts the try
880 * to a goto(terminal.block)
881 */
882 export function removeUnnecessaryTryCatch(fn: HIR): void {
883 for (const [, block] of fn.blocks) {
884 if (
885 block.terminal.kind === 'try' &&
886 !fn.blocks.has(block.terminal.handler)
887 ) {
888 const handlerId = block.terminal.handler;
889 const fallthroughId = block.terminal.fallthrough;
890 const fallthrough = fn.blocks.get(fallthroughId);
891 block.terminal = {
892 kind: 'goto',
893 block: block.terminal.block,
894 id: makeInstructionId(0),
895 loc: block.terminal.loc,
896 variant: GotoVariant.Break,
897 };
898
899 if (fallthrough != null) {
900 if (fallthrough.preds.size === 1 && fallthrough.preds.has(handlerId)) {
901 // delete fallthrough
902 fn.blocks.delete(fallthroughId);
903 } else {
904 fallthrough.preds.delete(handlerId);
905 }
906 }
907 }
908 }
909 }
910
911 export function createTemporaryPlace(
912 env: Environment,
913 loc: SourceLocation,
914 ): Place {
915 return {
916 kind: 'Identifier',
917 identifier: makeTemporaryIdentifier(env.nextIdentifierId, loc),
918 reactive: false,
919 effect: Effect.Unknown,
920 loc: GeneratedSource,
921 };
922 }
923
924 /**
925 * Clones an existing Place, returning a new temporary Place that shares the
926 * same metadata properties as the original place (effect, reactive flag, type)
927 * but has a new, temporary Identifier.
928 */
929 export function clonePlaceToTemporary(env: Environment, place: Place): Place {
930 const temp = createTemporaryPlace(env, place.loc);
931 temp.effect = place.effect;
932 temp.identifier.type = place.identifier.type;
933 temp.reactive = place.reactive;
934 return temp;
935 }
936
937 /**
938 * Fix scope and identifier ranges to account for renumbered instructions
939 */
940 export function fixScopeAndIdentifierRanges(func: HIR): void {
941 for (const [, block] of func.blocks) {
942 const terminal = block.terminal;
943 if (terminal.kind === 'scope' || terminal.kind === 'pruned-scope') {
944 /*
945 * Scope ranges should always align to start at the 'scope' terminal
946 * and end at the first instruction of the fallthrough block
947 */
948 const fallthroughBlock = func.blocks.get(terminal.fallthrough)!;
949 const firstId =
950 fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id;
951 terminal.scope.range.start = terminal.id;
952 terminal.scope.range.end = firstId;
953 }
954 }
955 }