| 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 | //! Prunes `MaybeThrow` terminals for blocks that can provably never throw. |
| 7 | //! |
| 8 | //! Currently very conservative: only affects blocks with primitives or |
| 9 | //! array/object literals. Even a variable reference could throw due to TDZ. |
| 10 | //! |
| 11 | //! Analogous to TS `Optimization/PruneMaybeThrows.ts`. |
| 12 | |
| 13 | use rustc_hash::FxHashMap; |
| 14 | |
| 15 | use react_compiler_diagnostics::{ |
| 16 | CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, GENERATED_SOURCE, |
| 17 | }; |
| 18 | use react_compiler_hir::{BlockId, HirFunction, Instruction, InstructionValue, Terminal}; |
| 19 | use react_compiler_lowering::{ |
| 20 | get_reverse_postordered_blocks, mark_instruction_ids, remove_dead_do_while_statements, |
| 21 | remove_unnecessary_try_catch, remove_unreachable_for_updates, |
| 22 | }; |
| 23 | |
| 24 | use crate::merge_consecutive_blocks::merge_consecutive_blocks; |
| 25 | |
| 26 | /// Prune `MaybeThrow` terminals for blocks that cannot throw, then clean up the CFG. |
| 27 | pub fn prune_maybe_throws( |
| 28 | func: &mut HirFunction, |
| 29 | functions: &mut [HirFunction], |
| 30 | ) -> Result<(), CompilerDiagnostic> { |
| 31 | let terminal_mapping = prune_maybe_throws_impl(func); |
| 32 | if let Some(terminal_mapping) = terminal_mapping { |
| 33 | // If terminals have changed then blocks may have become newly unreachable. |
| 34 | // Re-run minification of the graph (incl reordering instruction ids). |
| 35 | func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions); |
| 36 | remove_unreachable_for_updates(&mut func.body); |
| 37 | remove_dead_do_while_statements(&mut func.body); |
| 38 | remove_unnecessary_try_catch(&mut func.body); |
| 39 | mark_instruction_ids(&mut func.body, &mut func.instructions); |
| 40 | merge_consecutive_blocks(func, functions); |
| 41 | |
| 42 | // Rewrite phi operands to reference the updated predecessor blocks |
| 43 | for block in func.body.blocks.values_mut() { |
| 44 | let preds = &block.preds; |
| 45 | let mut phi_updates: Vec<(usize, Vec<(BlockId, BlockId)>)> = Vec::new(); |
| 46 | |
| 47 | for (phi_idx, phi) in block.phis.iter().enumerate() { |
| 48 | let mut updates = Vec::new(); |
| 49 | for (predecessor, _) in &phi.operands { |
| 50 | if !preds.contains(predecessor) { |
| 51 | let mapped_terminal = |
| 52 | terminal_mapping.get(predecessor).copied().ok_or_else(|| { |
| 53 | CompilerDiagnostic::new( |
| 54 | ErrorCategory::Invariant, |
| 55 | "Expected non-existing phi operand's predecessor to have been mapped to a new terminal", |
| 56 | Some(format!( |
| 57 | "Could not find mapping for predecessor bb{} in block bb{}", |
| 58 | predecessor.0, block.id.0, |
| 59 | )), |
| 60 | ) |
| 61 | .with_detail(CompilerDiagnosticDetail::Error { |
| 62 | loc: GENERATED_SOURCE, |
| 63 | message: None, |
| 64 | identifier_name: None, |
| 65 | }) |
| 66 | })?; |
| 67 | updates.push((*predecessor, mapped_terminal)); |
| 68 | } |
| 69 | } |
| 70 | if !updates.is_empty() { |
| 71 | phi_updates.push((phi_idx, updates)); |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | for (phi_idx, updates) in phi_updates { |
| 76 | for (old_pred, new_pred) in updates { |
| 77 | let operand = block.phis[phi_idx] |
| 78 | .operands |
| 79 | .shift_remove(&old_pred) |
| 80 | .unwrap(); |
| 81 | block.phis[phi_idx].operands.insert(new_pred, operand); |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | Ok(()) |
| 87 | } |
| 88 | |
| 89 | fn prune_maybe_throws_impl(func: &mut HirFunction) -> Option<FxHashMap<BlockId, BlockId>> { |
| 90 | let mut terminal_mapping: FxHashMap<BlockId, BlockId> = FxHashMap::default(); |
| 91 | let instructions = &func.instructions; |
| 92 | |
| 93 | for block in func.body.blocks.values_mut() { |
| 94 | let continuation = match &block.terminal { |
| 95 | Terminal::MaybeThrow { continuation, .. } => *continuation, |
| 96 | _ => continue, |
| 97 | }; |
| 98 | |
| 99 | let can_throw = block |
| 100 | .instructions |
| 101 | .iter() |
| 102 | .any(|instr_id| instruction_may_throw(&instructions[instr_id.0 as usize])); |
| 103 | |
| 104 | if !can_throw { |
| 105 | let source = terminal_mapping.get(&block.id).copied().unwrap_or(block.id); |
| 106 | terminal_mapping.insert(continuation, source); |
| 107 | // Null out the handler rather than replacing with Goto. |
| 108 | // Preserving the MaybeThrow makes the continuations clear for |
| 109 | // BuildReactiveFunction, while nulling out the handler tells us |
| 110 | // that control cannot flow to the handler. |
| 111 | if let Terminal::MaybeThrow { handler, .. } = &mut block.terminal { |
| 112 | *handler = None; |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | if terminal_mapping.is_empty() { |
| 118 | None |
| 119 | } else { |
| 120 | Some(terminal_mapping) |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | fn instruction_may_throw(instr: &Instruction) -> bool { |
| 125 | match &instr.value { |
| 126 | InstructionValue::Primitive { .. } |
| 127 | | InstructionValue::ArrayExpression { .. } |
| 128 | | InstructionValue::ObjectExpression { .. } => false, |
| 129 | _ => true, |
| 130 | } |
| 131 | } |