1use indexmap::{IndexMap, IndexSet};2use react_compiler_ast::scope::BindingId;3use react_compiler_ast::scope::ImportBindingKind;4use react_compiler_ast::scope::ScopeId;5use react_compiler_ast::scope::ScopeInfo;6use react_compiler_diagnostics::CompilerDiagnostic;7use react_compiler_diagnostics::CompilerDiagnosticDetail;8use react_compiler_diagnostics::CompilerError;9use react_compiler_diagnostics::CompilerErrorDetail;10use react_compiler_diagnostics::ErrorCategory;11use react_compiler_hir::environment::Environment;12use react_compiler_hir::visitors::each_terminal_successor;13use react_compiler_hir::visitors::terminal_fallthrough;14use react_compiler_hir::*;15use rustc_hash::FxBuildHasher;1617use crate::identifier_loc_index::IdentifierLocIndex;1819// ---------------------------------------------------------------------------20// Reserved word check (matches TS isReservedWord)21// ---------------------------------------------------------------------------2223pub(crate) fn is_always_reserved_word(s: &str) -> bool {24 matches!(25 s,26 "break"27 | "case"28 | "catch"29 | "continue"30 | "debugger"31 | "default"32 | "do"33 | "else"34 | "finally"35 | "for"36 | "function"37 | "if"38 | "in"39 | "instanceof"40 | "new"41 | "return"42 | "switch"43 | "this"44 | "throw"45 | "try"46 | "typeof"47 | "var"48 | "void"49 | "while"50 | "with"51 | "class"52 | "const"53 | "enum"54 | "export"55 | "extends"56 | "import"57 | "super"58 | "null"59 | "true"60 | "false"61 | "delete"62 )63}6465pub(crate) fn reserved_identifier_diagnostic(name: &str) -> CompilerDiagnostic {66 CompilerDiagnostic::new(67 ErrorCategory::Syntax,68 "Expected a non-reserved identifier name",69 Some(format!(70 "`{}` is a reserved word in JavaScript and cannot be used as an identifier name",71 name72 )),73 )74 .with_detail(CompilerDiagnosticDetail::Error {75 loc: None, // GeneratedSource in TS76 message: Some("reserved word".to_string()),77 identifier_name: None,78 })79}8081// ---------------------------------------------------------------------------82// Scope types for tracking break/continue targets83// ---------------------------------------------------------------------------8485enum Scope {86 Loop {87 label: Option<String>,88 continue_block: BlockId,89 break_block: BlockId,90 },91 Label {92 label: String,93 break_block: BlockId,94 },95 Switch {96 label: Option<String>,97 break_block: BlockId,98 },99}100101impl Scope {102 fn label(&self) -> Option<&str> {103 match self {104 Scope::Loop { label, .. } => label.as_deref(),105 Scope::Label { label, .. } => Some(label.as_str()),106 Scope::Switch { label, .. } => label.as_deref(),107 }108 }109110 fn break_block(&self) -> BlockId {111 match self {112 Scope::Loop { break_block, .. } => *break_block,113 Scope::Label { break_block, .. } => *break_block,114 Scope::Switch { break_block, .. } => *break_block,115 }116 }117}118119// ---------------------------------------------------------------------------120// WipBlock: a block under construction that does not yet have a terminal121// ---------------------------------------------------------------------------122123pub struct WipBlock {124 pub id: BlockId,125 pub instructions: Vec<InstructionId>,126 pub kind: BlockKind,127}128129fn new_block(id: BlockId, kind: BlockKind) -> WipBlock {130 WipBlock {131 id,132 kind,133 instructions: Vec::new(),134 }135}136137// ---------------------------------------------------------------------------138// HirBuilder: helper struct for constructing a CFG139// ---------------------------------------------------------------------------140141pub struct HirBuilder<'a> {142 completed: IndexMap<BlockId, BasicBlock, FxBuildHasher>,143 current: WipBlock,144 entry: BlockId,145 scopes: Vec<Scope>,146 /// Context identifiers: variables captured from an outer scope.147 /// Maps the outer scope's BindingId to the source location where it was referenced.148 context: IndexMap<BindingId, Option<SourceLocation>, FxBuildHasher>,149 /// Resolved bindings: maps a BindingId to the HIR IdentifierId created for it.150 bindings: IndexMap<BindingId, IdentifierId, FxBuildHasher>,151 /// Names already used by bindings, for collision avoidance.152 /// Maps name string -> how many times it has been used (for appending _0, _1, ...).153 used_names: IndexMap<String, BindingId, FxBuildHasher>,154 env: &'a mut Environment,155 scope_info: &'a ScopeInfo,156 exception_handler_stack: Vec<BlockId>,157 /// Flat instruction table being built up.158 instruction_table: Vec<Instruction>,159 /// Traversal context: counts the number of `fbt` tag parents160 /// of the current babel node.161 pub fbt_depth: u32,162 /// The scope of the function being compiled (for context identifier checks).163 function_scope: ScopeId,164 /// The scope of the outermost component/hook function (for gather_captured_context).165 component_scope: ScopeId,166 /// Set of BindingIds for variables declared in scopes between component_scope167 /// and any inner function scope, that are referenced from an inner function scope.168 /// These need StoreContext/LoadContext instead of StoreLocal/LoadLocal.169 context_identifiers: rustc_hash::FxHashSet<BindingId>,170 /// Set of ScopeIds that have been matched to synthetic blocks/functions.171 /// Prevents the same scope from being reused for different synthetic nodes.172 claimed_synthetic_scopes: rustc_hash::FxHashSet<ScopeId>,173 /// Index mapping identifier byte offsets to source locations and JSX status.174 identifier_locs: &'a IdentifierLocIndex,175}176177impl<'a> HirBuilder<'a> {178 // -----------------------------------------------------------------------179 // M2: Core methods180 // -----------------------------------------------------------------------181182 /// Create a new HirBuilder.183 ///184 /// - `env`: the shared environment (counters, arenas, error accumulator)185 /// - `scope_info`: the scope information from the AST186 /// - `function_scope`: the ScopeId of the function being compiled187 /// - `bindings`: optional pre-existing bindings (e.g., from a parent function)188 /// - `context`: optional pre-existing captured context map189 /// - `entry_block_kind`: the kind of the entry block (defaults to `Block`)190 pub fn new(191 env: &'a mut Environment,192 scope_info: &'a ScopeInfo,193 function_scope: ScopeId,194 component_scope: ScopeId,195 context_identifiers: rustc_hash::FxHashSet<BindingId>,196 bindings: Option<IndexMap<BindingId, IdentifierId, FxBuildHasher>>,197 context: Option<IndexMap<BindingId, Option<SourceLocation>, FxBuildHasher>>,198 entry_block_kind: Option<BlockKind>,199 used_names: Option<IndexMap<String, BindingId, FxBuildHasher>>,200 identifier_locs: &'a IdentifierLocIndex,201 ) -> Self {202 let entry = env.next_block_id();203 let kind = entry_block_kind.unwrap_or(BlockKind::Block);204 HirBuilder {205 completed: IndexMap::default(),206 current: new_block(entry, kind),207 entry,208 scopes: Vec::new(),209 context: context.unwrap_or_default(),210 bindings: bindings.unwrap_or_default(),211 used_names: used_names.unwrap_or_default(),212 env,213 scope_info,214 exception_handler_stack: Vec::new(),215 instruction_table: Vec::new(),216 fbt_depth: 0,217 function_scope,218 component_scope,219 context_identifiers,220 claimed_synthetic_scopes: rustc_hash::FxHashSet::default(),221 identifier_locs,222 }223 }224225 /// Check if a scope is the component scope or a descendant of it.226 /// Used to determine whether a binding is local to the compiled function227 /// or belongs to an ancestor function scope (e.g., a factory function228 /// wrapping a nested component declaration).229 /// Uses component_scope (the outermost compiled function's scope) rather230 /// than function_scope because inner function expressions within the231 /// compiled function have their own function_scope but still consider232 /// the outer component's variables as local.233 fn is_scope_within_compiled_function(&self, scope_id: ScopeId) -> bool {234 let mut current = Some(scope_id);235 while let Some(id) = current {236 if id == self.component_scope {237 return true;238 }239 current = self.scope_info.scopes[id.0 as usize].parent;240 }241 false242 }243244 /// Access the environment.245 pub fn environment(&self) -> &Environment {246 self.env247 }248249 /// Access the environment mutably.250 pub fn environment_mut(&mut self) -> &mut Environment {251 self.env252 }253254 /// Create a new unique TypeVar type, allocated from the environment's type arena255 /// so that TypeIds are consistent with identifier type slots.256 pub fn make_type(&mut self) -> Type {257 let type_id = self.env.make_type();258 Type::TypeVar { id: type_id }259 }260261 /// Access the scope info.262 pub fn scope_info(&self) -> &ScopeInfo {263 self.scope_info264 }265266 /// Look up the source location of an identifier by its node_id.267 pub fn get_identifier_loc(&self, node_id: u32) -> Option<SourceLocation> {268 self.identifier_locs269 .get(&node_id)270 .map(|entry| entry.loc.clone())271 }272273 /// Check whether a reference at the given byte offset corresponds to a274 /// JSXIdentifier. Scans the node_id-keyed index for an entry whose stored275 /// `start` matches the offset.276 pub fn is_jsx_identifier_at_pos(&self, offset: u32) -> bool {277 self.identifier_locs278 .values()279 .any(|entry| entry.start == offset && entry.is_jsx)280 }281282 /// Access the function scope (the scope of the function being compiled).283 pub fn function_scope(&self) -> ScopeId {284 self.function_scope285 }286287 /// Access the component scope.288 pub fn component_scope(&self) -> ScopeId {289 self.component_scope290 }291292 /// Access the context map.293 pub fn context(&self) -> &IndexMap<BindingId, Option<SourceLocation>, FxBuildHasher> {294 &self.context295 }296297 /// Access the pre-computed context identifiers set.298 pub fn context_identifiers(&self) -> &rustc_hash::FxHashSet<BindingId> {299 &self.context_identifiers300 }301302 /// Add a binding to the context identifiers set (used by hoisting).303 pub fn add_context_identifier(&mut self, binding_id: BindingId) {304 self.context_identifiers.insert(binding_id);305 }306307 pub fn claim_synthetic_scope(&mut self, scope_id: ScopeId) {308 self.claimed_synthetic_scopes.insert(scope_id);309 }310311 pub fn is_synthetic_scope_claimed(&self, scope_id: ScopeId) -> bool {312 self.claimed_synthetic_scopes.contains(&scope_id)313 }314315 /// Access scope_info and environment mutably at the same time.316 /// This is safe because they are disjoint fields, but Rust's borrow checker317 /// can't prove this through method calls alone.318 pub fn scope_info_and_env_mut(&mut self) -> (&ScopeInfo, &mut Environment) {319 (self.scope_info, self.env)320 }321322 /// Access the identifier location index.323 /// Returns the 'a reference to avoid conflicts with mutable borrows on self.324 pub fn identifier_locs(&self) -> &'a IdentifierLocIndex {325 self.identifier_locs326 }327328 /// Access the bindings map.329 pub fn bindings(&self) -> &IndexMap<BindingId, IdentifierId, FxBuildHasher> {330 &self.bindings331 }332333 /// Access the used names map.334 pub fn used_names(&self) -> &IndexMap<String, BindingId, FxBuildHasher> {335 &self.used_names336 }337338 /// Merge used names from a child builder back into this builder.339 /// This ensures name deduplication works across function scopes.340 pub fn merge_used_names(341 &mut self,342 child_used_names: IndexMap<String, BindingId, FxBuildHasher>,343 ) {344 for (name, binding_id) in child_used_names {345 self.used_names.entry(name).or_insert(binding_id);346 }347 }348349 /// Merge bindings (binding_id -> IdentifierId) from a child builder back into this builder.350 /// This matches TS behavior where parent and child share the same #bindings map by reference,351 /// so bindings resolved by the child are automatically visible to the parent.352 pub fn merge_bindings(353 &mut self,354 child_bindings: IndexMap<BindingId, IdentifierId, FxBuildHasher>,355 ) {356 for (binding_id, identifier_id) in child_bindings {357 self.bindings.entry(binding_id).or_insert(identifier_id);358 }359 }360361 /// Push an instruction onto the current block.362 ///363 /// Adds the instruction to the flat instruction table and records364 /// its InstructionId in the current block's instruction list.365 ///366 /// If an exception handler is active, also emits a MaybeThrow terminal367 /// after the instruction to model potential control flow to the handler,368 /// then continues in a new block.369 pub fn push(&mut self, instruction: Instruction) {370 let loc = instruction.loc.clone();371 let instr_id = InstructionId(self.instruction_table.len() as u32);372 self.instruction_table.push(instruction);373 self.current.instructions.push(instr_id);374375 if let Some(&handler) = self.exception_handler_stack.last() {376 let continuation = self.reserve(self.current_block_kind());377 self.terminate_with_continuation(378 Terminal::MaybeThrow {379 continuation: continuation.id,380 handler: Some(handler),381 id: EvaluationOrder(0),382 loc,383 effects: None,384 },385 continuation,386 );387 }388 }389390 /// Terminate the current block with the given terminal and start a new block.391 ///392 /// If `next_block_kind` is `Some`, a new current block is created with that kind.393 /// Returns the BlockId of the completed block.394 pub fn terminate(&mut self, terminal: Terminal, next_block_kind: Option<BlockKind>) -> BlockId {395 // The placeholder block created here (BlockId(u32::MAX)) is only used when396 // next_block_kind is None, meaning this is the final terminate() call.397 // It will never be read or completed because build() consumes self398 // immediately after, and no further operations should occur on the builder.399 let wip = std::mem::replace(400 &mut self.current,401 new_block(BlockId(u32::MAX), BlockKind::Block),402 );403 let block_id = wip.id;404405 self.completed.insert(406 block_id,407 BasicBlock {408 kind: wip.kind,409 id: block_id,410 instructions: wip.instructions,411 terminal,412 preds: IndexSet::default(),413 phis: Vec::new(),414 },415 );416417 if let Some(kind) = next_block_kind {418 let next_id = self.env.next_block_id();419 self.current = new_block(next_id, kind);420 }421 block_id422 }423424 /// Terminate the current block with the given terminal, and set425 /// a previously reserved block as the new current block.426 pub fn terminate_with_continuation(&mut self, terminal: Terminal, continuation: WipBlock) {427 let wip = std::mem::replace(&mut self.current, continuation);428 let block_id = wip.id;429 self.completed.insert(430 block_id,431 BasicBlock {432 kind: wip.kind,433 id: block_id,434 instructions: wip.instructions,435 terminal,436 preds: IndexSet::default(),437 phis: Vec::new(),438 },439 );440 }441442 /// Reserve a new block so it can be referenced before construction.443 /// Use `terminate_with_continuation()` to make it current, or `complete()` to444 /// save it directly.445 pub fn reserve(&mut self, kind: BlockKind) -> WipBlock {446 let id = self.env.next_block_id();447 new_block(id, kind)448 }449450 /// Save a previously reserved block as completed with the given terminal.451 pub fn complete(&mut self, block: WipBlock, terminal: Terminal) {452 let block_id = block.id;453 self.completed.insert(454 block_id,455 BasicBlock {456 kind: block.kind,457 id: block_id,458 instructions: block.instructions,459 terminal,460 preds: IndexSet::default(),461 phis: Vec::new(),462 },463 );464 }465466 /// Sets the given wip block as current, executes the closure to populate467 /// it and obtain its terminal, then completes the block and restores the468 /// previous current block.469 pub fn enter_reserved(&mut self, wip: WipBlock, f: impl FnOnce(&mut Self) -> Terminal) {470 let prev = std::mem::replace(&mut self.current, wip);471 let terminal = f(self);472 let completed_wip = std::mem::replace(&mut self.current, prev);473 self.completed.insert(474 completed_wip.id,475 BasicBlock {476 kind: completed_wip.kind,477 id: completed_wip.id,478 instructions: completed_wip.instructions,479 terminal,480 preds: IndexSet::default(),481 phis: Vec::new(),482 },483 );484 }485486 /// Like `enter_reserved`, but the closure returns a `Result<Terminal, CompilerDiagnostic>`.487 pub fn try_enter_reserved(488 &mut self,489 wip: WipBlock,490 f: impl FnOnce(&mut Self) -> Result<Terminal, CompilerDiagnostic>,491 ) -> Result<(), CompilerDiagnostic> {492 let prev = std::mem::replace(&mut self.current, wip);493 let terminal = f(self)?;494 let completed_wip = std::mem::replace(&mut self.current, prev);495 self.completed.insert(496 completed_wip.id,497 BasicBlock {498 kind: completed_wip.kind,499 id: completed_wip.id,500 instructions: completed_wip.instructions,501 terminal,502 preds: IndexSet::default(),503 phis: Vec::new(),504 },505 );506 Ok(())507 }508509 /// Create a new block, set it as current, run the closure to populate it510 /// and obtain its terminal, complete the block, and restore the previous511 /// current block. Returns the new block's BlockId.512 pub fn enter(513 &mut self,514 kind: BlockKind,515 f: impl FnOnce(&mut Self, BlockId) -> Terminal,516 ) -> BlockId {517 let wip = self.reserve(kind);518 let wip_id = wip.id;519 self.enter_reserved(wip, |this| f(this, wip_id));520 wip_id521 }522523 /// Like `enter`, but the closure returns a `Result<Terminal, CompilerDiagnostic>`.524 pub fn try_enter(525 &mut self,526 kind: BlockKind,527 f: impl FnOnce(&mut Self, BlockId) -> Result<Terminal, CompilerDiagnostic>,528 ) -> Result<BlockId, CompilerDiagnostic> {529 let wip = self.reserve(kind);530 let wip_id = wip.id;531 self.try_enter_reserved(wip, |this| f(this, wip_id))?;532 Ok(wip_id)533 }534535 /// Push an exception handler, run the closure, then pop the handler.536 pub fn enter_try_catch(&mut self, handler: BlockId, f: impl FnOnce(&mut Self)) {537 self.exception_handler_stack.push(handler);538 f(self);539 self.exception_handler_stack.pop();540 }541542 /// Like `enter_try_catch`, but the closure returns a `Result`.543 pub fn try_enter_try_catch(544 &mut self,545 handler: BlockId,546 f: impl FnOnce(&mut Self) -> Result<(), CompilerDiagnostic>,547 ) -> Result<(), CompilerDiagnostic> {548 self.exception_handler_stack.push(handler);549 let result = f(self);550 self.exception_handler_stack.pop();551 result552 }553554 /// Return the top of the exception handler stack, or None.555 pub fn resolve_throw_handler(&self) -> Option<BlockId> {556 self.exception_handler_stack.last().copied()557 }558559 /// Push a Loop scope, run the closure, pop and verify.560 pub fn loop_scope<T>(561 &mut self,562 label: Option<String>,563 continue_block: BlockId,564 break_block: BlockId,565 f: impl FnOnce(&mut Self) -> Result<T, CompilerDiagnostic>,566 ) -> Result<T, CompilerDiagnostic> {567 self.scopes.push(Scope::Loop {568 label: label.clone(),569 continue_block,570 break_block,571 });572 let value = f(self)?;573 let last = self574 .scopes575 .pop()576 .expect("Mismatched loop scope: stack empty");577 match &last {578 Scope::Loop {579 label: l,580 continue_block: c,581 break_block: b,582 } => {583 assert!(584 *l == label && *c == continue_block && *b == break_block,585 "Mismatched loop scope"586 );587 }588 _ => {589 return Err(CompilerDiagnostic::new(590 ErrorCategory::Invariant,591 "Mismatched loop scope: expected Loop, got other",592 None,593 ));594 }595 }596 Ok(value)597 }598599 /// Push a Label scope, run the closure, pop and verify.600 pub fn label_scope<T>(601 &mut self,602 label: String,603 break_block: BlockId,604 f: impl FnOnce(&mut Self) -> Result<T, CompilerDiagnostic>,605 ) -> Result<T, CompilerDiagnostic> {606 self.scopes.push(Scope::Label {607 label: label.clone(),608 break_block,609 });610 let value = f(self)?;611 let last = self612 .scopes613 .pop()614 .expect("Mismatched label scope: stack empty");615 match &last {616 Scope::Label {617 label: l,618 break_block: b,619 } => {620 assert!(*l == label && *b == break_block, "Mismatched label scope");621 }622 _ => {623 return Err(CompilerDiagnostic::new(624 ErrorCategory::Invariant,625 "Mismatched label scope: expected Label, got other",626 None,627 ));628 }629 }630 Ok(value)631 }632633 /// Push a Switch scope, run the closure, pop and verify.634 pub fn switch_scope<T>(635 &mut self,636 label: Option<String>,637 break_block: BlockId,638 f: impl FnOnce(&mut Self) -> Result<T, CompilerDiagnostic>,639 ) -> Result<T, CompilerDiagnostic> {640 self.scopes.push(Scope::Switch {641 label: label.clone(),642 break_block,643 });644 let value = f(self)?;645 let last = self646 .scopes647 .pop()648 .expect("Mismatched switch scope: stack empty");649 match &last {650 Scope::Switch {651 label: l,652 break_block: b,653 } => {654 assert!(*l == label && *b == break_block, "Mismatched switch scope");655 }656 _ => {657 return Err(CompilerDiagnostic::new(658 ErrorCategory::Invariant,659 "Mismatched switch scope: expected Switch, got other",660 None,661 ));662 }663 }664 Ok(value)665 }666667 /// Look up the break target for the given label (or the innermost668 /// loop/switch if label is None).669 pub fn lookup_break(&self, label: Option<&str>) -> Result<BlockId, CompilerDiagnostic> {670 for scope in self.scopes.iter().rev() {671 match scope {672 Scope::Loop { .. } | Scope::Switch { .. } if label.is_none() => {673 return Ok(scope.break_block());674 }675 _ if label.is_some() && scope.label() == label => {676 return Ok(scope.break_block());677 }678 _ => continue,679 }680 }681 Err(CompilerDiagnostic::new(682 ErrorCategory::Invariant,683 "Expected a loop or switch to be in scope for break",684 None,685 ))686 }687688 /// Look up the continue target for the given label (or the innermost689 /// loop if label is None). Only loops support continue.690 pub fn lookup_continue(&self, label: Option<&str>) -> Result<BlockId, CompilerDiagnostic> {691 for scope in self.scopes.iter().rev() {692 match scope {693 Scope::Loop {694 label: scope_label,695 continue_block,696 ..697 } => {698 if label.is_none() || label == scope_label.as_deref() {699 return Ok(*continue_block);700 }701 }702 _ => {703 if label.is_some() && scope.label() == label {704 return Err(CompilerDiagnostic::new(705 ErrorCategory::Invariant,706 "Continue may only refer to a labeled loop",707 None,708 ));709 }710 }711 }712 }713 Err(CompilerDiagnostic::new(714 ErrorCategory::Invariant,715 "Expected a loop to be in scope for continue",716 None,717 ))718 }719720 /// Create a temporary identifier with a fresh id, returning its IdentifierId.721 pub fn make_temporary(&mut self, loc: Option<SourceLocation>) -> IdentifierId {722 let id = self.env.next_identifier_id();723 // Update the loc on the allocated identifier724 self.env.identifiers[id.0 as usize].loc = loc;725 id726 }727728 /// Set the source location for an identifier.729 pub fn set_identifier_loc(&mut self, id: IdentifierId, loc: Option<SourceLocation>) {730 self.env.identifiers[id.0 as usize].loc = loc;731 }732733 /// Record an error on the environment.734 /// Returns `Err` for Invariant errors (matching TS throw behavior).735 pub fn record_error(&mut self, error: CompilerErrorDetail) -> Result<(), CompilerError> {736 self.env.record_error(error)737 }738739 /// Record a diagnostic on the environment.740 pub fn record_diagnostic(&mut self, diagnostic: CompilerDiagnostic) {741 self.env.record_diagnostic(diagnostic);742 }743744 /// Check if a name has a local binding (non-module-level).745 /// This is used for checking if fbt/fbs JSX tags are local bindings746 /// (which is not supported).747 pub fn has_local_binding(&self, name: &str) -> bool {748 if let Some(binding) = self749 .scope_info750 .find_binding_in_descendants(name, self.component_scope)751 {752 // When component_scope == program_scope (e2e path where scope info753 // is extracted from the function itself), any binding found is local.754 if self.component_scope == self.scope_info.program_scope {755 return true;756 }757 return binding.scope != self.scope_info.program_scope;758 }759 false760 }761762 /// Return the kind of the current block.763 pub fn current_block_kind(&self) -> BlockKind {764 self.current.kind765 }766767 /// Construct the final HIR and instruction table from the completed blocks.768 ///769 /// Performs these post-build passes:770 /// 1. Reverse-postorder sort + unreachable block removal771 /// 2. Check for unreachable blocks containing FunctionExpression instructions772 /// 3. Remove unreachable for-loop updates773 /// 4. Remove dead do-while statements774 /// 5. Remove unnecessary try-catch775 /// 6. Number all instructions and terminals776 /// 7. Mark predecessor blocks777 pub fn build(778 mut self,779 ) -> Result<780 (781 HIR,782 Vec<Instruction>,783 IndexMap<String, BindingId, FxBuildHasher>,784 IndexMap<BindingId, IdentifierId, FxBuildHasher>,785 ),786 CompilerError,787 > {788 let mut hir = HIR {789 blocks: std::mem::take(&mut self.completed),790 entry: self.entry,791 };792793 let mut instructions = std::mem::take(&mut self.instruction_table);794795 let rpo_blocks = get_reverse_postordered_blocks(&hir, &instructions);796797 // Check for unreachable blocks that contain FunctionExpression instructions.798 // These could contain hoisted declarations that we can't safely remove.799 for (id, block) in &hir.blocks {800 if !rpo_blocks.contains_key(id) {801 let has_function_expr = block.instructions.iter().any(|&instr_id| {802 matches!(803 instructions[instr_id.0 as usize].value,804 InstructionValue::FunctionExpression { .. }805 )806 });807 if has_function_expr {808 let loc = block809 .instructions810 .first()811 .and_then(|&i| instructions[i.0 as usize].loc.clone())812 .or_else(|| block.terminal.loc().copied());813 self.env.record_error(CompilerErrorDetail {814 category: ErrorCategory::Todo,815 reason: "Support functions with unreachable code that may contain hoisted declarations".to_string(),816 description: None,817 loc,818 suggestions: None,819 })?;820 }821 }822 }823824 hir.blocks = rpo_blocks;825826 remove_unreachable_for_updates(&mut hir);827 remove_dead_do_while_statements(&mut hir);828 remove_unnecessary_try_catch(&mut hir);829 mark_instruction_ids(&mut hir, &mut instructions);830 mark_predecessors(&mut hir);831832 let used_names = self.used_names;833 let bindings = self.bindings;834 Ok((hir, instructions, used_names, bindings))835 }836837 // -----------------------------------------------------------------------838 // M3: Binding resolution methods839 // -----------------------------------------------------------------------840841 /// Map a BindingId to an HIR IdentifierId.842 ///843 /// On first encounter, creates a new Identifier with the given name and a fresh id.844 /// On subsequent encounters, returns the cached IdentifierId.845 /// Handles name collisions by appending `_0`, `_1`, etc.846 ///847 /// Records errors for variables named 'fbt' or 'this'.848 pub fn resolve_binding(849 &mut self,850 name: &str,851 binding_id: BindingId,852 ) -> Result<IdentifierId, CompilerError> {853 self.resolve_binding_with_loc(name, binding_id, None)854 }855856 /// Map a BindingId to an HIR IdentifierId, with an optional source location.857 pub fn resolve_binding_with_loc(858 &mut self,859 name: &str,860 binding_id: BindingId,861 loc: Option<SourceLocation>,862 ) -> Result<IdentifierId, CompilerError> {863 // Check for unsupported names BEFORE the cache check.864 // In TS, resolveBinding records fbt errors when node.name === 'fbt'. After a name collision865 // causes a rename (e.g., "fbt" -> "fbt_0"), TS's scope.rename changes the AST node's name,866 // preventing subsequent fbt error recording. We simulate this by checking whether the867 // resolved name for this binding is still "fbt" (not renamed to "fbt_0" etc.).868 if name == "fbt" {869 // Check if this binding was previously resolved to a renamed version870 let should_record_fbt_error =871 if let Some(&identifier_id) = self.bindings.get(&binding_id) {872 // Already resolved - check if the resolved name is still "fbt"873 match &self.env.identifiers[identifier_id.0 as usize].name {874 Some(IdentifierName::Named(resolved_name)) => resolved_name == "fbt",875 _ => false,876 }877 } else {878 // First resolution - always record879 true880 };881 if should_record_fbt_error {882 let error_loc = self.scope_info.bindings[binding_id.0 as usize]883 .declaration_node_id884 .and_then(|nid| self.get_identifier_loc(nid))885 .or_else(|| loc.clone());886 self.env.record_error(CompilerErrorDetail {887 category: ErrorCategory::Todo,888 reason: "Support local variables named `fbt`".to_string(),889 description: Some(890 "Local variables named `fbt` may conflict with the fbt plugin and are not yet supported".to_string(),891 ),892 loc: error_loc,893 suggestions: None,894 })?;895 }896 }897898 // If we've already resolved this binding, return the cached IdentifierId899 if let Some(&identifier_id) = self.bindings.get(&binding_id) {900 return Ok(identifier_id);901 }902903 if is_always_reserved_word(name) {904 // Match TS behavior: makeIdentifierName throws for reserved words.905 return Err(CompilerError::from(reserved_identifier_diagnostic(name)));906 }907908 // Find a unique name: start with the original name, then try name_0, name_1, ...909 let mut candidate = name.to_string();910 let mut index = 0u32;911 loop {912 if let Some(&existing_binding_id) = self.used_names.get(&candidate) {913 if existing_binding_id == binding_id {914 // Same binding, use this name915 break;916 }917 // Name collision with a different binding, try the next suffix918 candidate = format!("{}_{}", name, index);919 index += 1;920 } else {921 // Name is available922 break;923 }924 }925926 // Record rename if the candidate differs from the original name927 if candidate != name {928 let binding = &self.scope_info.bindings[binding_id.0 as usize];929 if let Some(decl_start) = binding.declaration_start {930 self.env931 .renames932 .push(react_compiler_hir::environment::BindingRename {933 original: name.to_string(),934 renamed: candidate.clone(),935 declaration_start: decl_start,936 });937 }938 }939940 // Allocate identifier in the arena941 let id = self.env.next_identifier_id();942 // Update the name and loc on the allocated identifier943 self.env.identifiers[id.0 as usize].name = Some(IdentifierName::Named(candidate.clone()));944 // Prefer the binding's declaration loc over the reference loc.945 // This matches TS behavior where Babel's resolveBinding returns the946 // binding identifier's original loc (the declaration site).947 let binding = &self.scope_info.bindings[binding_id.0 as usize];948 let decl_loc = binding949 .declaration_node_id950 .and_then(|nid| self.get_identifier_loc(nid));951 if let Some(ref dl) = decl_loc {952 self.env.identifiers[id.0 as usize].loc = Some(dl.clone());953 } else if let Some(ref loc) = loc {954 self.env.identifiers[id.0 as usize].loc = Some(loc.clone());955 }956957 self.used_names.insert(candidate, binding_id);958 self.bindings.insert(binding_id, id);959 Ok(id)960 }961962 /// Set the loc on an identifier to the declaration-site loc.963 /// This overrides any previously-set loc (which may have come from a reference site).964 pub fn set_identifier_declaration_loc(965 &mut self,966 id: IdentifierId,967 loc: &Option<SourceLocation>,968 ) {969 if let Some(loc_val) = loc {970 self.env.identifiers[id.0 as usize].loc = Some(loc_val.clone());971 }972 }973974 /// Resolve an identifier reference to a VariableBinding.975 ///976 /// Uses ScopeInfo to determine whether the reference is:977 /// - Global (no binding found)978 /// - ImportDefault, ImportSpecifier, ImportNamespace (program-scope import binding)979 /// - ModuleLocal (program-scope non-import binding)980 /// - Identifier (local binding, resolved via resolve_binding)981 pub fn resolve_identifier(982 &mut self,983 name: &str,984 _start_offset: u32,985 loc: Option<SourceLocation>,986 node_id: Option<u32>,987 ) -> Result<VariableBinding, CompilerError> {988 let binding_data = self.scope_info.resolve_reference_for_node(node_id);989990 match binding_data {991 None => {992 // No binding found: this is a global993 Ok(VariableBinding::Global {994 name: name.to_string(),995 })996 }997 Some(binding) => {998 // Treat type-only declarations as globals so the compiler999 // doesn't try to create/initialize HIR bindings for them.1000 // TSEnumDeclaration is included because enums inside function1001 // bodies are lowered as UnsupportedNode and their binding1002 // is never initialized in HIR.1003 if matches!(1004 binding.declaration_type.as_str(),1005 "TSTypeAliasDeclaration"1006 | "TSInterfaceDeclaration"1007 | "TSEnumDeclaration"1008 | "TSModuleDeclaration"1009 ) {1010 return Ok(VariableBinding::Global {1011 name: name.to_string(),1012 });1013 }1014 if binding.scope == self.scope_info.program_scope {1015 // Module-level binding: check import info1016 Ok(match &binding.import {1017 Some(import_info) => match import_info.kind {1018 ImportBindingKind::Default => VariableBinding::ImportDefault {1019 name: name.to_string(),1020 module: import_info.source.clone(),1021 },1022 ImportBindingKind::Named => VariableBinding::ImportSpecifier {1023 name: name.to_string(),1024 module: import_info.source.clone(),1025 imported: import_info1026 .imported1027 .clone()1028 .unwrap_or_else(|| name.to_string()),1029 },1030 ImportBindingKind::Namespace => VariableBinding::ImportNamespace {1031 name: name.to_string(),1032 module: import_info.source.clone(),1033 },1034 },1035 None => VariableBinding::ModuleLocal {1036 name: name.to_string(),1037 },1038 })1039 } else if !self.is_scope_within_compiled_function(binding.scope) {1040 Ok(VariableBinding::ModuleLocal {1041 name: name.to_string(),1042 })1043 } else {1044 let binding_id = binding.id;1045 let binding_kind = crate::convert_binding_kind(&binding.kind);1046 let identifier_id = self.resolve_binding_with_loc(name, binding_id, loc)?;1047 Ok(VariableBinding::Identifier {1048 identifier: identifier_id,1049 binding_kind,1050 })1051 }1052 }1053 }1054 }10551056 /// Check if an identifier reference resolves to a context identifier.1057 ///1058 /// A context identifier is a variable declared in an ancestor scope of the1059 /// current function's scope, but NOT in the program scope itself and NOT1060 /// in the function's own scope. These are "captured" variables from an1061 /// enclosing function.1062 pub fn is_context_identifier(1063 &self,1064 _name: &str,1065 _start_offset: u32,1066 node_id: Option<u32>,1067 ) -> bool {1068 let binding = self.scope_info.resolve_reference_for_node(node_id);10691070 match binding {1071 None => false,1072 Some(binding_data) => {1073 if binding_data.scope == self.scope_info.program_scope {1074 return false;1075 }1076 self.context_identifiers.contains(&binding_data.id)1077 }1078 }1079 }10801081 /// Like `is_context_identifier`, for callers that already resolved a1082 /// BindingId instead of going through a reference node.1083 pub fn is_context_binding(&self, binding_id: BindingId) -> bool {1084 let binding = &self.scope_info.bindings[binding_id.0 as usize];1085 if binding.scope == self.scope_info.program_scope {1086 return false;1087 }1088 self.context_identifiers.contains(&binding_id)1089 }10901091 /// Resolve the binding for a function declaration's id the way TS does:1092 /// Babel's `path.scope.getBinding(name)` starts at the function's OWN1093 /// scope, so a body-level local (or parameter) that shadows the function's1094 /// name resolves to that inner binding rather than to the function's1095 /// hoisted binding in the parent scope.1096 ///1097 /// Babel's `scope.rename` re-keys a scope's bindings when the TS builder1098 /// renames a shadowed binding (e.g. `init` -> `init_0`), so a binding only1099 /// matches if its *current* name — the resolved HIR identifier name once1100 /// resolved — still equals `name`. A binding renamed *to* `name` overwrites1101 /// the original key in Babel and takes precedence over an unresolved1102 /// binding with that original name.1103 ///1104 /// Returns None when the walk resolves outside the compiled function1105 /// (degraded scope info); callers should fall back to node-based1106 /// resolution in that case.1107 pub fn get_function_declaration_binding(1108 &self,1109 function_scope: ScopeId,1110 name: &str,1111 ) -> Option<BindingId> {1112 // None = unresolved binding; Some(matches) = resolved, current name comparison1113 let resolved_name_matches = |bid: BindingId| -> Option<bool> {1114 let &identifier_id = self.bindings.get(&bid)?;1115 match &self.env.identifiers[identifier_id.0 as usize].name {1116 Some(IdentifierName::Named(n)) => Some(n == name),1117 _ => Some(false),1118 }1119 };1120 let mut current = Some(function_scope);1121 while let Some(id) = current {1122 let scope = &self.scope_info.scopes[id.0 as usize];1123 let mut found = scope1124 .bindings1125 .values()1126 .copied()1127 .find(|&bid| resolved_name_matches(bid) == Some(true));1128 if found.is_none() {1129 if let Some(&bid) = scope.bindings.get(name) {1130 // Skip bindings that were renamed away from `name`.1131 if resolved_name_matches(bid) != Some(false) {1132 found = Some(bid);1133 }1134 }1135 }1136 if let Some(bid) = found {1137 let binding_scope = self.scope_info.bindings[bid.0 as usize].scope;1138 if !self.is_scope_within_compiled_function(binding_scope) {1139 return None;1140 }1141 return Some(bid);1142 }1143 current = scope.parent;1144 }1145 None1146 }1147}11481149// ---------------------------------------------------------------------------1150// Post-build helper functions1151// ---------------------------------------------------------------------------11521153/// Compute a reverse-postorder of blocks reachable from the entry.1154///1155/// Visits successors in reverse order so that when the postorder list is1156/// reversed, sibling edges appear in program order.1157///1158/// Blocks not reachable through successors are removed. Blocks that are1159/// only reachable as fallthroughs (not through real successor edges) are1160/// replaced with empty blocks that have an Unreachable terminal.1161pub fn get_reverse_postordered_blocks(1162 hir: &HIR,1163 _instructions: &[Instruction],1164) -> IndexMap<BlockId, BasicBlock, FxBuildHasher> {1165 let mut visited: IndexSet<BlockId, FxBuildHasher> = IndexSet::default();1166 let mut used: IndexSet<BlockId, FxBuildHasher> = IndexSet::default();1167 let mut used_fallthroughs: IndexSet<BlockId, FxBuildHasher> = IndexSet::default();1168 let mut postorder: Vec<BlockId> = Vec::new();11691170 fn visit(1171 hir: &HIR,1172 block_id: BlockId,1173 is_used: bool,1174 visited: &mut IndexSet<BlockId, FxBuildHasher>,1175 used: &mut IndexSet<BlockId, FxBuildHasher>,1176 used_fallthroughs: &mut IndexSet<BlockId, FxBuildHasher>,1177 postorder: &mut Vec<BlockId>,1178 ) {1179 let was_used = used.contains(&block_id);1180 let was_visited = visited.contains(&block_id);1181 visited.insert(block_id);1182 if is_used {1183 used.insert(block_id);1184 }1185 if was_visited && (was_used || !is_used) {1186 return;1187 }11881189 let block = hir1190 .blocks1191 .get(&block_id)1192 .unwrap_or_else(|| panic!("[HIRBuilder] expected block {:?} to exist", block_id));11931194 // Visit successors in reverse order so that when we reverse the1195 // postorder list, sibling edges come out in program order.1196 let mut successors = each_terminal_successor(&block.terminal);1197 successors.reverse();11981199 let fallthrough = terminal_fallthrough(&block.terminal);12001201 // Visit fallthrough first (marking as not-yet-used) to ensure its1202 // block ID is emitted in the correct position.1203 if let Some(ft) = fallthrough {1204 if is_used {1205 used_fallthroughs.insert(ft);1206 }1207 visit(hir, ft, false, visited, used, used_fallthroughs, postorder);1208 }1209 for successor in successors {1210 visit(1211 hir,1212 successor,1213 is_used,1214 visited,1215 used,1216 used_fallthroughs,1217 postorder,1218 );1219 }12201221 if !was_visited {1222 postorder.push(block_id);1223 }1224 }12251226 visit(1227 hir,1228 hir.entry,1229 true,1230 &mut visited,1231 &mut used,1232 &mut used_fallthroughs,1233 &mut postorder,1234 );12351236 let mut blocks = IndexMap::default();1237 for block_id in postorder.into_iter().rev() {1238 let block = hir.blocks.get(&block_id).unwrap();1239 if used.contains(&block_id) {1240 blocks.insert(block_id, block.clone());1241 } else if used_fallthroughs.contains(&block_id) {1242 blocks.insert(1243 block_id,1244 BasicBlock {1245 kind: block.kind,1246 id: block_id,1247 instructions: Vec::new(),1248 terminal: Terminal::Unreachable {1249 id: block.terminal.evaluation_order(),1250 loc: block.terminal.loc().copied(),1251 },1252 preds: block.preds.clone(),1253 phis: Vec::new(),1254 },1255 );1256 }1257 // otherwise this block is unreachable and is dropped1258 }12591260 blocks1261}12621263/// For each block with a `For` terminal whose update block is not in the1264/// blocks map, set update to None.1265pub fn remove_unreachable_for_updates(hir: &mut HIR) {1266 let block_ids: IndexSet<BlockId, FxBuildHasher> = hir.blocks.keys().copied().collect();1267 for block in hir.blocks.values_mut() {1268 if let Terminal::For { update, .. } = &mut block.terminal {1269 if let Some(update_id) = *update {1270 if !block_ids.contains(&update_id) {1271 *update = None;1272 }1273 }1274 }1275 }1276}12771278/// For each block with a `DoWhile` terminal whose test block is not in1279/// the blocks map, replace the terminal with a Goto to the loop block.1280pub fn remove_dead_do_while_statements(hir: &mut HIR) {1281 let block_ids: IndexSet<BlockId, FxBuildHasher> = hir.blocks.keys().copied().collect();1282 for block in hir.blocks.values_mut() {1283 let should_replace = if let Terminal::DoWhile { test, .. } = &block.terminal {1284 !block_ids.contains(test)1285 } else {1286 false1287 };1288 if should_replace {1289 if let Terminal::DoWhile {1290 loop_block,1291 id,1292 loc,1293 ..1294 } = std::mem::replace(1295 &mut block.terminal,1296 Terminal::Unreachable {1297 id: EvaluationOrder(0),1298 loc: None,1299 },1300 ) {1301 block.terminal = Terminal::Goto {1302 block: loop_block,1303 variant: GotoVariant::Break,1304 id,1305 loc,1306 };1307 }1308 }1309 }1310}13111312/// For each block with a `Try` terminal whose handler block is not in1313/// the blocks map, replace the terminal with a Goto to the try block.1314///1315/// Also cleans up the fallthrough block's predecessors if the handler1316/// was the only path to it.1317pub fn remove_unnecessary_try_catch(hir: &mut HIR) {1318 let block_ids: IndexSet<BlockId, FxBuildHasher> = hir.blocks.keys().copied().collect();13191320 // Collect the blocks that need replacement and their associated data1321 let replacements: Vec<(BlockId, BlockId, BlockId, BlockId, Option<SourceLocation>)> = hir1322 .blocks1323 .iter()1324 .filter_map(|(&block_id, block)| {1325 if let Terminal::Try {1326 block: try_block,1327 handler,1328 fallthrough,1329 loc,1330 ..1331 } = &block.terminal1332 {1333 if !block_ids.contains(handler) {1334 return Some((block_id, *try_block, *handler, *fallthrough, loc.clone()));1335 }1336 }1337 None1338 })1339 .collect();13401341 for (block_id, try_block, handler_id, fallthrough_id, loc) in replacements {1342 // Replace the terminal1343 if let Some(block) = hir.blocks.get_mut(&block_id) {1344 block.terminal = Terminal::Goto {1345 block: try_block,1346 id: EvaluationOrder(0),1347 loc,1348 variant: GotoVariant::Break,1349 };1350 }13511352 // Clean up fallthrough predecessor info1353 if let Some(fallthrough) = hir.blocks.get_mut(&fallthrough_id) {1354 if fallthrough.preds.len() == 1 && fallthrough.preds.contains(&handler_id) {1355 // The handler was the only predecessor: remove the fallthrough block1356 hir.blocks.shift_remove(&fallthrough_id);1357 } else {1358 fallthrough.preds.shift_remove(&handler_id);1359 }1360 }1361 }1362}13631364/// Sequentially number all instructions and terminals starting from 1.1365pub fn mark_instruction_ids(hir: &mut HIR, instructions: &mut [Instruction]) {1366 let mut order: u32 = 0;1367 for block in hir.blocks.values_mut() {1368 for &instr_id in &block.instructions {1369 order += 1;1370 instructions[instr_id.0 as usize].id = EvaluationOrder(order);1371 }1372 order += 1;1373 block.terminal.set_evaluation_order(EvaluationOrder(order));1374 }1375}13761377/// DFS from entry, for each successor add the predecessor's id to1378/// the successor's preds set.1379///1380/// Note: This only visits direct successors (via `each_terminal_successor`),1381/// not fallthrough blocks. Fallthrough blocks are reached indirectly via1382/// Goto terminals from within branching blocks, matching the TypeScript1383/// `markPredecessors` behavior.1384pub fn mark_predecessors(hir: &mut HIR) {1385 // Clear all preds first1386 for block in hir.blocks.values_mut() {1387 block.preds.clear();1388 }13891390 let mut visited: IndexSet<BlockId, FxBuildHasher> = IndexSet::default();13911392 fn visit(1393 hir: &mut HIR,1394 block_id: BlockId,1395 prev_block_id: Option<BlockId>,1396 visited: &mut IndexSet<BlockId, FxBuildHasher>,1397 ) {1398 // Add predecessor1399 if let Some(prev_id) = prev_block_id {1400 if let Some(block) = hir.blocks.get_mut(&block_id) {1401 block.preds.insert(prev_id);1402 } else {1403 return;1404 }1405 }14061407 if visited.contains(&block_id) {1408 return;1409 }1410 visited.insert(block_id);14111412 // Get successors before mutating1413 let successors = if let Some(block) = hir.blocks.get(&block_id) {1414 each_terminal_successor(&block.terminal)1415 } else {1416 return;1417 };14181419 for successor in successors {1420 visit(hir, successor, Some(block_id), visited);1421 }1422 }14231424 visit(hir, hir.entry, None, &mut visited);1425}14261427// ---------------------------------------------------------------------------1428// Public helper functions1429// ---------------------------------------------------------------------------14301431/// Create a temporary Place with a fresh identifier allocated in the arena.1432pub fn create_temporary_place(env: &mut Environment, loc: Option<SourceLocation>) -> Place {1433 let id = env.next_identifier_id();1434 // Update the loc on the allocated identifier1435 env.identifiers[id.0 as usize].loc = loc;1436 Place {1437 identifier: id,1438 reactive: false,1439 effect: Effect::Unknown,1440 loc: None,1441 }1442}