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