Skip to main content

react_compiler_optimization/
outline_jsx.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//! Port of OutlineJsx from TypeScript.
7//!
8//! Outlines JSX expressions in callbacks into separate component functions.
9//! This pass is conditional on `env.config.enable_jsx_outlining` (defaults to false).
10
11use react_compiler_utils::FxIndexSet;
12use rustc_hash::{FxHashMap, FxHashSet};
13
14use react_compiler_utils::FxIndexMap;
15use react_compiler_hir::environment::Environment;
16use react_compiler_hir::{
17    BasicBlock, BlockId, BlockKind, EvaluationOrder, FunctionId, HIR, HirFunction, IdentifierId,
18    IdentifierName, Instruction, InstructionId, InstructionKind, InstructionValue, JsxAttribute,
19    JsxTag, LValuePattern, NonLocalBinding, ObjectPattern, ObjectProperty, ObjectPropertyKey,
20    ObjectPropertyOrSpread, ObjectPropertyType, ParamPattern, Pattern, Place, ReactFunctionType,
21    ReturnVariant, Terminal,
22};
23
24/// Outline JSX expressions in inner functions into separate outlined components.
25///
26/// Ported from TS `outlineJSX` in `Optimization/OutlineJsx.ts`.
27pub fn outline_jsx(func: &mut HirFunction, env: &mut Environment) {
28    let mut outlined_fns: Vec<HirFunction> = Vec::new();
29    outline_jsx_impl(func, env, &mut outlined_fns);
30
31    for outlined_fn in outlined_fns {
32        env.outline_function(outlined_fn, Some(ReactFunctionType::Component));
33    }
34}
35
36/// Data about a JSX instruction for outlining
37struct JsxInstrInfo {
38    instr_idx: usize, // index into func.instructions
39    #[allow(dead_code)]
40    instr_id: InstructionId, // the InstructionId
41    lvalue_id: IdentifierId,
42    eval_order: EvaluationOrder,
43}
44
45struct OutlinedJsxAttribute {
46    original_name: String,
47    new_name: String,
48    place: Place,
49}
50
51struct OutlinedResult {
52    instrs: Vec<Instruction>,
53    func: HirFunction,
54}
55
56fn outline_jsx_impl(
57    func: &mut HirFunction,
58    env: &mut Environment,
59    outlined_fns: &mut Vec<HirFunction>,
60) {
61    // Collect LoadGlobal instructions (tag -> instr)
62    let mut globals: FxHashMap<IdentifierId, usize> = FxHashMap::default(); // id -> instr_idx
63
64    // Process each block
65    let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
66    for block_id in &block_ids {
67        let block = &func.body.blocks[block_id];
68        let instr_ids = block.instructions.clone();
69
70        let mut rewrite_instr: FxHashMap<EvaluationOrder, Vec<Instruction>> = FxHashMap::default();
71        let mut jsx_group: Vec<JsxInstrInfo> = Vec::new();
72        let mut children_ids: FxHashSet<IdentifierId> = FxHashSet::default();
73
74        // First pass: collect all instruction info without borrowing func mutably
75        enum InstrAction {
76            LoadGlobal {
77                lvalue_id: IdentifierId,
78                instr_idx: usize,
79            },
80            FunctionExpr {
81                func_id: FunctionId,
82            },
83            JsxExpr {
84                lvalue_id: IdentifierId,
85                instr_idx: usize,
86                eval_order: EvaluationOrder,
87                child_ids: Vec<IdentifierId>,
88            },
89            Other,
90        }
91
92        let mut actions: Vec<InstrAction> = Vec::new();
93        for i in (0..instr_ids.len()).rev() {
94            let iid = instr_ids[i];
95            let instr = &func.instructions[iid.0 as usize];
96            let lvalue_id = instr.lvalue.identifier;
97
98            match &instr.value {
99                InstructionValue::LoadGlobal { .. } => {
100                    actions.push(InstrAction::LoadGlobal {
101                        lvalue_id,
102                        instr_idx: iid.0 as usize,
103                    });
104                }
105                InstructionValue::FunctionExpression { lowered_func, .. } => {
106                    actions.push(InstrAction::FunctionExpr {
107                        func_id: lowered_func.func,
108                    });
109                }
110                InstructionValue::JsxExpression { children, .. } => {
111                    let child_ids = children
112                        .as_ref()
113                        .map(|kids| kids.iter().map(|c| c.identifier).collect())
114                        .unwrap_or_default();
115                    actions.push(InstrAction::JsxExpr {
116                        lvalue_id,
117                        instr_idx: iid.0 as usize,
118                        eval_order: instr.id,
119                        child_ids,
120                    });
121                }
122                _ => {
123                    actions.push(InstrAction::Other);
124                }
125            }
126        }
127
128        // Second pass: process actions
129        for action in actions {
130            match action {
131                InstrAction::LoadGlobal {
132                    lvalue_id,
133                    instr_idx,
134                } => {
135                    globals.insert(lvalue_id, instr_idx);
136                }
137                InstrAction::FunctionExpr { func_id } => {
138                    let mut inner_func = std::mem::replace(
139                        &mut env.functions[func_id.0 as usize],
140                        react_compiler_ssa::enter_ssa::placeholder_function(),
141                    );
142                    outline_jsx_impl(&mut inner_func, env, outlined_fns);
143                    env.functions[func_id.0 as usize] = inner_func;
144                }
145                InstrAction::JsxExpr {
146                    lvalue_id,
147                    instr_idx,
148                    eval_order,
149                    child_ids,
150                } => {
151                    if !children_ids.contains(&lvalue_id) {
152                        process_and_outline_jsx(
153                            func,
154                            env,
155                            &mut jsx_group,
156                            &globals,
157                            &mut rewrite_instr,
158                            outlined_fns,
159                        );
160                        jsx_group.clear();
161                        children_ids.clear();
162                    }
163                    jsx_group.push(JsxInstrInfo {
164                        instr_idx,
165                        instr_id: InstructionId(instr_idx as u32),
166                        lvalue_id,
167                        eval_order,
168                    });
169                    for child_id in child_ids {
170                        children_ids.insert(child_id);
171                    }
172                }
173                InstrAction::Other => {}
174            }
175        }
176        // Process remaining JSX group after the loop
177        process_and_outline_jsx(
178            func,
179            env,
180            &mut jsx_group,
181            &globals,
182            &mut rewrite_instr,
183            outlined_fns,
184        );
185        if !rewrite_instr.is_empty() {
186            let block = func.body.blocks.get_mut(block_id).unwrap();
187            let old_instr_ids = block.instructions.clone();
188            let mut new_instr_ids = Vec::new();
189            for &iid in &old_instr_ids {
190                let eval_order = func.instructions[iid.0 as usize].id;
191                if let Some(replacement_instrs) = rewrite_instr.get(&eval_order) {
192                    // Add replacement instructions to the instruction table and reference them
193                    for new_instr in replacement_instrs {
194                        let new_idx = func.instructions.len();
195                        func.instructions.push(new_instr.clone());
196                        new_instr_ids.push(InstructionId(new_idx as u32));
197                    }
198                } else {
199                    new_instr_ids.push(iid);
200                }
201            }
202            let block = func.body.blocks.get_mut(block_id).unwrap();
203            block.instructions = new_instr_ids;
204
205            // Run dead code elimination after rewriting
206            super::dead_code_elimination(func, env);
207        }
208    }
209}
210
211fn process_and_outline_jsx(
212    func: &mut HirFunction,
213    env: &mut Environment,
214    jsx_group: &mut Vec<JsxInstrInfo>,
215    globals: &FxHashMap<IdentifierId, usize>,
216    rewrite_instr: &mut FxHashMap<EvaluationOrder, Vec<Instruction>>,
217    outlined_fns: &mut Vec<HirFunction>,
218) {
219    if jsx_group.len() <= 1 {
220        return;
221    }
222    // Sort by eval order ascending (TS: sort by a.id - b.id)
223    jsx_group.sort_by_key(|j| j.eval_order);
224
225    let result = process_jsx_group(func, env, jsx_group, globals);
226    if let Some(result) = result {
227        outlined_fns.push(result.func);
228        // Map from the LAST JSX instruction's eval order to the replacement instructions
229        // In the TS code, `state.jsx.at(0)` is the first element pushed during reverse iteration,
230        // which is the last JSX in forward block order (highest eval order).
231        // After sorting by eval_order ascending, that's jsx_group.last().
232        let last_eval_order = jsx_group.last().unwrap().eval_order;
233        rewrite_instr.insert(last_eval_order, result.instrs);
234    }
235}
236
237fn process_jsx_group(
238    func: &HirFunction,
239    env: &mut Environment,
240    jsx_group: &[JsxInstrInfo],
241    globals: &FxHashMap<IdentifierId, usize>,
242) -> Option<OutlinedResult> {
243    // Only outline in callbacks, not top-level components
244    if func.fn_type == ReactFunctionType::Component {
245        return None;
246    }
247
248    let props = collect_props(func, env, jsx_group)?;
249
250    let outlined_tag = env.generate_globally_unique_identifier_name(None);
251    let new_instrs = emit_outlined_jsx(func, env, jsx_group, &props, &outlined_tag)?;
252    let outlined_fn = emit_outlined_fn(func, env, jsx_group, &props, globals)?;
253
254    // Set the outlined function's id
255    let mut outlined_fn = outlined_fn;
256    outlined_fn.id = Some(outlined_tag);
257
258    Some(OutlinedResult {
259        instrs: new_instrs,
260        func: outlined_fn,
261    })
262}
263
264fn collect_props(
265    func: &HirFunction,
266    env: &mut Environment,
267    jsx_group: &[JsxInstrInfo],
268) -> Option<Vec<OutlinedJsxAttribute>> {
269    let mut id_counter = 1u32;
270    let mut seen: FxHashSet<String> = FxHashSet::default();
271    let mut attributes = Vec::new();
272    let jsx_ids: FxHashSet<IdentifierId> = jsx_group.iter().map(|j| j.lvalue_id).collect();
273
274    let mut generate_name = |old_name: &str, _env: &mut Environment| -> String {
275        let mut new_name = old_name.to_string();
276        while seen.contains(&new_name) {
277            new_name = format!("{}{}", old_name, id_counter);
278            id_counter += 1;
279        }
280        seen.insert(new_name.clone());
281        // TS: env.programContext.addNewReference(newName)
282        // We don't have programContext in Rust, but this is needed for unique name tracking
283        new_name
284    };
285
286    for info in jsx_group {
287        let instr = &func.instructions[info.instr_idx];
288        if let InstructionValue::JsxExpression {
289            props, children, ..
290        } = &instr.value
291        {
292            for attr in props {
293                match attr {
294                    JsxAttribute::SpreadAttribute { .. } => return None,
295                    JsxAttribute::Attribute { name, place } => {
296                        let new_name = generate_name(name, env);
297                        attributes.push(OutlinedJsxAttribute {
298                            original_name: name.clone(),
299                            new_name,
300                            place: place.clone(),
301                        });
302                    }
303                }
304            }
305
306            if let Some(kids) = children {
307                for child in kids {
308                    if jsx_ids.contains(&child.identifier) {
309                        continue;
310                    }
311                    // Promote the child's identifier to a named temporary
312                    let child_id = child.identifier;
313                    let decl_id = env.identifiers[child_id.0 as usize].declaration_id;
314                    if env.identifiers[child_id.0 as usize].name.is_none() {
315                        env.identifiers[child_id.0 as usize].name =
316                            Some(IdentifierName::Promoted(format!("#t{}", decl_id.0)));
317                    }
318
319                    let child_name = match &env.identifiers[child_id.0 as usize].name {
320                        Some(IdentifierName::Named(n)) => n.clone(),
321                        Some(IdentifierName::Promoted(n)) => n.clone(),
322                        None => format!("#t{}", decl_id.0),
323                    };
324                    let new_name = generate_name("t", env);
325                    attributes.push(OutlinedJsxAttribute {
326                        original_name: child_name,
327                        new_name,
328                        place: child.clone(),
329                    });
330                }
331            }
332        }
333    }
334
335    Some(attributes)
336}
337
338fn emit_outlined_jsx(
339    func: &HirFunction,
340    env: &mut Environment,
341    jsx_group: &[JsxInstrInfo],
342    outlined_props: &[OutlinedJsxAttribute],
343    outlined_tag: &str,
344) -> Option<Vec<Instruction>> {
345    let props: Vec<JsxAttribute> = outlined_props
346        .iter()
347        .map(|p| JsxAttribute::Attribute {
348            name: p.new_name.clone(),
349            place: p.place.clone(),
350        })
351        .collect();
352
353    // Create LoadGlobal for the outlined component
354    let load_id = env.next_identifier_id();
355    // Promote it as a JSX tag temporary
356    let decl_id = env.identifiers[load_id.0 as usize].declaration_id;
357    env.identifiers[load_id.0 as usize].name =
358        Some(IdentifierName::Promoted(format!("#T{}", decl_id.0)));
359
360    let load_place = Place {
361        identifier: load_id,
362        effect: react_compiler_hir::Effect::Unknown,
363        reactive: false,
364        loc: None,
365    };
366
367    let load_jsx = Instruction {
368        id: EvaluationOrder(0),
369        lvalue: load_place.clone(),
370        value: InstructionValue::LoadGlobal {
371            binding: NonLocalBinding::ModuleLocal {
372                name: outlined_tag.to_string(),
373            },
374            loc: None,
375        },
376        loc: None,
377        effects: None,
378    };
379
380    // Create the replacement JsxExpression using the last JSX instruction's lvalue
381    let last_info = jsx_group.last().unwrap();
382    let last_instr = &func.instructions[last_info.instr_idx];
383    let jsx_expr = Instruction {
384        id: EvaluationOrder(0),
385        lvalue: last_instr.lvalue.clone(),
386        value: InstructionValue::JsxExpression {
387            tag: JsxTag::Place(load_place),
388            props,
389            children: None,
390            loc: None,
391            opening_loc: None,
392            closing_loc: None,
393        },
394        loc: None,
395        effects: None,
396    };
397
398    Some(vec![load_jsx, jsx_expr])
399}
400
401fn emit_outlined_fn(
402    func: &HirFunction,
403    env: &mut Environment,
404    jsx_group: &[JsxInstrInfo],
405    old_props: &[OutlinedJsxAttribute],
406    globals: &FxHashMap<IdentifierId, usize>,
407) -> Option<HirFunction> {
408    let old_to_new_props = create_old_to_new_props_mapping(env, old_props);
409
410    // Create props parameter
411    let props_obj_id = env.next_identifier_id();
412    let decl_id = env.identifiers[props_obj_id.0 as usize].declaration_id;
413    env.identifiers[props_obj_id.0 as usize].name =
414        Some(IdentifierName::Promoted(format!("#t{}", decl_id.0)));
415    let props_obj = Place {
416        identifier: props_obj_id,
417        effect: react_compiler_hir::Effect::Unknown,
418        reactive: false,
419        loc: None,
420    };
421
422    // Create destructure instruction
423    let destructure_instr = emit_destructure_props(env, &props_obj, &old_to_new_props);
424
425    // Emit load globals for JSX tags
426    let load_global_instrs = emit_load_globals(func, jsx_group, globals)?;
427
428    // Emit updated JSX instructions
429    let updated_jsx_instrs = emit_updated_jsx(func, jsx_group, &old_to_new_props);
430
431    // Build instructions list
432    let mut instructions = Vec::new();
433    instructions.push(destructure_instr);
434    instructions.extend(load_global_instrs);
435    instructions.extend(updated_jsx_instrs);
436
437    // Build instruction table and instruction IDs
438    let mut instr_table = Vec::new();
439    let mut instr_ids = Vec::new();
440    for instr in instructions {
441        let idx = instr_table.len();
442        instr_table.push(instr);
443        instr_ids.push(InstructionId(idx as u32));
444    }
445
446    // Return terminal uses the last instruction's lvalue
447    let last_lvalue = instr_table.last().unwrap().lvalue.clone();
448
449    // Create return place
450    let returns_id = env.next_identifier_id();
451    let returns_place = Place {
452        identifier: returns_id,
453        effect: react_compiler_hir::Effect::Unknown,
454        reactive: false,
455        loc: None,
456    };
457
458    let block = BasicBlock {
459        kind: BlockKind::Block,
460        id: BlockId(0),
461        instructions: instr_ids,
462        preds: FxIndexSet::default(),
463        terminal: Terminal::Return {
464            value: last_lvalue,
465            return_variant: ReturnVariant::Explicit,
466            id: EvaluationOrder(0),
467            loc: None,
468            effects: None,
469        },
470        phis: Vec::new(),
471    };
472
473    let mut blocks = FxIndexMap::default();
474    blocks.insert(BlockId(0), block);
475
476    let outlined_fn = HirFunction {
477        id: None,
478        name_hint: None,
479        fn_type: ReactFunctionType::Other,
480        params: vec![ParamPattern::Place(props_obj)],
481        return_type_annotation: None,
482        returns: returns_place,
483        context: Vec::new(),
484        body: HIR {
485            entry: BlockId(0),
486            blocks,
487        },
488        instructions: instr_table,
489        generator: false,
490        is_async: false,
491        directives: Vec::new(),
492        aliasing_effects: Some(vec![]),
493        loc: None,
494    };
495
496    Some(outlined_fn)
497}
498
499fn emit_load_globals(
500    func: &HirFunction,
501    jsx_group: &[JsxInstrInfo],
502    globals: &FxHashMap<IdentifierId, usize>,
503) -> Option<Vec<Instruction>> {
504    let mut instructions = Vec::new();
505    for info in jsx_group {
506        let instr = &func.instructions[info.instr_idx];
507        if let InstructionValue::JsxExpression { tag, .. } = &instr.value {
508            if let JsxTag::Place(tag_place) = tag {
509                let global_instr_idx = globals.get(&tag_place.identifier)?;
510                instructions.push(func.instructions[*global_instr_idx].clone());
511            }
512        }
513    }
514    Some(instructions)
515}
516
517fn emit_updated_jsx(
518    func: &HirFunction,
519    jsx_group: &[JsxInstrInfo],
520    old_to_new_props: &FxIndexMap<IdentifierId, OutlinedJsxAttribute>,
521) -> Vec<Instruction> {
522    let jsx_ids: FxHashSet<IdentifierId> = jsx_group.iter().map(|j| j.lvalue_id).collect();
523    let mut new_instrs = Vec::new();
524
525    for info in jsx_group {
526        let instr = &func.instructions[info.instr_idx];
527        if let InstructionValue::JsxExpression {
528            tag,
529            props,
530            children,
531            loc,
532            opening_loc,
533            closing_loc,
534        } = &instr.value
535        {
536            let mut new_props = Vec::new();
537            for prop in props {
538                // TS: invariant(prop.kind === 'JsxAttribute', ...)
539                // Spread attributes would have caused collectProps to return null earlier
540                let (name, place) = match prop {
541                    JsxAttribute::Attribute { name, place } => (name, place),
542                    JsxAttribute::SpreadAttribute { .. } => {
543                        unreachable!("Expected only JsxAttribute, not spread")
544                    }
545                };
546                if name == "key" {
547                    continue;
548                }
549                // TS: invariant(newProp !== undefined, ...)
550                let new_prop = old_to_new_props
551                    .get(&place.identifier)
552                    .expect("Expected a new property for identifier");
553                new_props.push(JsxAttribute::Attribute {
554                    name: new_prop.original_name.clone(),
555                    place: new_prop.place.clone(),
556                });
557            }
558
559            let new_children = children.as_ref().map(|kids| {
560                kids.iter()
561                    .map(|child| {
562                        if jsx_ids.contains(&child.identifier) {
563                            child.clone()
564                        } else {
565                            // TS: invariant(newChild !== undefined, ...)
566                            let new_prop = old_to_new_props
567                                .get(&child.identifier)
568                                .expect("Expected a new prop for child identifier");
569                            new_prop.place.clone()
570                        }
571                    })
572                    .collect()
573            });
574
575            new_instrs.push(Instruction {
576                id: instr.id,
577                lvalue: instr.lvalue.clone(),
578                value: InstructionValue::JsxExpression {
579                    tag: tag.clone(),
580                    props: new_props,
581                    children: new_children,
582                    loc: *loc,
583                    opening_loc: *opening_loc,
584                    closing_loc: *closing_loc,
585                },
586                loc: instr.loc,
587                effects: instr.effects.clone(),
588            });
589        }
590    }
591
592    new_instrs
593}
594
595fn create_old_to_new_props_mapping(
596    env: &mut Environment,
597    old_props: &[OutlinedJsxAttribute],
598) -> FxIndexMap<IdentifierId, OutlinedJsxAttribute> {
599    let mut old_to_new = FxIndexMap::default();
600
601    for old_prop in old_props {
602        if old_prop.original_name == "key" {
603            continue;
604        }
605
606        let new_id = env.next_identifier_id();
607        env.identifiers[new_id.0 as usize].name =
608            Some(IdentifierName::Named(old_prop.new_name.clone()));
609
610        let new_place = Place {
611            identifier: new_id,
612            effect: react_compiler_hir::Effect::Unknown,
613            reactive: false,
614            loc: None,
615        };
616
617        old_to_new.insert(
618            old_prop.place.identifier,
619            OutlinedJsxAttribute {
620                original_name: old_prop.original_name.clone(),
621                new_name: old_prop.new_name.clone(),
622                place: new_place,
623            },
624        );
625    }
626
627    old_to_new
628}
629
630fn emit_destructure_props(
631    env: &mut Environment,
632    props_obj: &Place,
633    old_to_new_props: &FxIndexMap<IdentifierId, OutlinedJsxAttribute>,
634) -> Instruction {
635    let mut properties = Vec::new();
636    for prop in old_to_new_props.values() {
637        properties.push(ObjectPropertyOrSpread::Property(ObjectProperty {
638            key: ObjectPropertyKey::String {
639                name: prop.new_name.clone(),
640            },
641            property_type: ObjectPropertyType::Property,
642            place: prop.place.clone(),
643        }));
644    }
645
646    let lvalue_id = env.next_identifier_id();
647    let lvalue = Place {
648        identifier: lvalue_id,
649        effect: react_compiler_hir::Effect::Unknown,
650        reactive: false,
651        loc: None,
652    };
653
654    Instruction {
655        id: EvaluationOrder(0),
656        lvalue,
657        value: InstructionValue::Destructure {
658            lvalue: LValuePattern {
659                pattern: Pattern::Object(ObjectPattern {
660                    properties,
661                    loc: None,
662                }),
663                kind: InstructionKind::Let,
664            },
665            value: props_obj.clone(),
666            loc: None,
667        },
668        loc: None,
669        effects: None,
670    }
671}