1use std::collections::{HashMap, HashSet};
18
19use react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory};
20use react_compiler_hir::dominator::post_dominator_frontier;
21use react_compiler_hir::environment::Environment;
22use react_compiler_hir::object_shape::HookKind;
23use react_compiler_hir::visitors;
24use react_compiler_hir::{
25 BlockId, Effect, FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern,
26 Terminal, Type,
27};
28
29use react_compiler_utils::DisjointSet;
30
31use crate::infer_reactive_scope_variables::find_disjoint_mutable_values;
32
33pub fn infer_reactive_places(
41 func: &mut HirFunction,
42 env: &mut Environment,
43) -> Result<(), CompilerDiagnostic> {
44 let mut aliased_identifiers = find_disjoint_mutable_values(func, env);
45 let mut reactive_map = ReactivityMap::new(&mut aliased_identifiers);
46 let mut stable_sidemap = StableSidemap::new();
47
48 for param in &func.params {
50 let place = match param {
51 ParamPattern::Place(p) => p,
52 ParamPattern::Spread(s) => &s.place,
53 };
54 reactive_map.mark_reactive(place.identifier);
55 }
56
57 let post_dominators = react_compiler_hir::dominator::compute_post_dominator_tree(
59 func,
60 env.next_block_id().0,
61 false,
62 )?;
63
64 let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
66
67 let mut phi_operand_reactive: HashMap<(BlockId, usize, usize), bool> = HashMap::new();
73
74 loop {
76 for block_id in &block_ids {
77 let block = func.body.blocks.get(block_id).unwrap();
78 let has_reactive_control = is_reactive_controlled_block(
79 block.id,
80 func,
81 &post_dominators,
82 &mut reactive_map,
83 );
84
85 let block = func.body.blocks.get(block_id).unwrap();
87 for (phi_idx, phi) in block.phis.iter().enumerate() {
88 if reactive_map.is_reactive(phi.place.identifier) {
89 continue;
92 }
93 let mut is_phi_reactive = false;
94 for (op_idx, (_pred, operand)) in phi.operands.iter().enumerate() {
95 let op_reactive = reactive_map.is_reactive(operand.identifier);
96 phi_operand_reactive.insert((*block_id, phi_idx, op_idx), op_reactive);
98 if op_reactive {
99 is_phi_reactive = true;
100 break; }
102 }
103 if is_phi_reactive {
104 reactive_map.mark_reactive(phi.place.identifier);
105 } else {
106 for (pred, _operand) in &phi.operands {
107 if is_reactive_controlled_block(
108 *pred,
109 func,
110 &post_dominators,
111 &mut reactive_map,
112 ) {
113 reactive_map.mark_reactive(phi.place.identifier);
114 break;
115 }
116 }
117 }
118 }
119
120 let block = func.body.blocks.get(block_id).unwrap();
122 for instr_id in &block.instructions {
123 let instr = &func.instructions[instr_id.0 as usize];
124
125 stable_sidemap.handle_instruction(instr, env);
127
128 let value = &instr.value;
129
130 let mut has_reactive_input = false;
132 let operands: Vec<IdentifierId> =
133 visitors::each_instruction_value_operand(value, env)
134 .into_iter()
135 .map(|p| p.identifier)
136 .collect();
137 for &op_id in &operands {
138 let reactive = reactive_map.is_reactive(op_id);
139 has_reactive_input = has_reactive_input || reactive;
140 }
141
142 match value {
144 InstructionValue::CallExpression { callee, .. } => {
145 let callee_ty = &env.types
146 [env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
147 if get_hook_kind_for_type(env, callee_ty)?.is_some()
148 || is_use_operator_type(callee_ty)
149 {
150 has_reactive_input = true;
151 }
152 }
153 InstructionValue::MethodCall { property, .. } => {
154 let property_ty = &env.types
155 [env.identifiers[property.identifier.0 as usize].type_.0 as usize];
156 if get_hook_kind_for_type(env, property_ty)?.is_some()
157 || is_use_operator_type(property_ty)
158 {
159 has_reactive_input = true;
160 }
161 }
162 _ => {}
163 }
164
165 if has_reactive_input {
166 let lvalue_ids: Vec<IdentifierId> = visitors::each_instruction_lvalue(instr)
168 .into_iter()
169 .map(|p| p.identifier)
170 .collect();
171 for lvalue_id in lvalue_ids {
172 if stable_sidemap.is_stable(lvalue_id) {
173 continue;
174 }
175 reactive_map.mark_reactive(lvalue_id);
176 }
177 }
178
179 if has_reactive_input || has_reactive_control {
180 let operand_places = visitors::each_instruction_value_operand(value, env);
182 for op_place in &operand_places {
183 match op_place.effect {
184 Effect::Capture
185 | Effect::Store
186 | Effect::ConditionallyMutate
187 | Effect::ConditionallyMutateIterator
188 | Effect::Mutate => {
189 let op_range = &env.identifiers
190 [op_place.identifier.0 as usize]
191 .mutable_range;
192 if op_range.contains(instr.id) {
193 reactive_map.mark_reactive(op_place.identifier);
194 }
195 }
196 Effect::Freeze | Effect::Read => {
197 }
199 Effect::Unknown => {
200 return Err(CompilerDiagnostic::new(
201 ErrorCategory::Invariant,
202 &format!(
203 "Unexpected unknown effect at {:?}",
204 op_place.loc
205 ),
206 None,
207 ));
208 }
209 }
210 }
211 }
212 }
213
214 for op in visitors::each_terminal_operand(&block.terminal) {
216 reactive_map.is_reactive(op.identifier);
217 }
218 }
219
220 if !reactive_map.snapshot() {
221 break;
222 }
223 }
224
225 propagate_reactivity_to_inner_functions_outer(func, env, &mut reactive_map);
227
228 apply_reactive_flags_replay(
230 func,
231 env,
232 &mut reactive_map,
233 &mut stable_sidemap,
234 &phi_operand_reactive,
235 );
236
237 Ok(())
238}
239
240struct ReactivityMap<'a> {
245 has_changes: bool,
246 reactive: HashSet<IdentifierId>,
247 aliased_identifiers: &'a mut DisjointSet<IdentifierId>,
248}
249
250impl<'a> ReactivityMap<'a> {
251 fn new(aliased_identifiers: &'a mut DisjointSet<IdentifierId>) -> Self {
252 ReactivityMap {
253 has_changes: false,
254 reactive: HashSet::new(),
255 aliased_identifiers,
256 }
257 }
258
259 fn is_reactive(&mut self, id: IdentifierId) -> bool {
260 let canonical = self.aliased_identifiers.find_opt(id).unwrap_or(id);
261 self.reactive.contains(&canonical)
262 }
263
264 fn mark_reactive(&mut self, id: IdentifierId) {
265 let canonical = self.aliased_identifiers.find_opt(id).unwrap_or(id);
266 if self.reactive.insert(canonical) {
267 self.has_changes = true;
268 }
269 }
270
271 fn snapshot(&mut self) -> bool {
273 let had_changes = self.has_changes;
274 self.has_changes = false;
275 had_changes
276 }
277}
278
279struct StableSidemap {
284 map: HashMap<IdentifierId, bool>,
285}
286
287impl StableSidemap {
288 fn new() -> Self {
289 StableSidemap {
290 map: HashMap::new(),
291 }
292 }
293
294 fn handle_instruction(
295 &mut self,
296 instr: &react_compiler_hir::Instruction,
297 env: &Environment,
298 ) {
299 let lvalue_id = instr.lvalue.identifier;
300 let value = &instr.value;
301
302 match value {
303 InstructionValue::CallExpression { callee, .. } => {
304 let callee_ty =
305 &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
306 if evaluates_to_stable_type_or_container(env, callee_ty) {
307 let lvalue_ty =
308 &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize];
309 if is_stable_type(lvalue_ty) {
310 self.map.insert(lvalue_id, true);
311 } else {
312 self.map.insert(lvalue_id, false);
313 }
314 }
315 }
316 InstructionValue::MethodCall { property, .. } => {
317 let property_ty = &env.types
318 [env.identifiers[property.identifier.0 as usize].type_.0 as usize];
319 if evaluates_to_stable_type_or_container(env, property_ty) {
320 let lvalue_ty =
321 &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize];
322 if is_stable_type(lvalue_ty) {
323 self.map.insert(lvalue_id, true);
324 } else {
325 self.map.insert(lvalue_id, false);
326 }
327 }
328 }
329 InstructionValue::PropertyLoad { object, .. } => {
330 let source_id = object.identifier;
331 if self.map.contains_key(&source_id) {
332 let lvalue_ty =
333 &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize];
334 if is_stable_type_container(lvalue_ty) {
335 self.map.insert(lvalue_id, false);
336 } else if is_stable_type(lvalue_ty) {
337 self.map.insert(lvalue_id, true);
338 }
339 }
340 }
341 InstructionValue::Destructure { value: val, .. } => {
342 let source_id = val.identifier;
343 if self.map.contains_key(&source_id) {
344 let lvalue_ids: Vec<IdentifierId> = visitors::each_instruction_lvalue(instr)
345 .into_iter()
346 .map(|p| p.identifier)
347 .collect();
348 for lid in lvalue_ids {
349 let lid_ty =
350 &env.types[env.identifiers[lid.0 as usize].type_.0 as usize];
351 if is_stable_type_container(lid_ty) {
352 self.map.insert(lid, false);
353 } else if is_stable_type(lid_ty) {
354 self.map.insert(lid, true);
355 }
356 }
357 }
358 }
359 InstructionValue::StoreLocal {
360 lvalue, value: val, ..
361 } => {
362 if let Some(&entry) = self.map.get(&val.identifier) {
363 self.map.insert(lvalue_id, entry);
364 self.map.insert(lvalue.place.identifier, entry);
365 }
366 }
367 InstructionValue::LoadLocal { place, .. } => {
368 if let Some(&entry) = self.map.get(&place.identifier) {
369 self.map.insert(lvalue_id, entry);
370 }
371 }
372 _ => {}
373 }
374 }
375
376 fn is_stable(&self, id: IdentifierId) -> bool {
377 self.map.get(&id).copied().unwrap_or(false)
378 }
379}
380
381fn is_reactive_controlled_block(
386 block_id: BlockId,
387 func: &HirFunction,
388 post_dominators: &react_compiler_hir::dominator::PostDominator,
389 reactive_map: &mut ReactivityMap,
390) -> bool {
391 let frontier = post_dominator_frontier(func, post_dominators, block_id);
392 for frontier_block_id in &frontier {
393 let control_block = func.body.blocks.get(frontier_block_id).unwrap();
394 match &control_block.terminal {
395 Terminal::If { test, .. } | Terminal::Branch { test, .. } => {
396 if reactive_map.is_reactive(test.identifier) {
397 return true;
398 }
399 }
400 Terminal::Switch { test, cases, .. } => {
401 if reactive_map.is_reactive(test.identifier) {
402 return true;
403 }
404 for case in cases {
405 if let Some(ref case_test) = case.test {
406 if reactive_map.is_reactive(case_test.identifier) {
407 return true;
408 }
409 }
410 }
411 }
412 _ => {}
413 }
414 }
415 false
416}
417
418use react_compiler_hir::is_use_operator_type;
423
424fn get_hook_kind_for_type<'a>(
425 env: &'a Environment,
426 ty: &Type,
427) -> Result<Option<&'a HookKind>, CompilerDiagnostic> {
428 env.get_hook_kind_for_type(ty)
429}
430
431fn is_stable_type(ty: &Type) -> bool {
432 match ty {
433 Type::Function {
434 shape_id: Some(id), ..
435 } => {
436 matches!(
437 id.as_str(),
438 "BuiltInSetState"
439 | "BuiltInSetActionState"
440 | "BuiltInDispatch"
441 | "BuiltInStartTransition"
442 | "BuiltInSetOptimistic"
443 )
444 }
445 Type::Object {
446 shape_id: Some(id),
447 } => {
448 matches!(id.as_str(), "BuiltInUseRefId")
449 }
450 _ => false,
451 }
452}
453
454fn is_stable_type_container(ty: &Type) -> bool {
455 match ty {
456 Type::Object {
457 shape_id: Some(id),
458 } => {
459 matches!(
460 id.as_str(),
461 "BuiltInUseState"
462 | "BuiltInUseActionState"
463 | "BuiltInUseReducer"
464 | "BuiltInUseOptimistic"
465 | "BuiltInUseTransition"
466 )
467 }
468 _ => false,
469 }
470}
471
472fn evaluates_to_stable_type_or_container(env: &Environment, callee_ty: &Type) -> bool {
473 if let Some(hook_kind) = get_hook_kind_for_type(env, callee_ty).ok().flatten() {
474 matches!(
475 hook_kind,
476 HookKind::UseState
477 | HookKind::UseReducer
478 | HookKind::UseActionState
479 | HookKind::UseRef
480 | HookKind::UseTransition
481 | HookKind::UseOptimistic
482 )
483 } else {
484 false
485 }
486}
487
488fn propagate_reactivity_to_inner_functions_outer(
493 func: &HirFunction,
494 env: &Environment,
495 reactive_map: &mut ReactivityMap,
496) {
497 for (_block_id, block) in &func.body.blocks {
498 for instr_id in &block.instructions {
499 let instr = &func.instructions[instr_id.0 as usize];
500 match &instr.value {
501 InstructionValue::FunctionExpression { lowered_func, .. }
502 | InstructionValue::ObjectMethod { lowered_func, .. } => {
503 propagate_reactivity_to_inner_functions_inner(
504 lowered_func.func,
505 env,
506 reactive_map,
507 );
508 }
509 _ => {}
510 }
511 }
512 }
513}
514
515fn propagate_reactivity_to_inner_functions_inner(
516 func_id: FunctionId,
517 env: &Environment,
518 reactive_map: &mut ReactivityMap,
519) {
520 let inner_func = &env.functions[func_id.0 as usize];
521
522 for (_block_id, block) in &inner_func.body.blocks {
523 for instr_id in &block.instructions {
524 let instr = &inner_func.instructions[instr_id.0 as usize];
525
526 for op in visitors::each_instruction_value_operand(&instr.value, env) {
527 reactive_map.is_reactive(op.identifier);
528 }
529
530 match &instr.value {
531 InstructionValue::FunctionExpression { lowered_func, .. }
532 | InstructionValue::ObjectMethod { lowered_func, .. } => {
533 propagate_reactivity_to_inner_functions_inner(
534 lowered_func.func,
535 env,
536 reactive_map,
537 );
538 }
539 _ => {}
540 }
541 }
542
543 for op in visitors::each_terminal_operand(&block.terminal) {
544 reactive_map.is_reactive(op.identifier);
545 }
546 }
547}
548
549fn apply_reactive_flags_replay(
554 func: &mut HirFunction,
555 env: &mut Environment,
556 reactive_map: &mut ReactivityMap,
557 stable_sidemap: &mut StableSidemap,
558 phi_operand_reactive: &HashMap<(BlockId, usize, usize), bool>,
559) {
560 let reactive_ids = build_reactive_id_set(reactive_map);
561
562 for param in &mut func.params {
564 let place = match param {
565 ParamPattern::Place(p) => p,
566 ParamPattern::Spread(s) => &mut s.place,
567 };
568 place.reactive = true;
569 }
570
571 let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
573
574 for block_id in &block_ids {
575 let block = func.body.blocks.get(block_id).unwrap();
576
577 let phi_count = block.phis.len();
579 for phi_idx in 0..phi_count {
580 let block = func.body.blocks.get_mut(block_id).unwrap();
581 let phi = &mut block.phis[phi_idx];
582
583 if reactive_ids.contains(&phi.place.identifier) {
584 phi.place.reactive = true;
585 }
586
587 for (op_idx, (_pred, operand)) in phi.operands.iter_mut().enumerate() {
588 if let Some(&is_reactive) =
589 phi_operand_reactive.get(&(*block_id, phi_idx, op_idx))
590 {
591 if is_reactive {
592 operand.reactive = true;
593 }
594 }
595 }
596 }
597
598 let block = func.body.blocks.get(block_id).unwrap();
600 let instr_ids: Vec<react_compiler_hir::InstructionId> = block.instructions.clone();
601
602 for instr_id in &instr_ids {
603 let instr = &func.instructions[instr_id.0 as usize];
604
605 let value_operand_ids: Vec<IdentifierId> =
607 visitors::each_instruction_value_operand(&instr.value, env)
608 .into_iter()
609 .map(|p| p.identifier)
610 .collect();
611 let mut has_reactive_input = false;
612 for &op_id in &value_operand_ids {
613 if reactive_ids.contains(&op_id) {
614 has_reactive_input = true;
615 }
616 }
617
618 match &instr.value {
620 InstructionValue::CallExpression { callee, .. } => {
621 let callee_ty = &env.types
622 [env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
623 if get_hook_kind_for_type(env, callee_ty).ok().flatten().is_some()
624 || is_use_operator_type(callee_ty)
625 {
626 has_reactive_input = true;
627 }
628 }
629 InstructionValue::MethodCall { property, .. } => {
630 let property_ty = &env.types
631 [env.identifiers[property.identifier.0 as usize].type_.0 as usize];
632 if get_hook_kind_for_type(env, property_ty)
633 .ok()
634 .flatten()
635 .is_some()
636 || is_use_operator_type(property_ty)
637 {
638 has_reactive_input = true;
639 }
640 }
641 _ => {}
642 }
643
644 let instr = &mut func.instructions[instr_id.0 as usize];
646 visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| {
647 if reactive_ids.contains(&place.identifier) {
648 place.reactive = true;
649 }
650 });
651 if let InstructionValue::FunctionExpression { lowered_func, .. }
653 | InstructionValue::ObjectMethod { lowered_func, .. } = &mut instr.value
654 {
655 let inner_func = &mut env.functions[lowered_func.func.0 as usize];
656 for ctx in &mut inner_func.context {
657 if reactive_ids.contains(&ctx.identifier) {
658 ctx.reactive = true;
659 }
660 }
661 }
662
663 if has_reactive_input {
665 let lvalue_id = instr.lvalue.identifier;
666 if !stable_sidemap.is_stable(lvalue_id) && reactive_ids.contains(&lvalue_id) {
667 instr.lvalue.reactive = true;
668 }
669 match &mut instr.value {
672 InstructionValue::DeclareLocal { lvalue, .. }
673 | InstructionValue::DeclareContext { lvalue, .. }
674 | InstructionValue::StoreLocal { lvalue, .. }
675 | InstructionValue::StoreContext { lvalue, .. } => {
676 let id = lvalue.place.identifier;
677 if !stable_sidemap.is_stable(id) && reactive_ids.contains(&id) {
678 lvalue.place.reactive = true;
679 }
680 }
681 InstructionValue::Destructure { lvalue, .. } => {
682 visitors::for_each_pattern_operand_mut(
683 &mut lvalue.pattern,
684 &mut |place| {
685 if !stable_sidemap.is_stable(place.identifier)
686 && reactive_ids.contains(&place.identifier)
687 {
688 place.reactive = true;
689 }
690 },
691 );
692 }
693 InstructionValue::PrefixUpdate { lvalue, .. }
694 | InstructionValue::PostfixUpdate { lvalue, .. } => {
695 let id = lvalue.identifier;
696 if !stable_sidemap.is_stable(id) && reactive_ids.contains(&id) {
697 lvalue.reactive = true;
698 }
699 }
700 _ => {}
701 }
702 }
703 }
704
705 let block = func.body.blocks.get_mut(block_id).unwrap();
707 visitors::for_each_terminal_operand_mut(&mut block.terminal, &mut |place| {
708 if reactive_ids.contains(&place.identifier) {
709 place.reactive = true;
710 }
711 });
712 }
713
714 apply_reactive_flags_to_inner_functions(func, env, &reactive_ids);
716}
717
718fn build_reactive_id_set(reactive_map: &mut ReactivityMap) -> HashSet<IdentifierId> {
719 let mut result = HashSet::new();
720 for &id in &reactive_map.reactive {
721 result.insert(id);
722 }
723 let reactive = &reactive_map.reactive;
724 reactive_map.aliased_identifiers.for_each(|id, canonical| {
725 if reactive.contains(&canonical) {
726 result.insert(id);
727 }
728 });
729 result
730}
731
732fn apply_reactive_flags_to_inner_functions(
733 func: &HirFunction,
734 env: &mut Environment,
735 reactive_ids: &HashSet<IdentifierId>,
736) {
737 for (_block_id, block) in &func.body.blocks {
738 for instr_id in &block.instructions {
739 let instr = &func.instructions[instr_id.0 as usize];
740 match &instr.value {
741 InstructionValue::FunctionExpression { lowered_func, .. }
742 | InstructionValue::ObjectMethod { lowered_func, .. } => {
743 apply_reactive_flags_to_inner_func(lowered_func.func, env, reactive_ids);
744 }
745 _ => {}
746 }
747 }
748 }
749}
750
751fn apply_reactive_flags_to_inner_func(
752 func_id: FunctionId,
753 env: &mut Environment,
754 reactive_ids: &HashSet<IdentifierId>,
755) {
756 let nested_func_ids: Vec<FunctionId> = {
758 let func = &env.functions[func_id.0 as usize];
759 let mut ids = Vec::new();
760 for (_block_id, block) in &func.body.blocks {
761 for instr_id in &block.instructions {
762 let instr = &func.instructions[instr_id.0 as usize];
763 match &instr.value {
764 InstructionValue::FunctionExpression { lowered_func, .. }
765 | InstructionValue::ObjectMethod { lowered_func, .. } => {
766 ids.push(lowered_func.func);
767 }
768 _ => {}
769 }
770 }
771 }
772 ids
773 };
774
775 let inner_func = &mut env.functions[func_id.0 as usize];
777 for (_block_id, block) in &mut inner_func.body.blocks {
778 for instr_id in &block.instructions {
779 let instr = &mut inner_func.instructions[instr_id.0 as usize];
780 visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| {
781 if reactive_ids.contains(&place.identifier) {
782 place.reactive = true;
783 }
784 });
785 }
786 visitors::for_each_terminal_operand_mut(&mut block.terminal, &mut |place| {
787 if reactive_ids.contains(&place.identifier) {
788 place.reactive = true;
789 }
790 });
791 }
792
793 for nested_id in nested_func_ids {
795 let nested_func = &mut env.functions[nested_id.0 as usize];
796 for ctx in &mut nested_func.context {
797 if reactive_ids.contains(&ctx.identifier) {
798 ctx.reactive = true;
799 }
800 }
801 apply_reactive_flags_to_inner_func(nested_id, env, reactive_ids);
802 }
803}