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