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