Skip to main content

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