| 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 | IdentifierId, |
| 11 | ReactiveScope, |
| 12 | makeInstructionId, |
| 13 | } from '../HIR'; |
| 14 | import DisjointSet from '../Utils/DisjointSet'; |
| 15 | |
| 16 | /** |
| 17 | * Ensures that method call instructions have scopes such that either: |
| 18 | * - Both the MethodCall and its property have the same scope |
| 19 | * - OR neither has a scope |
| 20 | */ |
| 21 | export function alignMethodCallScopes(fn: HIRFunction): void { |
| 22 | const scopeMapping = new Map<IdentifierId, ReactiveScope | null>(); |
| 23 | const mergedScopes = new DisjointSet<ReactiveScope>(); |
| 24 | |
| 25 | for (const [, block] of fn.body.blocks) { |
| 26 | for (const instr of block.instructions) { |
| 27 | const {lvalue, value} = instr; |
| 28 | if (value.kind === 'MethodCall') { |
| 29 | const lvalueScope = lvalue.identifier.scope; |
| 30 | const propertyScope = value.property.identifier.scope; |
| 31 | if (lvalueScope !== null) { |
| 32 | if (propertyScope !== null) { |
| 33 | // Both have a scope: merge the scopes |
| 34 | mergedScopes.union([lvalueScope, propertyScope]); |
| 35 | } else { |
| 36 | /* |
| 37 | * Else the call itself has a scope but not the property, |
| 38 | * record that this property should be in this scope |
| 39 | */ |
| 40 | scopeMapping.set(value.property.identifier.id, lvalueScope); |
| 41 | } |
| 42 | } else if (propertyScope !== null) { |
| 43 | // else this property does not need a scope |
| 44 | scopeMapping.set(value.property.identifier.id, null); |
| 45 | } |
| 46 | } else if ( |
| 47 | value.kind === 'FunctionExpression' || |
| 48 | value.kind === 'ObjectMethod' |
| 49 | ) { |
| 50 | alignMethodCallScopes(value.loweredFunc.func); |
| 51 | } |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | mergedScopes.forEach((scope, root) => { |
| 56 | if (scope === root) { |
| 57 | return; |
| 58 | } |
| 59 | root.range.start = makeInstructionId( |
| 60 | Math.min(scope.range.start, root.range.start), |
| 61 | ); |
| 62 | root.range.end = makeInstructionId( |
| 63 | Math.max(scope.range.end, root.range.end), |
| 64 | ); |
| 65 | }); |
| 66 | |
| 67 | for (const [, block] of fn.body.blocks) { |
| 68 | for (const instr of block.instructions) { |
| 69 | const mappedScope = scopeMapping.get(instr.lvalue.identifier.id); |
| 70 | if (mappedScope !== undefined) { |
| 71 | instr.lvalue.identifier.scope = mappedScope; |
| 72 | } else if (instr.lvalue.identifier.scope !== null) { |
| 73 | const mergedScope = mergedScopes.find(instr.lvalue.identifier.scope); |
| 74 | if (mergedScope != null) { |
| 75 | instr.lvalue.identifier.scope = mergedScope; |
| 76 | } |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | } |