main
md 203 lines 6.59 KB
Rendered Raw
1 # promoteUsedTemporaries
2
3 ## File
4 `src/ReactiveScopes/PromoteUsedTemporaries.ts`
5
6 ## Purpose
7 This pass promotes temporary variables (identifiers with no name) to named variables when they need to be referenced across scope boundaries or in code generation. Temporaries are intermediate values that the compiler creates during lowering; they are typically inlined at their use sites during codegen. However, some temporaries must be emitted as separate declarations - this pass identifies and names them.
8
9 The pass ensures that:
10 1. Scope dependencies and declarations have proper names for codegen
11 2. Variables referenced across reactive scope boundaries are named
12 3. JSX tag identifiers get special naming (`T0`, `T1`, etc.)
13 4. Temporaries with interposing side-effects are promoted to preserve ordering
14
15 ## Input Invariants
16 - The ReactiveFunction has undergone scope construction and dependency propagation
17 - Identifiers may have `name === null` (temporaries) or be named
18 - Scopes have `dependencies`, `declarations`, and `reassignments` populated
19 - Pruned scopes are properly marked with `kind: 'pruned-scope'`
20
21 ## Output Guarantees
22 - All scope dependencies have non-null names
23 - All scope declarations have non-null names
24 - JSX tag temporaries use uppercase naming (`T0`, `T1`, ...)
25 - Regular temporaries use lowercase naming (`#t{id}`)
26 - All instances of a promoted identifier share the same name (via DeclarationId tracking)
27 - Temporaries with interposing mutating instructions are promoted to preserve source ordering
28
29 ## Algorithm
30
31 The pass operates in four phases using visitor classes:
32
33 ### Phase 1: CollectPromotableTemporaries
34 Collects information about which temporaries may need promotion:
35
36 ```typescript
37 class CollectPromotableTemporaries {
38 // Tracks pruned scope declarations and whether they're used outside their scope
39 pruned: Map<DeclarationId, {activeScopes: Array<ScopeId>; usedOutsideScope: boolean}>
40
41 // Tracks identifiers used as JSX tags (need uppercase names)
42 tags: Set<DeclarationId>
43 }
44 ```
45
46 - When visiting a `JsxExpression`, adds the tag identifier to `tags`
47 - When visiting a `PrunedScope`, records its declarations
48 - Tracks when pruned declarations are used in different scopes
49
50 ### Phase 2: PromoteTemporaries
51 Promotes temporaries that appear in positions requiring names:
52
53 ```typescript
54 override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
55 // Promote all dependencies without names
56 for (const dep of scopeBlock.scope.dependencies) {
57 if (identifier.name == null) {
58 promoteIdentifier(identifier, state);
59 }
60 }
61 // Promote all declarations without names
62 for (const [, declaration] of scopeBlock.scope.declarations) {
63 if (declaration.identifier.name == null) {
64 promoteIdentifier(declaration.identifier, state);
65 }
66 }
67 }
68 ```
69
70 Also promotes:
71 - Function parameters without names
72 - Pruned scope declarations used outside their scope
73
74 ### Phase 3: PromoteInterposedTemporaries
75 Handles ordering-sensitive promotion:
76
77 ```typescript
78 class PromoteInterposedTemporaries {
79 // Instructions that emit as statements can interpose between temp defs and uses
80 // If such an instruction occurs, mark pending temporaries as needing promotion
81
82 override visitInstruction(instruction: ReactiveInstruction, state: InterState): void {
83 // For instructions that become statements (calls, stores, etc.):
84 if (willBeStatement && !constStore) {
85 // Mark all pending temporaries as needing promotion
86 for (const [key, [ident, _]] of state.entries()) {
87 state.set(key, [ident, true]); // Mark as needing promotion
88 }
89 }
90 }
91 }
92 ```
93
94 This preserves source ordering when side-effects occur between a temporary's definition and use.
95
96 ### Phase 4: PromoteAllInstancesOfPromotedTemporaries
97 Ensures all instances of a promoted identifier share the same name:
98
99 ```typescript
100 class PromoteAllInstancesOfPromotedTemporaries {
101 override visitPlace(_id: InstructionId, place: Place, state: State): void {
102 if (place.identifier.name === null &&
103 state.promoted.has(place.identifier.declarationId)) {
104 promoteIdentifier(place.identifier, state);
105 }
106 }
107 }
108 ```
109
110 ### Naming Convention
111 ```typescript
112 function promoteIdentifier(identifier: Identifier, state: State): void {
113 if (state.tags.has(identifier.declarationId)) {
114 promoteTemporaryJsxTag(identifier); // Uses #T{id} for JSX tags
115 } else {
116 promoteTemporary(identifier); // Uses #t{id} for regular temps
117 }
118 state.promoted.add(identifier.declarationId);
119 }
120 ```
121
122 ## Edge Cases
123
124 ### JSX Tag Temporaries
125 JSX tags require uppercase names to be valid JSX syntax. The pass tracks which temporaries are used as JSX tags and uses `T0`, `T1`, etc. instead of `t0`, `t1`.
126
127 ### Pruned Scope Declarations
128 Declarations in pruned scopes are only promoted if they're actually used outside the pruned scope, avoiding unnecessary variable declarations.
129
130 ### Const vs Let Temporaries
131 The pass tracks const identifiers specially - they don't need promotion for ordering purposes since they can't be mutated by interposing instructions.
132
133 ### Global Loads
134 Values loaded from globals (and their property loads) are treated as const-like for promotion purposes.
135
136 ### Method Call Properties
137 The property identifier in a method call is treated as const-like to avoid unnecessary promotion.
138
139 ## TODOs
140 None in the source file.
141
142 ## Example
143
144 ### Fixture: `simple.js`
145
146 **Input:**
147 ```javascript
148 export default function foo(x, y) {
149 if (x) {
150 return foo(false, y);
151 }
152 return [y * 10];
153 }
154 ```
155
156 **Before PromoteUsedTemporaries:**
157 ```
158 scope @0 [...] dependencies=[y$14] declarations=[$19_@0]
159 scope @1 [...] dependencies=[$22] declarations=[$23_@1]
160 ```
161
162 **After PromoteUsedTemporaries:**
163 ```
164 scope @0 [...] dependencies=[y$14] declarations=[#t5$19_@0]
165 scope @1 [...] dependencies=[#t9$22] declarations=[#t10$23_@1]
166 ```
167
168 Key observations:
169 - `$19_@0` is promoted to `#t5$19_@0` because it's a scope declaration
170 - `$22` is promoted to `#t9$22` because it's a scope dependency
171 - `$23_@1` is promoted to `#t10$23_@1` because it's a scope declaration
172 - The `#t` prefix indicates this is a promoted temporary (later renamed by `renameVariables`)
173
174 **Generated Code:**
175 ```javascript
176 import { c as _c } from "react/compiler-runtime";
177 export default function foo(x, y) {
178 const $ = _c(4);
179 if (x) {
180 let t0;
181 if ($[0] !== y) {
182 t0 = foo(false, y);
183 $[0] = y;
184 $[1] = t0;
185 } else {
186 t0 = $[1];
187 }
188 return t0;
189 }
190 const t0 = y * 10;
191 let t1;
192 if ($[2] !== t0) {
193 t1 = [t0];
194 $[2] = t0;
195 $[3] = t1;
196 } else {
197 t1 = $[3];
198 }
199 return t1;
200 }
201 ```
202
203 The promoted temporaries (`#t5`, `#t9`, `#t10`) become the named variables (`t0`, `t1`) in the output after `renameVariables` runs.