1use 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
24pub 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
36struct JsxInstrInfo {
38 instr_idx: usize, #[allow(dead_code)]
40 instr_id: InstructionId, 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 let mut globals: FxHashMap<IdentifierId, usize> = FxHashMap::default(); 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 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 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_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 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 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 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 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 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 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 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 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 let load_id = env.next_identifier_id();
355 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 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 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 let destructure_instr = emit_destructure_props(env, &props_obj, &old_to_new_props);
424
425 let load_global_instrs = emit_load_globals(func, jsx_group, globals)?;
427
428 let updated_jsx_instrs = emit_updated_jsx(func, jsx_group, &old_to_new_props);
430
431 let mut instructions = Vec::new();
433 instructions.push(destructure_instr);
434 instructions.extend(load_global_instrs);
435 instructions.extend(updated_jsx_instrs);
436
437 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 let last_lvalue = instr_table.last().unwrap().lvalue.clone();
448
449 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 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 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 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}