| 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 | GeneratedSource, |
| 11 | HIRFunction, |
| 12 | Identifier, |
| 13 | ReactiveScope, |
| 14 | makeInstructionId, |
| 15 | } from '../HIR'; |
| 16 | import {eachInstructionValueOperand} from '../HIR/visitors'; |
| 17 | import DisjointSet from '../Utils/DisjointSet'; |
| 18 | |
| 19 | /** |
| 20 | * Align scopes of object method values to that of their enclosing object expressions. |
| 21 | * To produce a well-formed JS program in Codegen, object methods and object expressions |
| 22 | * must be in the same ReactiveBlock as object method definitions must be inlined. |
| 23 | */ |
| 24 | |
| 25 | function findScopesToMerge(fn: HIRFunction): DisjointSet<ReactiveScope> { |
| 26 | const objectMethodDecls: Set<Identifier> = new Set(); |
| 27 | const mergeScopesBuilder = new DisjointSet<ReactiveScope>(); |
| 28 | |
| 29 | for (const [_, block] of fn.body.blocks) { |
| 30 | for (const {lvalue, value} of block.instructions) { |
| 31 | if (value.kind === 'ObjectMethod') { |
| 32 | objectMethodDecls.add(lvalue.identifier); |
| 33 | } else if (value.kind === 'ObjectExpression') { |
| 34 | for (const operand of eachInstructionValueOperand(value)) { |
| 35 | if (objectMethodDecls.has(operand.identifier)) { |
| 36 | const operandScope = operand.identifier.scope; |
| 37 | const lvalueScope = lvalue.identifier.scope; |
| 38 | |
| 39 | CompilerError.invariant( |
| 40 | operandScope != null && lvalueScope != null, |
| 41 | { |
| 42 | reason: |
| 43 | 'Internal error: Expected all ObjectExpressions and ObjectMethods to have non-null scope.', |
| 44 | loc: GeneratedSource, |
| 45 | }, |
| 46 | ); |
| 47 | mergeScopesBuilder.union([operandScope, lvalueScope]); |
| 48 | } |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | return mergeScopesBuilder; |
| 54 | } |
| 55 | |
| 56 | export function alignObjectMethodScopes(fn: HIRFunction): void { |
| 57 | // Handle inner functions: we assume that Scopes are disjoint across functions |
| 58 | for (const [_, block] of fn.body.blocks) { |
| 59 | for (const {value} of block.instructions) { |
| 60 | if ( |
| 61 | value.kind === 'ObjectMethod' || |
| 62 | value.kind === 'FunctionExpression' |
| 63 | ) { |
| 64 | alignObjectMethodScopes(value.loweredFunc.func); |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | const scopeGroupsMap = findScopesToMerge(fn).canonicalize(); |
| 70 | /** |
| 71 | * Step 1: Merge affected scopes to their canonical root. |
| 72 | */ |
| 73 | for (const [scope, root] of scopeGroupsMap) { |
| 74 | if (scope !== root) { |
| 75 | root.range.start = makeInstructionId( |
| 76 | Math.min(scope.range.start, root.range.start), |
| 77 | ); |
| 78 | root.range.end = makeInstructionId( |
| 79 | Math.max(scope.range.end, root.range.end), |
| 80 | ); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Step 2: Repoint identifiers whose scopes were merged. |
| 86 | */ |
| 87 | for (const [_, block] of fn.body.blocks) { |
| 88 | for (const { |
| 89 | lvalue: {identifier}, |
| 90 | } of block.instructions) { |
| 91 | if (identifier.scope != null) { |
| 92 | const root = scopeGroupsMap.get(identifier.scope); |
| 93 | if (root != null) { |
| 94 | identifier.scope = root; |
| 95 | } |
| 96 | // otherwise, this identifier's scope was not affected by this pass |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | } |