main
rs 422 lines 16.9 KB
Raw
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
14 use rustc_hash::FxHashSet;
15
16 use react_compiler_hir::environment::{Environment, OutputMode};
17 use react_compiler_hir::object_shape::HookKind;
18 use react_compiler_hir::visitors;
19 use 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`.
29 pub 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.
70 struct State {
71 /// SSA-specific usages (by IdentifierId)
72 identifiers: FxHashSet<IdentifierId>,
73 /// Named variable usages (any version)
74 named: FxHashSet<String>,
75 }
76
77 impl 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).
91 fn 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.
105 fn 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.
122 fn 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.
127 fn 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).
200 fn 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.
290 fn 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).
411 fn 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 }