| 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 {ProgramContext} from '..'; |
| 9 | import {CompilerError} from '../CompilerError'; |
| 10 | import { |
| 11 | DeclarationId, |
| 12 | GeneratedSource, |
| 13 | Identifier, |
| 14 | IdentifierName, |
| 15 | InstructionId, |
| 16 | Place, |
| 17 | PrunedReactiveScopeBlock, |
| 18 | ReactiveBlock, |
| 19 | ReactiveFunction, |
| 20 | ReactiveScopeBlock, |
| 21 | ReactiveValue, |
| 22 | ValidIdentifierName, |
| 23 | isPromotedJsxTemporary, |
| 24 | isPromotedTemporary, |
| 25 | makeIdentifierName, |
| 26 | } from '../HIR/HIR'; |
| 27 | import {collectReferencedGlobals} from './CollectReferencedGlobals'; |
| 28 | import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors'; |
| 29 | |
| 30 | /** |
| 31 | * Ensures that each named variable in the given function has a unique name |
| 32 | * that does not conflict with any other variables in the same block scope. |
| 33 | * Note that the scoping is based on the final inferred blocks, not the |
| 34 | * block scopes that were present in the original source. Thus variables |
| 35 | * that shadowed in the original source may end up with unique names in the |
| 36 | * output, if Forget would merge those two blocks into a single scope. |
| 37 | * |
| 38 | * Variables are renamed using their original name followed by a number, |
| 39 | * starting with 0 and incrementing until a unique name is found. Eg if the |
| 40 | * compiler collapses three scopes that each had their own `foo` declaration, |
| 41 | * they will be renamed to `foo`, `foo0`, and `foo1`, assuming that no conflicts' |
| 42 | * exist for `foo0` and `foo1`. |
| 43 | * |
| 44 | * For temporary values that are promoted to named variables, the starting name |
| 45 | * is "T0" for values that appear in JSX tag position and "t0" otherwise. If this |
| 46 | * name conflicts, the number portion increments until the name is unique (t1, t2, etc). |
| 47 | * |
| 48 | * Returns a Set of all the unique variable names in the function after renaming. |
| 49 | */ |
| 50 | export function renameVariables(fn: ReactiveFunction): Set<string> { |
| 51 | const globals = collectReferencedGlobals(fn); |
| 52 | const scopes = new Scopes(globals, fn.env.programContext); |
| 53 | renameVariablesImpl(fn, new Visitor(), scopes); |
| 54 | return new Set([...scopes.names, ...globals]); |
| 55 | } |
| 56 | |
| 57 | function renameVariablesImpl( |
| 58 | fn: ReactiveFunction, |
| 59 | visitor: Visitor, |
| 60 | scopes: Scopes, |
| 61 | ): void { |
| 62 | scopes.enter(() => { |
| 63 | for (const param of fn.params) { |
| 64 | if (param.kind === 'Identifier') { |
| 65 | scopes.visit(param.identifier); |
| 66 | } else { |
| 67 | scopes.visit(param.place.identifier); |
| 68 | } |
| 69 | } |
| 70 | visitReactiveFunction(fn, visitor, scopes); |
| 71 | }); |
| 72 | } |
| 73 | |
| 74 | class Visitor extends ReactiveFunctionVisitor<Scopes> { |
| 75 | override visitParam(place: Place, state: Scopes): void { |
| 76 | state.visit(place.identifier); |
| 77 | } |
| 78 | override visitLValue(_id: InstructionId, lvalue: Place, state: Scopes): void { |
| 79 | state.visit(lvalue.identifier); |
| 80 | } |
| 81 | override visitPlace(id: InstructionId, place: Place, state: Scopes): void { |
| 82 | state.visit(place.identifier); |
| 83 | } |
| 84 | override visitBlock(block: ReactiveBlock, state: Scopes): void { |
| 85 | state.enter(() => { |
| 86 | this.traverseBlock(block, state); |
| 87 | }); |
| 88 | } |
| 89 | |
| 90 | override visitPrunedScope( |
| 91 | scopeBlock: PrunedReactiveScopeBlock, |
| 92 | state: Scopes, |
| 93 | ): void { |
| 94 | this.traverseBlock(scopeBlock.instructions, state); |
| 95 | } |
| 96 | |
| 97 | override visitScope(scope: ReactiveScopeBlock, state: Scopes): void { |
| 98 | for (const [_, declaration] of scope.scope.declarations) { |
| 99 | state.visit(declaration.identifier); |
| 100 | } |
| 101 | this.traverseScope(scope, state); |
| 102 | } |
| 103 | |
| 104 | override visitValue( |
| 105 | id: InstructionId, |
| 106 | value: ReactiveValue, |
| 107 | state: Scopes, |
| 108 | ): void { |
| 109 | this.traverseValue(id, value, state); |
| 110 | if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') { |
| 111 | this.visitHirFunction(value.loweredFunc.func, state); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | override visitReactiveFunctionValue( |
| 116 | _id: InstructionId, |
| 117 | _dependencies: Array<Place>, |
| 118 | _fn: ReactiveFunction, |
| 119 | _state: Scopes, |
| 120 | ): void { |
| 121 | renameVariablesImpl(_fn, this, _state); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | class Scopes { |
| 126 | #seen: Map<DeclarationId, IdentifierName> = new Map(); |
| 127 | #stack: Array<Map<string, DeclarationId>> = [new Map()]; |
| 128 | #globals: Set<string>; |
| 129 | #programContext: ProgramContext; |
| 130 | names: Set<ValidIdentifierName> = new Set(); |
| 131 | |
| 132 | constructor(globals: Set<string>, programContext: ProgramContext) { |
| 133 | this.#globals = globals; |
| 134 | this.#programContext = programContext; |
| 135 | } |
| 136 | |
| 137 | visit(identifier: Identifier): void { |
| 138 | const originalName = identifier.name; |
| 139 | if (originalName === null) { |
| 140 | return; |
| 141 | } |
| 142 | const mappedName = this.#seen.get(identifier.declarationId); |
| 143 | if (mappedName !== undefined) { |
| 144 | identifier.name = mappedName; |
| 145 | return; |
| 146 | } |
| 147 | let name: string = originalName.value; |
| 148 | let id = 0; |
| 149 | if (isPromotedTemporary(originalName.value)) { |
| 150 | name = `t${id++}`; |
| 151 | } else if (isPromotedJsxTemporary(originalName.value)) { |
| 152 | name = `T${id++}`; |
| 153 | } |
| 154 | while (this.#lookup(name) !== null || this.#globals.has(name)) { |
| 155 | if (isPromotedTemporary(originalName.value)) { |
| 156 | name = `t${id++}`; |
| 157 | } else if (isPromotedJsxTemporary(originalName.value)) { |
| 158 | name = `T${id++}`; |
| 159 | } else { |
| 160 | name = `${originalName.value}$${id++}`; |
| 161 | } |
| 162 | } |
| 163 | this.#programContext.addNewReference(name); |
| 164 | const identifierName = makeIdentifierName(name); |
| 165 | identifier.name = identifierName; |
| 166 | this.#seen.set(identifier.declarationId, identifierName); |
| 167 | this.#stack.at(-1)!.set(identifierName.value, identifier.declarationId); |
| 168 | this.names.add(identifierName.value); |
| 169 | } |
| 170 | |
| 171 | #lookup(name: string): DeclarationId | null { |
| 172 | for (let i = this.#stack.length - 1; i >= 0; i--) { |
| 173 | const scope = this.#stack[i]!; |
| 174 | const entry = scope.get(name); |
| 175 | if (entry !== undefined) { |
| 176 | return entry; |
| 177 | } |
| 178 | } |
| 179 | return null; |
| 180 | } |
| 181 | |
| 182 | enter(fn: () => void): void { |
| 183 | const next = new Map(); |
| 184 | this.#stack.push(next); |
| 185 | fn(); |
| 186 | const last = this.#stack.pop(); |
| 187 | CompilerError.invariant(last === next, { |
| 188 | reason: 'Mismatch push/pop calls', |
| 189 | loc: GeneratedSource, |
| 190 | }); |
| 191 | } |
| 192 | } |