main
md 200 lines 5.93 KB
Rendered Raw
1 # renameVariables
2
3 ## File
4 `src/ReactiveScopes/RenameVariables.ts`
5
6 ## Purpose
7 This pass ensures that every named variable in the function has a unique name that doesn't conflict with other variables in the same block scope or with global identifiers. After scope construction and temporary promotion, variables from different source scopes may end up in the same reactive block - this pass resolves any naming conflicts.
8
9 The pass also converts the `#t{id}` promoted temporary names into clean output names like `t0`, `t1`, etc.
10
11 ## Input Invariants
12 - The ReactiveFunction has been through `promoteUsedTemporaries`
13 - Variables may have names that conflict with:
14 - Other variables in the same or ancestor block scope
15 - Global identifiers referenced by the function
16 - Promoted temporaries with `#t{id}` or `#T{id}` naming
17 - The function parameters have names (either from source or promoted)
18
19 ## Output Guarantees
20 - Every named variable has a unique name within its scope
21 - No variable shadows a global identifier referenced by the function
22 - Promoted temporaries are renamed to `t0`, `t1`, ... (for regular temps)
23 - Promoted JSX temporaries are renamed to `T0`, `T1`, ... (for JSX tags)
24 - Conflicting source names get disambiguated with `$` suffix (e.g., `foo$0`, `foo$1`)
25 - Returns a `Set<string>` of all unique variable names in the function
26
27 ## Algorithm
28
29 ### Phase 1: Collect Referenced Globals
30 Uses `collectReferencedGlobals(fn)` to build a set of all global identifiers referenced by the function. Variable names must not conflict with these.
31
32 ### Phase 2: Rename with Scope Stack
33 The `Scopes` class maintains:
34
35 ```typescript
36 class Scopes {
37 #seen: Map<DeclarationId, IdentifierName> = new Map(); // Canonical name for each declaration
38 #stack: Array<Map<string, DeclarationId>> = [new Map()]; // Block scope stack
39 #globals: Set<string>; // Global names to avoid
40 names: Set<ValidIdentifierName> = new Set(); // All assigned names
41 }
42 ```
43
44 ### Renaming Logic
45 ```typescript
46 visit(identifier: Identifier): void {
47 // Skip unnamed identifiers
48 if (originalName === null) return;
49
50 // If we've already named this declaration, reuse that name
51 const mappedName = this.#seen.get(identifier.declarationId);
52 if (mappedName !== undefined) {
53 identifier.name = mappedName;
54 return;
55 }
56
57 // Find a unique name
58 let name = originalName.value;
59 let id = 0;
60
61 // Promoted temporaries start with t0/T0
62 if (isPromotedTemporary(originalName.value)) {
63 name = `t${id++}`;
64 } else if (isPromotedJsxTemporary(originalName.value)) {
65 name = `T${id++}`;
66 }
67
68 // Increment until we find a unique name
69 while (this.#lookup(name) !== null || this.#globals.has(name)) {
70 if (isPromotedTemporary(...)) {
71 name = `t${id++}`;
72 } else if (isPromotedJsxTemporary(...)) {
73 name = `T${id++}`;
74 } else {
75 name = `${originalName.value}$${id++}`; // foo$0, foo$1, etc.
76 }
77 }
78
79 identifier.name = makeIdentifierName(name);
80 this.#seen.set(identifier.declarationId, identifier.name);
81 }
82 ```
83
84 ### Scope Management
85 ```typescript
86 enter(fn: () => void): void {
87 this.#stack.push(new Map());
88 fn();
89 this.#stack.pop();
90 }
91
92 #lookup(name: string): DeclarationId | null {
93 // Search from innermost to outermost scope
94 for (let i = this.#stack.length - 1; i >= 0; i--) {
95 const entry = this.#stack[i].get(name);
96 if (entry !== undefined) return entry;
97 }
98 return null;
99 }
100 ```
101
102 ### Visitor Pattern
103 ```typescript
104 class Visitor extends ReactiveFunctionVisitor<Scopes> {
105 override visitBlock(block: ReactiveBlock, state: Scopes): void {
106 state.enter(() => {
107 this.traverseBlock(block, state);
108 });
109 }
110
111 override visitScope(scope: ReactiveScopeBlock, state: Scopes): void {
112 // Visit scope declarations first
113 for (const [_, declaration] of scope.scope.declarations) {
114 state.visit(declaration.identifier);
115 }
116 this.traverseScope(scope, state);
117 }
118
119 override visitPlace(id: InstructionId, place: Place, state: Scopes): void {
120 state.visit(place.identifier);
121 }
122 }
123 ```
124
125 ## Edge Cases
126
127 ### Shadowed Variables
128 When the compiler merges scopes that had shadowing in the source:
129 ```javascript
130 function foo() {
131 const x = 1;
132 {
133 const x = 2; // Shadowed in source
134 }
135 }
136 ```
137 If both `x` declarations end up in the same compiled scope, they become `x` and `x$0`.
138
139 ### Global Name Conflicts
140 If a local variable would conflict with a referenced global:
141 ```javascript
142 function foo() {
143 const Math = 1; // Conflicts with global Math if used
144 }
145 ```
146 The local gets renamed to `Math$0` if `Math` global is referenced.
147
148 ### Nested Functions
149 The pass recursively processes nested function expressions, entering a new scope for each function body.
150
151 ### Pruned Scopes
152 Pruned scopes don't create a new block scope in the output - the pass traverses their instructions without entering a new scope level.
153
154 ### DeclarationId Consistency
155 The pass uses `DeclarationId` to track which identifiers refer to the same variable, ensuring all references get the same renamed name.
156
157 ## TODOs
158 None in the source file.
159
160 ## Example
161
162 ### Fixture: `simple.js`
163
164 **Before RenameVariables:**
165 ```
166 scope @0 [...] declarations=[#t5$19_@0]
167 scope @1 [...] dependencies=[#t9$22] declarations=[#t10$23_@1]
168 ```
169
170 **After RenameVariables:**
171 ```
172 scope @0 [...] declarations=[t0$19_@0]
173 scope @1 [...] dependencies=[t0$22] declarations=[t1$23_@1]
174 ```
175
176 Key observations:
177 - `#t5$19_@0` becomes `t0$19_@0` (first temporary in scope)
178 - `#t9$22` becomes `t0$22` (first temporary in a different block scope)
179 - `#t10$23_@1` becomes `t1$23_@1` (second temporary in that block)
180 - The `#t` prefix is removed and sequential numbering is applied
181
182 **Generated Code:**
183 ```javascript
184 export default function foo(x, y) {
185 const $ = _c(4);
186 if (x) {
187 let t0; // Was #t5
188 if ($[0] !== y) {
189 t0 = foo(false, y);
190 // ...
191 }
192 return t0;
193 }
194 const t0 = y * 10; // Was #t9, reuses t0 since different block scope
195 let t1; // Was #t10
196 // ...
197 }
198 ```
199
200 The pass produces clean, readable output with minimal variable names while avoiding conflicts.