Skip to main content

react_compiler_optimization/
dead_code_elimination.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//! Dead code elimination pass.
7//!
8//! Eliminates instructions whose values are unused, reducing generated code size.
9//! Performs mark-and-sweep analysis to identify and remove dead code while
10//! preserving side effects and program semantics.
11//!
12//! Ported from TypeScript `src/Optimization/DeadCodeElimination.ts`.
13
14use rustc_hash::FxHashSet;
15
16use react_compiler_hir::environment::{Environment, OutputMode};
17use react_compiler_hir::object_shape::HookKind;
18use react_compiler_hir::visitors;
19use react_compiler_hir::{
20    ArrayPatternElement, BlockId, BlockKind, HirFunction, IdentifierId, InstructionKind,
21    InstructionValue, ObjectPropertyOrSpread, Pattern,
22};
23
24/// Implements dead-code elimination, eliminating instructions whose values are unused.
25///
26/// Note that unreachable blocks are already pruned during HIR construction.
27///
28/// Corresponds to TS `deadCodeElimination(fn: HIRFunction): void`.
29pub fn dead_code_elimination(func: &mut HirFunction, env: &Environment) {
30    // Phase 1: Find/mark all referenced identifiers
31    let state = find_referenced_identifiers(func, env);
32
33    // Phase 2: Prune / sweep unreferenced identifiers and instructions
34    // Collect instructions to rewrite (two-phase: collect then apply to avoid borrow conflicts)
35    let mut instructions_to_rewrite: Vec<react_compiler_hir::InstructionId> = Vec::new();
36
37    for (_block_id, block) in &mut func.body.blocks {
38        // Remove unused phi nodes
39        block
40            .phis
41            .retain(|phi| is_id_or_name_used(&state, &env.identifiers, phi.place.identifier));
42
43        // Remove instructions with unused lvalues
44        block.instructions.retain(|instr_id| {
45            let instr = &func.instructions[instr_id.0 as usize];
46            is_id_or_name_used(&state, &env.identifiers, instr.lvalue.identifier)
47        });
48
49        // Collect instructions that need rewriting (not the block value)
50        let retained_count = block.instructions.len();
51        for i in 0..retained_count {
52            let is_block_value = block.kind != BlockKind::Block && i == retained_count - 1;
53            if !is_block_value {
54                instructions_to_rewrite.push(block.instructions[i]);
55            }
56        }
57    }
58
59    // Apply rewrites
60    for instr_id in instructions_to_rewrite {
61        rewrite_instruction(func, instr_id, &state, env);
62    }
63
64    // Remove unused context variables
65    func.context
66        .retain(|ctx_var| is_id_or_name_used(&state, &env.identifiers, ctx_var.identifier));
67}
68
69/// State for tracking referenced identifiers during mark phase.
70struct State {
71    /// SSA-specific usages (by IdentifierId)
72    identifiers: FxHashSet<IdentifierId>,
73    /// Named variable usages (any version)
74    named: FxHashSet<String>,
75}
76
77impl State {
78    fn new() -> Self {
79        State {
80            identifiers: FxHashSet::default(),
81            named: FxHashSet::default(),
82        }
83    }
84
85    fn count(&self) -> usize {
86        self.identifiers.len()
87    }
88}
89
90/// Mark an identifier as being referenced (not dead code).
91fn reference(
92    state: &mut State,
93    identifiers: &[react_compiler_hir::Identifier],
94    identifier_id: IdentifierId,
95) {
96    state.identifiers.insert(identifier_id);
97    let ident = &identifiers[identifier_id.0 as usize];
98    if let Some(ref name) = ident.name {
99        state.named.insert(name.value().to_string());
100    }
101}
102
103/// Check if any version of the given identifier is used somewhere.
104/// Checks both the specific SSA id and (for named identifiers) any usage of that name.
105fn is_id_or_name_used(
106    state: &State,
107    identifiers: &[react_compiler_hir::Identifier],
108    identifier_id: IdentifierId,
109) -> bool {
110    if state.identifiers.contains(&identifier_id) {
111        return true;
112    }
113    let ident = &identifiers[identifier_id.0 as usize];
114    if let Some(ref name) = ident.name {
115        state.named.contains(name.value())
116    } else {
117        false
118    }
119}
120
121/// Check if this specific SSA id is used.
122fn is_id_used(state: &State, identifier_id: IdentifierId) -> bool {
123    state.identifiers.contains(&identifier_id)
124}
125
126/// Phase 1: Find all referenced identifiers via fixed-point iteration.
127fn find_referenced_identifiers(func: &HirFunction, env: &Environment) -> State {
128    let has_loop = has_back_edge(func);
129    // Collect block ids in reverse order (postorder - successors before predecessors)
130    let reversed_block_ids: Vec<BlockId> = func.body.blocks.keys().rev().copied().collect();
131
132    let mut state = State::new();
133    let mut size;
134
135    loop {
136        size = state.count();
137
138        for &block_id in &reversed_block_ids {
139            let block = &func.body.blocks[&block_id];
140
141            // Mark terminal operands
142            for place in visitors::each_terminal_operand(&block.terminal) {
143                reference(&mut state, &env.identifiers, place.identifier);
144            }
145
146            // Process instructions in reverse order
147            let instr_count = block.instructions.len();
148            for i in (0..instr_count).rev() {
149                let instr_id = block.instructions[i];
150                let instr = &func.instructions[instr_id.0 as usize];
151
152                let is_block_value = block.kind != BlockKind::Block && i == instr_count - 1;
153
154                if is_block_value {
155                    // Last instr of a value block is never eligible for pruning
156                    reference(&mut state, &env.identifiers, instr.lvalue.identifier);
157                    for place in visitors::each_instruction_value_operand(&instr.value, env) {
158                        reference(&mut state, &env.identifiers, place.identifier);
159                    }
160                } else if is_id_or_name_used(&state, &env.identifiers, instr.lvalue.identifier)
161                    || !pruneable_value(&instr.value, &state, env)
162                {
163                    reference(&mut state, &env.identifiers, instr.lvalue.identifier);
164
165                    if let InstructionValue::StoreLocal { lvalue, value, .. } = &instr.value {
166                        // If this is a Let/Const declaration, mark the initializer as referenced
167                        // only if the SSA'd lval is also referenced
168                        if lvalue.kind == InstructionKind::Reassign
169                            || is_id_used(&state, lvalue.place.identifier)
170                        {
171                            reference(&mut state, &env.identifiers, value.identifier);
172                        }
173                    } else {
174                        for place in visitors::each_instruction_value_operand(&instr.value, env) {
175                            reference(&mut state, &env.identifiers, place.identifier);
176                        }
177                    }
178                }
179            }
180
181            // Mark phi operands if phi result is used
182            for phi in &block.phis {
183                if is_id_or_name_used(&state, &env.identifiers, phi.place.identifier) {
184                    for (_pred, operand) in &phi.operands {
185                        reference(&mut state, &env.identifiers, operand.identifier);
186                    }
187                }
188            }
189        }
190
191        if !(state.count() > size && has_loop) {
192            break;
193        }
194    }
195
196    state
197}
198
199/// Rewrite a retained instruction (destructuring cleanup, StoreLocal -> DeclareLocal).
200fn rewrite_instruction(
201    func: &mut HirFunction,
202    instr_id: react_compiler_hir::InstructionId,
203    state: &State,
204    env: &Environment,
205) {
206    let instr = &mut func.instructions[instr_id.0 as usize];
207
208    match &mut instr.value {
209        InstructionValue::Destructure { lvalue, .. } => {
210            match &mut lvalue.pattern {
211                Pattern::Array(arr) => {
212                    // For arrays, replace unused items with holes, truncate trailing holes
213                    let mut last_entry_index = 0;
214                    for i in 0..arr.items.len() {
215                        match &arr.items[i] {
216                            ArrayPatternElement::Place(p) => {
217                                if !is_id_or_name_used(state, &env.identifiers, p.identifier) {
218                                    arr.items[i] = ArrayPatternElement::Hole;
219                                } else {
220                                    last_entry_index = i;
221                                }
222                            }
223                            ArrayPatternElement::Spread(s) => {
224                                if !is_id_or_name_used(state, &env.identifiers, s.place.identifier)
225                                {
226                                    arr.items[i] = ArrayPatternElement::Hole;
227                                } else {
228                                    last_entry_index = i;
229                                }
230                            }
231                            ArrayPatternElement::Hole => {}
232                        }
233                    }
234                    arr.items.truncate(last_entry_index + 1);
235                }
236                Pattern::Object(obj) => {
237                    // For objects, prune unused properties if rest element is unused or absent
238                    let mut next_properties: Option<Vec<ObjectPropertyOrSpread>> = None;
239                    for prop in &obj.properties {
240                        match prop {
241                            ObjectPropertyOrSpread::Property(p) => {
242                                if is_id_or_name_used(state, &env.identifiers, p.place.identifier) {
243                                    next_properties
244                                        .get_or_insert_with(Vec::new)
245                                        .push(prop.clone());
246                                }
247                            }
248                            ObjectPropertyOrSpread::Spread(s) => {
249                                if is_id_or_name_used(state, &env.identifiers, s.place.identifier) {
250                                    // Rest element is used, can't prune anything
251                                    next_properties = None;
252                                    break;
253                                }
254                            }
255                        }
256                    }
257                    if let Some(props) = next_properties {
258                        obj.properties = props;
259                    }
260                }
261            }
262        }
263        InstructionValue::StoreLocal {
264            lvalue,
265            type_annotation,
266            loc,
267            ..
268        } => {
269            if lvalue.kind != InstructionKind::Reassign
270                && !is_id_used(state, lvalue.place.identifier)
271            {
272                // This is a const/let declaration where the variable is accessed later,
273                // but where the value is always overwritten before being read.
274                // Rewrite to DeclareLocal so the initializer value can be DCE'd.
275                let new_lvalue = lvalue.clone();
276                let new_type_annotation = type_annotation.clone();
277                let new_loc = *loc;
278                instr.value = InstructionValue::DeclareLocal {
279                    lvalue: new_lvalue,
280                    type_annotation: new_type_annotation,
281                    loc: new_loc,
282                };
283            }
284        }
285        _ => {}
286    }
287}
288
289/// Returns true if it is safe to prune an instruction with the given value.
290fn pruneable_value(value: &InstructionValue, state: &State, env: &Environment) -> bool {
291    match value {
292        InstructionValue::DeclareLocal { lvalue, .. } => {
293            // Declarations are pruneable only if the named variable is never read later
294            !is_id_or_name_used(state, &env.identifiers, lvalue.place.identifier)
295        }
296        InstructionValue::StoreLocal { lvalue, .. } => {
297            if lvalue.kind == InstructionKind::Reassign {
298                // Reassignments can be pruned if the specific instance being assigned is never read
299                !is_id_used(state, lvalue.place.identifier)
300            } else {
301                // Declarations are pruneable only if the named variable is never read later
302                !is_id_or_name_used(state, &env.identifiers, lvalue.place.identifier)
303            }
304        }
305        InstructionValue::Destructure { lvalue, .. } => {
306            let mut is_id_or_name_used_flag = false;
307            let mut is_id_used_flag = false;
308            for place in visitors::each_pattern_operand(&lvalue.pattern) {
309                if is_id_used(state, place.identifier) {
310                    is_id_or_name_used_flag = true;
311                    is_id_used_flag = true;
312                } else if is_id_or_name_used(state, &env.identifiers, place.identifier) {
313                    is_id_or_name_used_flag = true;
314                }
315            }
316            if lvalue.kind == InstructionKind::Reassign {
317                !is_id_used_flag
318            } else {
319                !is_id_or_name_used_flag
320            }
321        }
322        InstructionValue::PostfixUpdate { lvalue, .. }
323        | InstructionValue::PrefixUpdate { lvalue, .. } => {
324            // Updates are pruneable if the specific instance being assigned is never read
325            !is_id_used(state, lvalue.identifier)
326        }
327        InstructionValue::Debugger { .. } => {
328            // explicitly retain debugger statements
329            false
330        }
331        InstructionValue::CallExpression { callee, .. } => {
332            if env.output_mode == OutputMode::Ssr {
333                let callee_ty =
334                    &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
335                if let Some(hook_kind) = env.get_hook_kind_for_type(callee_ty).ok().flatten() {
336                    match hook_kind {
337                        HookKind::UseState | HookKind::UseReducer | HookKind::UseRef => {
338                            return true;
339                        }
340                        _ => {}
341                    }
342                }
343            }
344            false
345        }
346        InstructionValue::MethodCall { property, .. } => {
347            if env.output_mode == OutputMode::Ssr {
348                let callee_ty =
349                    &env.types[env.identifiers[property.identifier.0 as usize].type_.0 as usize];
350                if let Some(hook_kind) = env.get_hook_kind_for_type(callee_ty).ok().flatten() {
351                    match hook_kind {
352                        HookKind::UseState | HookKind::UseReducer | HookKind::UseRef => {
353                            return true;
354                        }
355                        _ => {}
356                    }
357                }
358            }
359            false
360        }
361        InstructionValue::Await { .. }
362        | InstructionValue::ComputedDelete { .. }
363        | InstructionValue::ComputedStore { .. }
364        | InstructionValue::PropertyDelete { .. }
365        | InstructionValue::PropertyStore { .. }
366        | InstructionValue::StoreGlobal { .. } => {
367            // Mutating instructions are not safe to prune
368            false
369        }
370        InstructionValue::NewExpression { .. }
371        | InstructionValue::UnsupportedNode { .. }
372        | InstructionValue::TaggedTemplateExpression { .. } => {
373            // Potentially safe to prune, but we conservatively keep them
374            false
375        }
376        InstructionValue::GetIterator { .. }
377        | InstructionValue::NextPropertyOf { .. }
378        | InstructionValue::IteratorNext { .. } => {
379            // Iterator operations are always used downstream
380            false
381        }
382        InstructionValue::LoadContext { .. }
383        | InstructionValue::DeclareContext { .. }
384        | InstructionValue::StoreContext { .. } => false,
385        InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => false,
386        InstructionValue::RegExpLiteral { .. }
387        | InstructionValue::MetaProperty { .. }
388        | InstructionValue::LoadGlobal { .. }
389        | InstructionValue::ArrayExpression { .. }
390        | InstructionValue::BinaryExpression { .. }
391        | InstructionValue::ComputedLoad { .. }
392        | InstructionValue::ObjectMethod { .. }
393        | InstructionValue::FunctionExpression { .. }
394        | InstructionValue::LoadLocal { .. }
395        | InstructionValue::JsxExpression { .. }
396        | InstructionValue::JsxFragment { .. }
397        | InstructionValue::JSXText { .. }
398        | InstructionValue::ObjectExpression { .. }
399        | InstructionValue::Primitive { .. }
400        | InstructionValue::PropertyLoad { .. }
401        | InstructionValue::TemplateLiteral { .. }
402        | InstructionValue::TypeCastExpression { .. }
403        | InstructionValue::UnaryExpression { .. } => {
404            // Definitely safe to prune since they are read-only
405            true
406        }
407    }
408}
409
410/// Check if the CFG has any back edges (indicating loops).
411fn has_back_edge(func: &HirFunction) -> bool {
412    let mut visited: FxHashSet<BlockId> = FxHashSet::default();
413    for (block_id, block) in &func.body.blocks {
414        for pred_id in &block.preds {
415            if !visited.contains(pred_id) {
416                return true;
417            }
418        }
419        visited.insert(*block_id);
420    }
421    false
422}