main
ts 192 lines 5.9 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 {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 }