Skip to main content

react_compiler_optimization/
inline_iifes.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//! Inlines immediately invoked function expressions (IIFEs) to allow more
7//! fine-grained memoization of the values they produce.
8//!
9//! Example:
10//! ```text
11//! const x = (() => {
12//!    const x = [];
13//!    x.push(foo());
14//!    return x;
15//! })();
16//!
17//! =>
18//!
19//! bb0:
20//!     // placeholder for the result, all return statements will assign here
21//!    let t0;
22//!    // Label allows using a goto (break) to exit out of the body
23//!    Label block=bb1 fallthrough=bb2
24//! bb1:
25//!    // code within the function expression
26//!    const x0 = [];
27//!    x0.push(foo());
28//!    // return is replaced by assignment to the result variable...
29//!    t0 = x0;
30//!    // ...and a goto to the code after the function expression invocation
31//!    Goto bb2
32//! bb2:
33//!    // code after the IIFE call
34//!    const x = t0;
35//! ```
36//!
37//! If the inlined function has only one return, we avoid the labeled block
38//! and fully inline the code. The original return is replaced with an assignment
39//! to the IIFE's call expression lvalue.
40//!
41//! Analogous to TS `Inference/InlineImmediatelyInvokedFunctionExpressions.ts`.
42
43use std::collections::{HashMap, HashSet};
44
45use react_compiler_hir::environment::Environment;
46use react_compiler_hir::visitors;
47use react_compiler_hir::{
48    BasicBlock, BlockId, BlockKind, EvaluationOrder, FunctionId, GENERATED_SOURCE, GotoVariant,
49    HirFunction, IdentifierId, IdentifierName, Instruction, InstructionId, InstructionKind,
50    InstructionValue, LValue, Place, Terminal,
51};
52use react_compiler_lowering::{
53    create_temporary_place, get_reverse_postordered_blocks, mark_instruction_ids, mark_predecessors,
54};
55
56use crate::merge_consecutive_blocks::merge_consecutive_blocks;
57
58/// Inline immediately invoked function expressions into the enclosing function's
59/// control flow graph.
60pub fn inline_immediately_invoked_function_expressions(
61    func: &mut HirFunction,
62    env: &mut Environment,
63) {
64    // Track all function expressions that are assigned to a temporary
65    let mut functions: HashMap<IdentifierId, FunctionId> = HashMap::new();
66    // Functions that are inlined (by identifier id of the callee)
67    let mut inlined_functions: HashSet<IdentifierId> = HashSet::new();
68
69    // Iterate the *existing* blocks from the outer component to find IIFEs
70    // and inline them. During iteration we will modify `func` (by inlining the CFG
71    // of IIFEs) so we explicitly copy references to just the original
72    // function's block IDs first. As blocks are split to make room for IIFE calls,
73    // the split portions of the blocks will be added to this queue.
74    let mut queue: Vec<BlockId> = func.body.blocks.keys().copied().collect();
75    let mut queue_idx = 0;
76
77    'queue: while queue_idx < queue.len() {
78        let block_id = queue[queue_idx];
79        queue_idx += 1;
80
81        let block = match func.body.blocks.get(&block_id) {
82            Some(b) => b,
83            None => continue,
84        };
85
86        // We can't handle labels inside expressions yet, so we don't inline IIFEs
87        // if they are in an expression block.
88        if !is_statement_block_kind(block.kind) {
89            continue;
90        }
91
92        let num_instructions = block.instructions.len();
93        for ii in 0..num_instructions {
94            let instr_id = func.body.blocks[&block_id].instructions[ii];
95            let instr = &func.instructions[instr_id.0 as usize];
96
97            match &instr.value {
98                InstructionValue::FunctionExpression { lowered_func, .. } => {
99                    let identifier_id = instr.lvalue.identifier;
100                    if env.identifiers[identifier_id.0 as usize].name.is_none() {
101                        functions.insert(identifier_id, lowered_func.func);
102                    }
103                    continue;
104                }
105                InstructionValue::CallExpression { callee, args, .. } => {
106                    if !args.is_empty() {
107                        // We don't support inlining when there are arguments
108                        continue;
109                    }
110
111                    let callee_id = callee.identifier;
112                    let inner_func_id = match functions.get(&callee_id) {
113                        Some(id) => *id,
114                        None => continue, // Not invoking a local function expression
115                    };
116
117                    let inner_func = &env.functions[inner_func_id.0 as usize];
118                    if !inner_func.params.is_empty() || inner_func.is_async || inner_func.generator
119                    {
120                        // Can't inline functions with params, or async/generator functions
121                        continue;
122                    }
123
124                    // We know this function is used for an IIFE and can prune it later
125                    inlined_functions.insert(callee_id);
126
127                    // Capture the lvalue from the call instruction
128                    let call_lvalue = func.instructions[instr_id.0 as usize].lvalue.clone();
129                    let block_terminal_id = func.body.blocks[&block_id].terminal.evaluation_order();
130                    let block_terminal_loc = func.body.blocks[&block_id].terminal.loc().cloned();
131                    let block_kind = func.body.blocks[&block_id].kind;
132
133                    // Create a new block which will contain code following the IIFE call
134                    let continuation_block_id = env.next_block_id();
135                    let continuation_instructions: Vec<InstructionId> =
136                        func.body.blocks[&block_id].instructions[ii + 1..].to_vec();
137                    let continuation_terminal = func.body.blocks[&block_id].terminal.clone();
138                    let continuation_block = BasicBlock {
139                        id: continuation_block_id,
140                        instructions: continuation_instructions,
141                        kind: block_kind,
142                        phis: Vec::new(),
143                        preds: indexmap::IndexSet::new(),
144                        terminal: continuation_terminal,
145                    };
146                    func.body
147                        .blocks
148                        .insert(continuation_block_id, continuation_block);
149
150                    // Trim the original block to contain instructions up to (but not including)
151                    // the IIFE
152                    func.body
153                        .blocks
154                        .get_mut(&block_id)
155                        .unwrap()
156                        .instructions
157                        .truncate(ii);
158
159                    let has_single_return =
160                        has_single_exit_return_terminal(&env.functions[inner_func_id.0 as usize]);
161                    let inner_entry = env.functions[inner_func_id.0 as usize].body.entry;
162
163                    if has_single_return {
164                        // Single-return path: simple goto replacement
165                        func.body.blocks.get_mut(&block_id).unwrap().terminal = Terminal::Goto {
166                            block: inner_entry,
167                            id: block_terminal_id,
168                            loc: block_terminal_loc,
169                            variant: GotoVariant::Break,
170                        };
171
172                        // Take blocks and instructions from inner function
173                        let inner_func = &mut env.functions[inner_func_id.0 as usize];
174                        let inner_blocks: Vec<(BlockId, BasicBlock)> =
175                            inner_func.body.blocks.drain(..).collect();
176                        let inner_instructions: Vec<Instruction> =
177                            inner_func.instructions.drain(..).collect();
178
179                        // Append inner instructions first, then remap block instruction IDs
180                        let instr_offset = func.instructions.len() as u32;
181                        func.instructions.extend(inner_instructions);
182
183                        for (_, mut inner_block) in inner_blocks {
184                            // Remap instruction IDs in the block
185                            for iid in &mut inner_block.instructions {
186                                *iid = InstructionId(iid.0 + instr_offset);
187                            }
188                            inner_block.preds.clear();
189
190                            if let Terminal::Return {
191                                value,
192                                id: ret_id,
193                                loc: ret_loc,
194                                ..
195                            } = &inner_block.terminal
196                            {
197                                // Replace return with LoadLocal + goto
198                                let load_instr = Instruction {
199                                    id: EvaluationOrder(0),
200                                    loc: ret_loc.clone(),
201                                    lvalue: call_lvalue.clone(),
202                                    value: InstructionValue::LoadLocal {
203                                        place: value.clone(),
204                                        loc: ret_loc.clone(),
205                                    },
206                                    effects: None,
207                                };
208                                let load_instr_id = InstructionId(func.instructions.len() as u32);
209                                func.instructions.push(load_instr);
210                                inner_block.instructions.push(load_instr_id);
211
212                                let ret_id = *ret_id;
213                                let ret_loc = ret_loc.clone();
214                                inner_block.terminal = Terminal::Goto {
215                                    block: continuation_block_id,
216                                    id: ret_id,
217                                    loc: ret_loc,
218                                    variant: GotoVariant::Break,
219                                };
220                            }
221
222                            func.body.blocks.insert(inner_block.id, inner_block);
223                        }
224                    } else {
225                        // Multi-return path: uses LabelTerminal
226                        let result = call_lvalue.clone();
227
228                        // Set block terminal to Label
229                        func.body.blocks.get_mut(&block_id).unwrap().terminal = Terminal::Label {
230                            block: inner_entry,
231                            id: EvaluationOrder(0),
232                            fallthrough: continuation_block_id,
233                            loc: block_terminal_loc,
234                        };
235
236                        // Declare the IIFE temporary
237                        declare_temporary(env, func, block_id, &result);
238
239                        // Promote the temporary with a name as we require this to persist
240                        let identifier_id = result.identifier;
241                        if env.identifiers[identifier_id.0 as usize].name.is_none() {
242                            promote_temporary(env, identifier_id);
243                        }
244
245                        // Take blocks and instructions from inner function
246                        let inner_func = &mut env.functions[inner_func_id.0 as usize];
247                        let inner_blocks: Vec<(BlockId, BasicBlock)> =
248                            inner_func.body.blocks.drain(..).collect();
249                        let inner_instructions: Vec<Instruction> =
250                            inner_func.instructions.drain(..).collect();
251
252                        // Append inner instructions first, then remap block instruction IDs
253                        let instr_offset = func.instructions.len() as u32;
254                        func.instructions.extend(inner_instructions);
255
256                        for (_, mut inner_block) in inner_blocks {
257                            for iid in &mut inner_block.instructions {
258                                *iid = InstructionId(iid.0 + instr_offset);
259                            }
260                            inner_block.preds.clear();
261
262                            // Rewrite return terminals to StoreLocal + goto
263                            if matches!(inner_block.terminal, Terminal::Return { .. }) {
264                                rewrite_block(
265                                    env,
266                                    &mut func.instructions,
267                                    &mut inner_block,
268                                    continuation_block_id,
269                                    &result,
270                                );
271                            }
272
273                            func.body.blocks.insert(inner_block.id, inner_block);
274                        }
275                    }
276
277                    // Ensure we visit the continuation block, since there may have been
278                    // sequential IIFEs that need to be visited.
279                    queue.push(continuation_block_id);
280                    continue 'queue;
281                }
282                _ => {
283                    // Any other use of a function expression means it isn't an IIFE
284                    for id in visitors::each_instruction_value_operand_ids(&instr.value, env) {
285                        functions.remove(&id);
286                    }
287                }
288            }
289        }
290    }
291
292    if !inlined_functions.is_empty() {
293        // Remove instructions that define lambdas which we inlined
294        for block in func.body.blocks.values_mut() {
295            block.instructions.retain(|instr_id| {
296                let instr = &func.instructions[instr_id.0 as usize];
297                !inlined_functions.contains(&instr.lvalue.identifier)
298            });
299        }
300
301        // If terminals have changed then blocks may have become newly unreachable.
302        // Re-run minification of the graph (incl reordering instruction ids).
303        func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions);
304        mark_instruction_ids(&mut func.body, &mut func.instructions);
305        mark_predecessors(&mut func.body);
306        merge_consecutive_blocks(func, &mut env.functions);
307    }
308}
309
310/// Returns true for "block" and "catch" block kinds which correspond to statements
311/// in the source.
312fn is_statement_block_kind(kind: BlockKind) -> bool {
313    matches!(kind, BlockKind::Block | BlockKind::Catch)
314}
315
316/// Returns true if the function has a single exit terminal (throw/return) which is a return.
317fn has_single_exit_return_terminal(func: &HirFunction) -> bool {
318    let mut has_return = false;
319    let mut exit_count = 0;
320    for block in func.body.blocks.values() {
321        match &block.terminal {
322            Terminal::Return { .. } => {
323                has_return = true;
324                exit_count += 1;
325            }
326            Terminal::Throw { .. } => {
327                exit_count += 1;
328            }
329            _ => {}
330        }
331    }
332    exit_count == 1 && has_return
333}
334
335/// Rewrites the block so that all `return` terminals are replaced:
336/// * Add a StoreLocal <return_value> = <terminal.value>
337/// * Replace the terminal with a Goto to <return_target>
338fn rewrite_block(
339    env: &mut Environment,
340    instructions: &mut Vec<Instruction>,
341    block: &mut BasicBlock,
342    return_target: BlockId,
343    return_value: &Place,
344) {
345    if let Terminal::Return {
346        value,
347        loc: ret_loc,
348        ..
349    } = &block.terminal
350    {
351        let store_lvalue = create_temporary_place(env, ret_loc.clone());
352        let store_instr = Instruction {
353            id: EvaluationOrder(0),
354            loc: ret_loc.clone(),
355            lvalue: store_lvalue,
356            value: InstructionValue::StoreLocal {
357                lvalue: LValue {
358                    kind: InstructionKind::Reassign,
359                    place: return_value.clone(),
360                },
361                value: value.clone(),
362                type_annotation: None,
363                loc: ret_loc.clone(),
364            },
365            effects: None,
366        };
367        let store_instr_id = InstructionId(instructions.len() as u32);
368        instructions.push(store_instr);
369        block.instructions.push(store_instr_id);
370
371        let ret_loc = ret_loc.clone();
372        block.terminal = Terminal::Goto {
373            block: return_target,
374            id: EvaluationOrder(0),
375            variant: GotoVariant::Break,
376            loc: ret_loc,
377        };
378    }
379}
380
381/// Emits a DeclareLocal instruction for the result temporary.
382fn declare_temporary(
383    env: &mut Environment,
384    func: &mut HirFunction,
385    block_id: BlockId,
386    result: &Place,
387) {
388    let declare_lvalue = create_temporary_place(env, result.loc.clone());
389    let declare_instr = Instruction {
390        id: EvaluationOrder(0),
391        loc: GENERATED_SOURCE,
392        lvalue: declare_lvalue,
393        value: InstructionValue::DeclareLocal {
394            lvalue: LValue {
395                place: result.clone(),
396                kind: InstructionKind::Let,
397            },
398            type_annotation: None,
399            loc: result.loc.clone(),
400        },
401        effects: None,
402    };
403    let instr_id = InstructionId(func.instructions.len() as u32);
404    func.instructions.push(declare_instr);
405    func.body
406        .blocks
407        .get_mut(&block_id)
408        .unwrap()
409        .instructions
410        .push(instr_id);
411}
412
413/// Promote a temporary identifier to a named identifier.
414fn promote_temporary(env: &mut Environment, identifier_id: IdentifierId) {
415    let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id;
416    env.identifiers[identifier_id.0 as usize].name =
417        Some(IdentifierName::Promoted(format!("#t{}", decl_id.0)));
418}
419