Skip to main content

react_compiler_reactive_scopes/
rename_variables.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This source code is licensed under the MIT license found in the
4// LICENSE file in the root directory of this source tree.
5
6//! RenameVariables — renames variables for output, assigns unique names,
7//! handles SSA renames.
8//!
9//! Corresponds to `src/ReactiveScopes/RenameVariables.ts`.
10
11use rustc_hash::FxHashMap;
12use rustc_hash::FxHashSet;
13
14use react_compiler_hir::DeclarationId;
15use react_compiler_hir::EvaluationOrder;
16use react_compiler_hir::FunctionId;
17use react_compiler_hir::IdentifierName;
18use react_compiler_hir::InstructionValue;
19use react_compiler_hir::ParamPattern;
20use react_compiler_hir::Place;
21use react_compiler_hir::PrunedReactiveScopeBlock;
22use react_compiler_hir::ReactiveBlock;
23use react_compiler_hir::ReactiveFunction;
24use react_compiler_hir::ReactiveScopeBlock;
25use react_compiler_hir::ReactiveValue;
26use react_compiler_hir::environment::Environment;
27
28use crate::visitors::ReactiveFunctionVisitor;
29use crate::visitors::{self};
30
31// =============================================================================
32// Scopes
33// =============================================================================
34
35struct Scopes {
36    seen: FxHashMap<DeclarationId, IdentifierName>,
37    stack: Vec<FxHashMap<String, DeclarationId>>,
38    globals: FxHashSet<String>,
39    names: FxHashSet<String>,
40}
41
42impl Scopes {
43    fn new(globals: FxHashSet<String>) -> Self {
44        Self {
45            seen: FxHashMap::default(),
46            stack: vec![FxHashMap::default()],
47            globals,
48            names: FxHashSet::default(),
49        }
50    }
51
52    fn visit_identifier(
53        &mut self,
54        identifier_id: react_compiler_hir::IdentifierId,
55        env: &Environment,
56    ) {
57        let identifier = &env.identifiers[identifier_id.0 as usize];
58        let original_name = match &identifier.name {
59            Some(name) => name.clone(),
60            None => return,
61        };
62        let declaration_id = identifier.declaration_id;
63
64        if self.seen.contains_key(&declaration_id) {
65            return;
66        }
67
68        let original_value = original_name.value().to_string();
69        let is_promoted = matches!(original_name, IdentifierName::Promoted(_));
70        let is_promoted_temp = is_promoted && original_value.starts_with("#t");
71        let is_promoted_jsx = is_promoted && original_value.starts_with("#T");
72
73        let mut name: String;
74        let mut id: u32 = 0;
75        if is_promoted_temp {
76            name = format!("t{}", id);
77            id += 1;
78        } else if is_promoted_jsx {
79            name = format!("T{}", id);
80            id += 1;
81        } else {
82            name = original_value.clone();
83        }
84
85        while self.lookup(&name).is_some() || self.globals.contains(&name) {
86            if is_promoted_temp {
87                name = format!("t{}", id);
88                id += 1;
89            } else if is_promoted_jsx {
90                name = format!("T{}", id);
91                id += 1;
92            } else {
93                name = format!("{}${}", original_value, id);
94                id += 1;
95            }
96        }
97
98        let identifier_name = IdentifierName::Named(name.clone());
99        self.seen.insert(declaration_id, identifier_name);
100        self.stack
101            .last_mut()
102            .unwrap()
103            .insert(name.clone(), declaration_id);
104        self.names.insert(name);
105    }
106
107    fn lookup(&self, name: &str) -> Option<DeclarationId> {
108        for scope in self.stack.iter().rev() {
109            if let Some(id) = scope.get(name) {
110                return Some(*id);
111            }
112        }
113        None
114    }
115
116    fn enter(&mut self) {
117        self.stack.push(FxHashMap::default());
118    }
119
120    fn leave(&mut self) {
121        self.stack.pop();
122    }
123}
124
125// =============================================================================
126// Visitor — TS: `class Visitor extends ReactiveFunctionVisitor<Scopes>`
127// =============================================================================
128
129struct Visitor<'a> {
130    env: &'a Environment,
131}
132
133impl ReactiveFunctionVisitor for Visitor<'_> {
134    type State = Scopes;
135
136    fn env(&self) -> &Environment {
137        self.env
138    }
139
140    /// TS: `visitParam(place, state) { state.visit(place.identifier) }`
141    fn visit_param(&self, place: &Place, state: &mut Scopes) {
142        state.visit_identifier(place.identifier, self.env);
143    }
144
145    /// TS: `visitLValue(_id, lvalue, state) { state.visit(lvalue.identifier) }`
146    fn visit_lvalue(&self, _id: EvaluationOrder, lvalue: &Place, state: &mut Scopes) {
147        state.visit_identifier(lvalue.identifier, self.env);
148    }
149
150    /// TS: `visitPlace(_id, place, state) { state.visit(place.identifier) }`
151    fn visit_place(&self, _id: EvaluationOrder, place: &Place, state: &mut Scopes) {
152        state.visit_identifier(place.identifier, self.env);
153    }
154
155    /// TS: `visitBlock(block, state) { state.enter(() => { this.traverseBlock(block, state) }) }`
156    fn visit_block(&self, block: &ReactiveBlock, state: &mut Scopes) {
157        state.enter();
158        self.traverse_block(block, state);
159        state.leave();
160    }
161
162    /// TS: `visitPrunedScope(scopeBlock, state) { this.traverseBlock(scopeBlock.instructions, state) }`
163    /// No enter/leave — names assigned inside pruned scopes remain visible in
164    /// the enclosing scope, preventing name reuse.
165    fn visit_pruned_scope(&self, scope: &PrunedReactiveScopeBlock, state: &mut Scopes) {
166        self.traverse_block(&scope.instructions, state);
167    }
168
169    /// TS: `visitScope(scope, state) { for (const [_, decl] of scope.scope.declarations) state.visit(decl.identifier); this.traverseScope(scope, state) }`
170    fn visit_scope(&self, scope: &ReactiveScopeBlock, state: &mut Scopes) {
171        let scope_data = &self.env.scopes[scope.scope.0 as usize];
172        let decl_ids: Vec<react_compiler_hir::IdentifierId> = scope_data
173            .declarations
174            .iter()
175            .map(|(_, d)| d.identifier)
176            .collect();
177        for id in decl_ids {
178            state.visit_identifier(id, self.env);
179        }
180        self.traverse_scope(scope, state);
181    }
182
183    /// TS: `visitValue(id, value, state) { this.traverseValue(id, value, state); if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') this.visitHirFunction(value.loweredFunc.func, state) }`
184    fn visit_value(&self, id: EvaluationOrder, value: &ReactiveValue, state: &mut Scopes) {
185        self.traverse_value(id, value, state);
186        if let ReactiveValue::Instruction(iv) = value {
187            match iv {
188                InstructionValue::FunctionExpression { lowered_func, .. }
189                | InstructionValue::ObjectMethod { lowered_func, .. } => {
190                    self.visit_hir_function(lowered_func.func, state);
191                }
192                _ => {}
193            }
194        }
195    }
196}
197
198// =============================================================================
199// Public entry point
200// =============================================================================
201
202/// Renames variables for output — assigns unique names, handles SSA renames.
203/// Returns a Set of all unique variable names used.
204/// TS: `renameVariables`
205pub fn rename_variables(func: &mut ReactiveFunction, env: &mut Environment) -> FxHashSet<String> {
206    rename_variables_with_parent(func, env, None)
207}
208
209fn rename_variables_with_parent(
210    func: &mut ReactiveFunction,
211    env: &mut Environment,
212    parent_names: Option<&FxHashSet<String>>,
213) -> FxHashSet<String> {
214    let globals = collect_referenced_globals(&func.body, env);
215
216    // Phase 1: Use ReactiveFunctionVisitor to compute the rename mapping.
217    // This collects DeclarationId -> IdentifierName without mutating env.
218    let mut scopes = Scopes::new(globals.clone());
219    // If parent names are provided (for outlined functions), pre-populate
220    // the scope stack so that parameter names don't collide with parent
221    // variables. In the TS compiler, outlined functions are placed in the
222    // parent function body and processed within the parent's scope context.
223    if let Some(parent) = parent_names {
224        scopes.enter();
225        for name in parent {
226            scopes
227                .stack
228                .last_mut()
229                .unwrap()
230                .insert(name.clone(), DeclarationId(u32::MAX));
231            scopes.names.insert(name.clone());
232        }
233    }
234    rename_variables_impl(func, &Visitor { env }, &mut scopes);
235
236    // Phase 2: Apply the computed renames to all identifiers in env.
237    for identifier in env.identifiers.iter_mut() {
238        if let Some(mapped_name) = scopes.seen.get(&identifier.declaration_id) {
239            if identifier.name.is_some() {
240                identifier.name = Some(mapped_name.clone());
241            }
242        }
243    }
244
245    let mut result: FxHashSet<String> = scopes.names;
246    result.extend(globals);
247    result
248}
249
250/// TS: `renameVariablesImpl`
251fn rename_variables_impl(func: &ReactiveFunction, visitor: &Visitor, scopes: &mut Scopes) {
252    scopes.enter();
253    for param in &func.params {
254        let place = match param {
255            ParamPattern::Place(p) => p,
256            ParamPattern::Spread(s) => &s.place,
257        };
258        visitor.visit_param(place, scopes);
259    }
260    visitors::visit_reactive_function(func, visitor, scopes);
261    scopes.leave();
262}
263
264// =============================================================================
265// CollectReferencedGlobals
266// =============================================================================
267
268/// Collects all globally referenced names from the reactive function.
269/// TS: `collectReferencedGlobals`
270fn collect_referenced_globals(block: &ReactiveBlock, env: &Environment) -> FxHashSet<String> {
271    let mut globals = FxHashSet::default();
272    collect_globals_block(block, &mut globals, env);
273    globals
274}
275
276fn collect_globals_block(block: &ReactiveBlock, globals: &mut FxHashSet<String>, env: &Environment) {
277    for stmt in block {
278        match stmt {
279            react_compiler_hir::ReactiveStatement::Instruction(instr) => {
280                collect_globals_value(&instr.value, globals, env);
281            }
282            react_compiler_hir::ReactiveStatement::Scope(scope) => {
283                collect_globals_block(&scope.instructions, globals, env);
284            }
285            react_compiler_hir::ReactiveStatement::PrunedScope(scope) => {
286                collect_globals_block(&scope.instructions, globals, env);
287            }
288            react_compiler_hir::ReactiveStatement::Terminal(terminal) => {
289                collect_globals_terminal(terminal, globals, env);
290            }
291        }
292    }
293}
294
295fn collect_globals_value(value: &ReactiveValue, globals: &mut FxHashSet<String>, env: &Environment) {
296    match value {
297        ReactiveValue::Instruction(iv) => {
298            if let InstructionValue::LoadGlobal { binding, .. } = iv {
299                globals.insert(binding.name().to_string());
300            }
301            // Visit inner functions
302            match iv {
303                InstructionValue::FunctionExpression { lowered_func, .. }
304                | InstructionValue::ObjectMethod { lowered_func, .. } => {
305                    collect_globals_hir_function(lowered_func.func, globals, env);
306                }
307                _ => {}
308            }
309        }
310        ReactiveValue::SequenceExpression {
311            instructions,
312            value: inner,
313            ..
314        } => {
315            for instr in instructions {
316                collect_globals_value(&instr.value, globals, env);
317            }
318            collect_globals_value(inner, globals, env);
319        }
320        ReactiveValue::ConditionalExpression {
321            test,
322            consequent,
323            alternate,
324            ..
325        } => {
326            collect_globals_value(test, globals, env);
327            collect_globals_value(consequent, globals, env);
328            collect_globals_value(alternate, globals, env);
329        }
330        ReactiveValue::LogicalExpression { left, right, .. } => {
331            collect_globals_value(left, globals, env);
332            collect_globals_value(right, globals, env);
333        }
334        ReactiveValue::OptionalExpression { value: inner, .. } => {
335            collect_globals_value(inner, globals, env);
336        }
337    }
338}
339
340/// Recursively collects LoadGlobal names from an inner HIR function.
341fn collect_globals_hir_function(
342    func_id: FunctionId,
343    globals: &mut FxHashSet<String>,
344    env: &Environment,
345) {
346    let inner_func = &env.functions[func_id.0 as usize];
347    let block_ids: Vec<_> = inner_func.body.blocks.keys().copied().collect();
348    for block_id in block_ids {
349        let inner_func = &env.functions[func_id.0 as usize];
350        let block = &inner_func.body.blocks[&block_id];
351        for instr_id in &block.instructions {
352            let instr = &inner_func.instructions[instr_id.0 as usize];
353            if let InstructionValue::LoadGlobal { binding, .. } = &instr.value {
354                globals.insert(binding.name().to_string());
355            }
356            // Recurse into nested function expressions
357            match &instr.value {
358                InstructionValue::FunctionExpression { lowered_func, .. }
359                | InstructionValue::ObjectMethod { lowered_func, .. } => {
360                    collect_globals_hir_function(lowered_func.func, globals, env);
361                }
362                _ => {}
363            }
364        }
365    }
366}
367
368fn collect_globals_terminal(
369    stmt: &react_compiler_hir::ReactiveTerminalStatement,
370    globals: &mut FxHashSet<String>,
371    env: &Environment,
372) {
373    match &stmt.terminal {
374        react_compiler_hir::ReactiveTerminal::Break { .. }
375        | react_compiler_hir::ReactiveTerminal::Continue { .. } => {}
376        react_compiler_hir::ReactiveTerminal::Return { .. }
377        | react_compiler_hir::ReactiveTerminal::Throw { .. } => {}
378        react_compiler_hir::ReactiveTerminal::For {
379            init,
380            test,
381            update,
382            loop_block,
383            ..
384        } => {
385            collect_globals_value(init, globals, env);
386            collect_globals_value(test, globals, env);
387            collect_globals_block(loop_block, globals, env);
388            if let Some(update) = update {
389                collect_globals_value(update, globals, env);
390            }
391        }
392        react_compiler_hir::ReactiveTerminal::ForOf {
393            init,
394            test,
395            loop_block,
396            ..
397        } => {
398            collect_globals_value(init, globals, env);
399            collect_globals_value(test, globals, env);
400            collect_globals_block(loop_block, globals, env);
401        }
402        react_compiler_hir::ReactiveTerminal::ForIn {
403            init, loop_block, ..
404        } => {
405            collect_globals_value(init, globals, env);
406            collect_globals_block(loop_block, globals, env);
407        }
408        react_compiler_hir::ReactiveTerminal::DoWhile {
409            loop_block, test, ..
410        } => {
411            collect_globals_block(loop_block, globals, env);
412            collect_globals_value(test, globals, env);
413        }
414        react_compiler_hir::ReactiveTerminal::While {
415            test, loop_block, ..
416        } => {
417            collect_globals_value(test, globals, env);
418            collect_globals_block(loop_block, globals, env);
419        }
420        react_compiler_hir::ReactiveTerminal::If {
421            consequent,
422            alternate,
423            ..
424        } => {
425            collect_globals_block(consequent, globals, env);
426            if let Some(alt) = alternate {
427                collect_globals_block(alt, globals, env);
428            }
429        }
430        react_compiler_hir::ReactiveTerminal::Switch { cases, .. } => {
431            for case in cases {
432                if let Some(block) = &case.block {
433                    collect_globals_block(block, globals, env);
434                }
435            }
436        }
437        react_compiler_hir::ReactiveTerminal::Label { block, .. } => {
438            collect_globals_block(block, globals, env);
439        }
440        react_compiler_hir::ReactiveTerminal::Try { block, handler, .. } => {
441            collect_globals_block(block, globals, env);
442            collect_globals_block(handler, globals, env);
443        }
444    }
445}