1use std::collections::{BTreeSet, HashMap, HashSet};
16use indexmap::IndexMap;
17
18use react_compiler_hir::environment::Environment;
19use react_compiler_hir::{
20 BasicBlock, BlockId, DeclarationId, DependencyPathEntry, EvaluationOrder,
21 FunctionId, GotoVariant, HirFunction, IdentifierId, Instruction, InstructionId,
22 InstructionKind, InstructionValue, MutableRange, ParamPattern,
23 Place, PlaceOrSpread, PropertyLiteral, ReactFunctionType, ReactiveScopeDependency,
24 ScopeId, Terminal, Type, visitors,
25};
26use react_compiler_hir::visitors::{ScopeBlockTraversal, ScopeBlockInfo};
27
28pub fn propagate_scope_dependencies_hir(func: &mut HirFunction, env: &mut Environment) {
35 let used_outside_declaring_scope = find_temporaries_used_outside_declaring_scope(func, env);
36 let temporaries = collect_temporaries_sidemap(func, env, &used_outside_declaring_scope);
37
38 let OptionalChainSidemap {
39 temporaries_read_in_optional,
40 processed_instrs_in_optional,
41 hoistable_objects,
42 } = collect_optional_chain_sidemap(func, env);
43
44 let hoistable_property_loads = {
45 let (working, registry) = collect_hoistable_and_propagate(func, env, &temporaries, &hoistable_objects);
46 let mut keyed: HashMap<ScopeId, Vec<ReactiveScopeDependency>> = HashMap::new();
48 for (_block_id, block) in &func.body.blocks {
49 if let Terminal::Scope { scope, block: inner_block, .. } = &block.terminal {
50 if let Some(node_indices) = working.get(inner_block) {
51 let deps: Vec<ReactiveScopeDependency> = node_indices
52 .iter()
53 .map(|&idx| registry.nodes[idx].full_path.clone())
54 .collect();
55 keyed.insert(*scope, deps);
56 }
57 }
58 }
59 keyed
60 };
61
62 let mut merged_temporaries = temporaries;
64 for (k, v) in temporaries_read_in_optional {
65 merged_temporaries.insert(k, v);
66 }
67
68 let scope_deps = collect_dependencies(
69 func,
70 env,
71 &used_outside_declaring_scope,
72 &merged_temporaries,
73 &processed_instrs_in_optional,
74 );
75
76 for (scope_id, deps) in &scope_deps {
78 if deps.is_empty() {
79 continue;
80 }
81
82 let hoistables = hoistable_property_loads.get(scope_id);
83 let hoistables = hoistables.expect(
84 "[PropagateScopeDependencies] Scope not found in tracked blocks",
85 );
86
87 let mut tree = ReactiveScopeDependencyTreeHIR::new(
89 hoistables.iter(),
90 env,
91 );
92 for dep in deps {
93 tree.add_dependency(dep.clone(), env);
94 }
95
96 let candidates = tree.derive_minimal_dependencies(env);
98 let scope = &mut env.scopes[scope_id.0 as usize];
99 for candidate_dep in candidates {
100 let already_exists = scope.dependencies.iter().any(|existing_dep| {
101 let existing_decl_id = env.identifiers[existing_dep.identifier.0 as usize].declaration_id;
102 let candidate_decl_id = env.identifiers[candidate_dep.identifier.0 as usize].declaration_id;
103 existing_decl_id == candidate_decl_id
104 && are_equal_paths(&existing_dep.path, &candidate_dep.path)
105 });
106 if !already_exists {
107 scope.dependencies.push(candidate_dep);
108 }
109 }
110 }
111}
112
113fn are_equal_paths(a: &[DependencyPathEntry], b: &[DependencyPathEntry]) -> bool {
114 a.len() == b.len()
115 && a.iter().zip(b.iter()).all(|(ai, bi)| {
116 ai.property == bi.property && ai.optional == bi.optional
117 })
118}
119
120fn find_temporaries_used_outside_declaring_scope(
126 func: &HirFunction,
127 env: &Environment,
128) -> HashSet<DeclarationId> {
129 let mut declarations: HashMap<DeclarationId, ScopeId> = HashMap::new();
130 let mut pruned_scopes: HashSet<ScopeId> = HashSet::new();
131 let mut traversal = ScopeBlockTraversal::new();
132 let mut used_outside_declaring_scope: HashSet<DeclarationId> = HashSet::new();
133
134 let handle_place = |place_id: IdentifierId,
135 declarations: &HashMap<DeclarationId, ScopeId>,
136 traversal: &ScopeBlockTraversal,
137 pruned_scopes: &HashSet<ScopeId>,
138 used_outside: &mut HashSet<DeclarationId>,
139 env: &Environment| {
140 let decl_id = env.identifiers[place_id.0 as usize].declaration_id;
141 if let Some(&declaring_scope) = declarations.get(&decl_id) {
142 if !traversal.is_scope_active(declaring_scope) && !pruned_scopes.contains(&declaring_scope) {
143 used_outside.insert(decl_id);
144 }
145 }
146 };
147
148 for (block_id, block) in &func.body.blocks {
149 traversal.record_scopes(block);
151
152 let scope_start_info = traversal.block_infos.get(block_id);
153 if let Some(ScopeBlockInfo::Begin { scope, pruned: true, .. }) = scope_start_info {
154 pruned_scopes.insert(*scope);
155 }
156
157 for &instr_id in &block.instructions {
158 let instr = &func.instructions[instr_id.0 as usize];
159 for op_id in visitors::each_instruction_operand(instr, env).into_iter().map(|p| p.identifier).collect::<Vec<_>>() {
161 handle_place(
162 op_id,
163 &declarations,
164 &traversal,
165 &pruned_scopes,
166 &mut used_outside_declaring_scope,
167 env,
168 );
169 }
170 let current_scope = traversal.current_scope();
172 if let Some(scope) = current_scope {
173 if !pruned_scopes.contains(&scope) {
174 match &instr.value {
175 InstructionValue::LoadLocal { .. }
176 | InstructionValue::LoadContext { .. }
177 | InstructionValue::PropertyLoad { .. } => {
178 let decl_id = env.identifiers[instr.lvalue.identifier.0 as usize].declaration_id;
179 declarations.insert(decl_id, scope);
180 }
181 _ => {}
182 }
183 }
184 }
185 }
186
187 for op_id in visitors::each_terminal_operand(&block.terminal).into_iter().map(|p| p.identifier).collect::<Vec<_>>() {
189 handle_place(
190 op_id,
191 &declarations,
192 &traversal,
193 &pruned_scopes,
194 &mut used_outside_declaring_scope,
195 env,
196 );
197 }
198 }
199
200 used_outside_declaring_scope
201}
202
203fn collect_temporaries_sidemap(
209 func: &HirFunction,
210 env: &Environment,
211 used_outside_declaring_scope: &HashSet<DeclarationId>,
212) -> HashMap<IdentifierId, ReactiveScopeDependency> {
213 let mut temporaries = HashMap::new();
214 collect_temporaries_sidemap_impl(
215 func,
216 env,
217 used_outside_declaring_scope,
218 &mut temporaries,
219 None,
220 );
221 temporaries
222}
223
224fn is_load_context_mutable(
226 value: &InstructionValue,
227 id: EvaluationOrder,
228 env: &Environment,
229) -> bool {
230 if let InstructionValue::LoadContext { place, .. } = value {
231 if let Some(scope_id) = env.identifiers[place.identifier.0 as usize].scope {
232 let scope_range = &env.scopes[scope_id.0 as usize].range;
233 return id >= scope_range.end;
234 }
235 }
236 false
237}
238
239fn convert_hoisted_lvalue_kind(kind: InstructionKind) -> Option<InstructionKind> {
241 match kind {
242 InstructionKind::HoistedLet => Some(InstructionKind::Let),
243 InstructionKind::HoistedConst => Some(InstructionKind::Const),
244 InstructionKind::HoistedFunction => Some(InstructionKind::Function),
245 _ => None,
246 }
247}
248
249fn collect_temporaries_sidemap_impl(
251 func: &HirFunction,
252 env: &Environment,
253 used_outside_declaring_scope: &HashSet<DeclarationId>,
254 temporaries: &mut HashMap<IdentifierId, ReactiveScopeDependency>,
255 inner_fn_context: Option<EvaluationOrder>,
256) {
257 for (_block_id, block) in &func.body.blocks {
258 for &instr_id in &block.instructions {
259 let instr = &func.instructions[instr_id.0 as usize];
260 let instr_eval_order = if let Some(outer_id) = inner_fn_context {
261 outer_id
262 } else {
263 instr.id
264 };
265 let lvalue_decl_id = env.identifiers[instr.lvalue.identifier.0 as usize].declaration_id;
266 let used_outside = used_outside_declaring_scope.contains(&lvalue_decl_id);
267
268 match &instr.value {
269 InstructionValue::PropertyLoad {
270 object, property, loc, ..
271 } if !used_outside => {
272 if inner_fn_context.is_none()
273 || temporaries.contains_key(&object.identifier)
274 {
275 let prop = get_property(object, property, false, *loc, temporaries, env);
276 temporaries.insert(instr.lvalue.identifier, prop);
277 }
278 }
279 InstructionValue::LoadLocal { place, loc, .. }
280 if env.identifiers[instr.lvalue.identifier.0 as usize].name.is_none()
281 && env.identifiers[place.identifier.0 as usize].name.is_some()
282 && !used_outside =>
283 {
284 if inner_fn_context.is_none()
285 || func
286 .context
287 .iter()
288 .any(|ctx| ctx.identifier == place.identifier)
289 {
290 temporaries.insert(
291 instr.lvalue.identifier,
292 ReactiveScopeDependency {
293 identifier: place.identifier,
294 reactive: place.reactive,
295 path: vec![],
296 loc: *loc,
297 },
298 );
299 }
300 }
301 value @ InstructionValue::LoadContext { place, loc, .. }
302 if is_load_context_mutable(value, instr_eval_order, env)
303 && env.identifiers[instr.lvalue.identifier.0 as usize].name.is_none()
304 && env.identifiers[place.identifier.0 as usize].name.is_some()
305 && !used_outside =>
306 {
307 if inner_fn_context.is_none()
308 || func
309 .context
310 .iter()
311 .any(|ctx| ctx.identifier == place.identifier)
312 {
313 temporaries.insert(
314 instr.lvalue.identifier,
315 ReactiveScopeDependency {
316 identifier: place.identifier,
317 reactive: place.reactive,
318 path: vec![],
319 loc: *loc,
320 },
321 );
322 }
323 }
324 InstructionValue::FunctionExpression { lowered_func, .. }
325 | InstructionValue::ObjectMethod { lowered_func, .. } => {
326 let inner_func = &env.functions[lowered_func.func.0 as usize];
327 let ctx = inner_fn_context.unwrap_or(instr.id);
328 collect_temporaries_sidemap_impl(
329 inner_func,
330 env,
331 used_outside_declaring_scope,
332 temporaries,
333 Some(ctx),
334 );
335 }
336 _ => {}
337 }
338 }
339 }
340}
341
342fn get_property(
344 object: &Place,
345 property_name: &PropertyLiteral,
346 optional: bool,
347 loc: Option<react_compiler_hir::SourceLocation>,
348 temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
349 _env: &Environment,
350) -> ReactiveScopeDependency {
351 let resolved = temporaries.get(&object.identifier);
352 if let Some(resolved) = resolved {
353 let mut path = resolved.path.clone();
354 path.push(DependencyPathEntry {
355 property: property_name.clone(),
356 optional,
357 loc,
358 });
359 ReactiveScopeDependency {
360 identifier: resolved.identifier,
361 reactive: resolved.reactive,
362 path,
363 loc,
364 }
365 } else {
366 ReactiveScopeDependency {
367 identifier: object.identifier,
368 reactive: object.reactive,
369 path: vec![DependencyPathEntry {
370 property: property_name.clone(),
371 optional,
372 loc,
373 }],
374 loc,
375 }
376 }
377}
378
379struct OptionalChainSidemap {
384 temporaries_read_in_optional: HashMap<IdentifierId, ReactiveScopeDependency>,
385 processed_instrs_in_optional: HashSet<ProcessedInstr>,
386 hoistable_objects: HashMap<BlockId, ReactiveScopeDependency>,
387}
388
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
395enum ProcessedInstr {
396 Instruction(IdentifierId),
397 Terminal(BlockId),
398}
399
400fn collect_optional_chain_sidemap(
401 func: &HirFunction,
402 env: &Environment,
403) -> OptionalChainSidemap {
404 let mut ctx = OptionalTraversalContext {
405 seen_optionals: HashSet::new(),
406 processed_instrs_in_optional: HashSet::new(),
407 temporaries_read_in_optional: HashMap::new(),
408 hoistable_objects: HashMap::new(),
409 };
410
411 traverse_function_optional(func, env, &mut ctx);
412
413 OptionalChainSidemap {
414 temporaries_read_in_optional: ctx.temporaries_read_in_optional,
415 processed_instrs_in_optional: ctx.processed_instrs_in_optional,
416 hoistable_objects: ctx.hoistable_objects,
417 }
418}
419
420struct OptionalTraversalContext {
421 seen_optionals: HashSet<BlockId>,
422 processed_instrs_in_optional: HashSet<ProcessedInstr>,
423 temporaries_read_in_optional: HashMap<IdentifierId, ReactiveScopeDependency>,
424 hoistable_objects: HashMap<BlockId, ReactiveScopeDependency>,
425}
426
427fn traverse_function_optional(
428 func: &HirFunction,
429 env: &Environment,
430 ctx: &mut OptionalTraversalContext,
431) {
432 for (_block_id, block) in &func.body.blocks {
433 for &instr_id in &block.instructions {
434 let instr = &func.instructions[instr_id.0 as usize];
435 match &instr.value {
436 InstructionValue::FunctionExpression { lowered_func, .. }
437 | InstructionValue::ObjectMethod { lowered_func, .. } => {
438 let inner_func = &env.functions[lowered_func.func.0 as usize];
439 traverse_function_optional(inner_func, env, ctx);
440 }
441 _ => {}
442 }
443 }
444 if let Terminal::Optional { .. } = &block.terminal {
445 if !ctx.seen_optionals.contains(&block.id) {
446 traverse_optional_block(block, func, env, ctx, None);
447 }
448 }
449 }
450}
451
452struct MatchConsequentResult {
453 consequent_id: IdentifierId,
454 property: PropertyLiteral,
455 property_id: IdentifierId,
456 store_local_lvalue_id: IdentifierId,
457 consequent_goto: BlockId,
458 property_load_loc: Option<react_compiler_hir::SourceLocation>,
459}
460
461fn match_optional_test_block(
462 test: &Terminal,
463 func: &HirFunction,
464 _env: &Environment,
465) -> Option<MatchConsequentResult> {
466 let (test_place, consequent_block_id, alternate_block_id) = match test {
467 Terminal::Branch {
468 test,
469 consequent,
470 alternate,
471 ..
472 } => (test, *consequent, *alternate),
473 _ => return None,
474 };
475
476 let consequent_block = func.body.blocks.get(&consequent_block_id)?;
477 if consequent_block.instructions.len() != 2 {
478 return None;
479 }
480
481 let instr0 = &func.instructions[consequent_block.instructions[0].0 as usize];
482 let instr1 = &func.instructions[consequent_block.instructions[1].0 as usize];
483
484 let (property_load_object, property, property_load_loc) = match &instr0.value {
485 InstructionValue::PropertyLoad {
486 object,
487 property,
488 loc,
489 } => (object, property, loc),
490 _ => return None,
491 };
492
493 let store_local_value = match &instr1.value {
494 InstructionValue::StoreLocal { value, lvalue, .. } => {
495 if value.identifier != instr0.lvalue.identifier {
497 return None;
498 }
499 &lvalue.place
500 }
501 _ => return None,
502 };
503
504 if property_load_object.identifier != test_place.identifier {
506 return None;
507 }
508
509 match &consequent_block.terminal {
511 Terminal::Goto {
512 variant: GotoVariant::Break,
513 block: goto_block,
514 ..
515 } => {
516 let alternate_block = func.body.blocks.get(&alternate_block_id)?;
518 if alternate_block.instructions.len() != 2 {
519 return None;
520 }
521 let alt_instr0 = &func.instructions[alternate_block.instructions[0].0 as usize];
522 let alt_instr1 = &func.instructions[alternate_block.instructions[1].0 as usize];
523 match (&alt_instr0.value, &alt_instr1.value) {
524 (InstructionValue::Primitive { .. }, InstructionValue::StoreLocal { .. }) => {}
525 _ => return None,
526 }
527
528 Some(MatchConsequentResult {
529 consequent_id: store_local_value.identifier,
530 property: property.clone(),
531 property_id: instr0.lvalue.identifier,
532 store_local_lvalue_id: instr1.lvalue.identifier,
533 consequent_goto: *goto_block,
534 property_load_loc: *property_load_loc,
535 })
536 }
537 _ => None,
538 }
539}
540
541fn traverse_optional_block(
542 optional_block: &BasicBlock,
543 func: &HirFunction,
544 env: &Environment,
545 ctx: &mut OptionalTraversalContext,
546 outer_alternate: Option<BlockId>,
547) -> Option<IdentifierId> {
548 ctx.seen_optionals.insert(optional_block.id);
549
550 let (test_block_id, is_optional, fallthrough_block_id) = match &optional_block.terminal {
551 Terminal::Optional {
552 test,
553 optional,
554 fallthrough,
555 ..
556 } => (*test, *optional, *fallthrough),
557 _ => return None,
558 };
559
560 let maybe_test_block = func.body.blocks.get(&test_block_id)?;
561
562 let (test_terminal, base_object) = match &maybe_test_block.terminal {
563 Terminal::Branch { .. } => {
564 if !is_optional {
566 return None;
567 }
568 if maybe_test_block.instructions.is_empty() {
570 return None;
571 }
572 let first_instr = &func.instructions[maybe_test_block.instructions[0].0 as usize];
573 if !matches!(&first_instr.value, InstructionValue::LoadLocal { .. }) {
574 return None;
575 }
576
577 let mut path: Vec<DependencyPathEntry> = Vec::new();
578 for i in 1..maybe_test_block.instructions.len() {
579 let curr_instr = &func.instructions[maybe_test_block.instructions[i].0 as usize];
580 let prev_instr =
581 &func.instructions[maybe_test_block.instructions[i - 1].0 as usize];
582 match &curr_instr.value {
583 InstructionValue::PropertyLoad {
584 object, property, loc, ..
585 } if object.identifier == prev_instr.lvalue.identifier => {
586 path.push(DependencyPathEntry {
587 property: property.clone(),
588 optional: false,
589 loc: *loc,
590 });
591 }
592 _ => return None,
593 }
594 }
595
596 let last_instr_id = *maybe_test_block.instructions.last().unwrap();
598 let last_instr = &func.instructions[last_instr_id.0 as usize];
599 let test_ident = match &maybe_test_block.terminal {
600 Terminal::Branch { test, .. } => test.identifier,
601 _ => return None,
602 };
603 if test_ident != last_instr.lvalue.identifier {
604 return None;
605 }
606
607 let first_place = match &first_instr.value {
608 InstructionValue::LoadLocal { place, .. } => place,
609 _ => return None,
610 };
611
612 let base = ReactiveScopeDependency {
613 identifier: first_place.identifier,
614 reactive: first_place.reactive,
615 path,
616 loc: first_place.loc,
617 };
618 (&maybe_test_block.terminal, base)
619 }
620 Terminal::Optional {
621 fallthrough: inner_fallthrough,
622 optional: _inner_optional,
623 ..
624 } => {
625 let test_block = func.body.blocks.get(inner_fallthrough)?;
626 if !matches!(&test_block.terminal, Terminal::Branch { .. }) {
627 return None;
628 }
629
630 let inner_alternate = match &test_block.terminal {
632 Terminal::Branch { alternate, .. } => Some(*alternate),
633 _ => None,
634 };
635 let inner_optional_result =
636 traverse_optional_block(maybe_test_block, func, env, ctx, inner_alternate);
637 let inner_optional_id = inner_optional_result?;
638
639 let test_ident = match &test_block.terminal {
641 Terminal::Branch { test, .. } => test.identifier,
642 _ => return None,
643 };
644 if test_ident != inner_optional_id {
645 return None;
646 }
647
648 if !is_optional {
649 if let Some(inner_dep) = ctx.temporaries_read_in_optional.get(&inner_optional_id) {
651 ctx.hoistable_objects
652 .insert(optional_block.id, inner_dep.clone());
653 }
654 }
655
656 let base = ctx
657 .temporaries_read_in_optional
658 .get(&inner_optional_id)?
659 .clone();
660 (&test_block.terminal, base)
661 }
662 _ => return None,
663 };
664
665 if let Some(outer_alt) = outer_alternate {
667 let test_alternate = match test_terminal {
668 Terminal::Branch { alternate, .. } => *alternate,
669 _ => return None,
670 };
671 if test_alternate == outer_alt {
672 if !optional_block.instructions.is_empty() {
674 return None;
675 }
676 }
677 }
678
679 let match_result = match_optional_test_block(test_terminal, func, env)?;
680
681 if match_result.consequent_goto != fallthrough_block_id {
683 return None;
684 }
685
686 let load = ReactiveScopeDependency {
687 identifier: base_object.identifier,
688 reactive: base_object.reactive,
689 path: {
690 let mut p = base_object.path.clone();
691 p.push(DependencyPathEntry {
692 property: match_result.property.clone(),
693 optional: is_optional,
694 loc: match_result.property_load_loc,
695 });
696 p
697 },
698 loc: match_result.property_load_loc,
699 };
700
701 ctx.processed_instrs_in_optional
702 .insert(ProcessedInstr::Instruction(match_result.store_local_lvalue_id));
703 ctx.processed_instrs_in_optional
704 .insert(ProcessedInstr::Terminal(match &test_terminal {
705 Terminal::Branch { .. } => {
706 let mut found_block = BlockId(0);
738 for (bid, blk) in &func.body.blocks {
739 if std::ptr::eq(&blk.terminal, test_terminal) {
740 found_block = *bid;
741 break;
742 }
743 }
744 found_block
745 }
746 _ => BlockId(0),
747 }));
748 ctx.temporaries_read_in_optional
749 .insert(match_result.consequent_id, load.clone());
750 ctx.temporaries_read_in_optional
751 .insert(match_result.property_id, load);
752
753 Some(match_result.consequent_id)
754}
755
756#[derive(Debug, Clone)]
761struct PropertyPathNode {
762 properties: HashMap<PropertyLiteral, usize>, optional_properties: HashMap<PropertyLiteral, usize>, #[allow(dead_code)]
765 parent: Option<usize>,
766 full_path: ReactiveScopeDependency,
767 has_optional: bool,
768 #[allow(dead_code)]
769 root: Option<IdentifierId>,
770}
771
772struct PropertyPathRegistry {
773 nodes: Vec<PropertyPathNode>,
774 roots: HashMap<IdentifierId, usize>,
775}
776
777impl PropertyPathRegistry {
778 fn new() -> Self {
779 Self {
780 nodes: Vec::new(),
781 roots: HashMap::new(),
782 }
783 }
784
785 fn get_or_create_identifier(
786 &mut self,
787 identifier_id: IdentifierId,
788 reactive: bool,
789 loc: Option<react_compiler_hir::SourceLocation>,
790 ) -> usize {
791 if let Some(&idx) = self.roots.get(&identifier_id) {
792 return idx;
793 }
794 let idx = self.nodes.len();
795 self.nodes.push(PropertyPathNode {
796 properties: HashMap::new(),
797 optional_properties: HashMap::new(),
798 parent: None,
799 full_path: ReactiveScopeDependency {
800 identifier: identifier_id,
801 reactive,
802 path: vec![],
803 loc,
804 },
805 has_optional: false,
806 root: Some(identifier_id),
807 });
808 self.roots.insert(identifier_id, idx);
809 idx
810 }
811
812 fn get_or_create_property_entry(
813 &mut self,
814 parent_idx: usize,
815 entry: &DependencyPathEntry,
816 ) -> usize {
817 let map_key = entry.property.clone();
818 let existing = if entry.optional {
819 self.nodes[parent_idx].optional_properties.get(&map_key).copied()
820 } else {
821 self.nodes[parent_idx].properties.get(&map_key).copied()
822 };
823 if let Some(idx) = existing {
824 return idx;
825 }
826 let parent_full_path = self.nodes[parent_idx].full_path.clone();
827 let parent_has_optional = self.nodes[parent_idx].has_optional;
828 let idx = self.nodes.len();
829 let mut new_path = parent_full_path.path.clone();
830 new_path.push(entry.clone());
831 self.nodes.push(PropertyPathNode {
832 properties: HashMap::new(),
833 optional_properties: HashMap::new(),
834 parent: Some(parent_idx),
835 full_path: ReactiveScopeDependency {
836 identifier: parent_full_path.identifier,
837 reactive: parent_full_path.reactive,
838 path: new_path,
839 loc: entry.loc,
840 },
841 has_optional: parent_has_optional || entry.optional,
842 root: None,
843 });
844 if entry.optional {
845 self.nodes[parent_idx]
846 .optional_properties
847 .insert(map_key, idx);
848 } else {
849 self.nodes[parent_idx].properties.insert(map_key, idx);
850 }
851 idx
852 }
853
854 fn get_or_create_property(&mut self, dep: &ReactiveScopeDependency) -> usize {
855 let mut curr = self.get_or_create_identifier(dep.identifier, dep.reactive, dep.loc);
856 for entry in &dep.path {
857 curr = self.get_or_create_property_entry(curr, entry);
858 }
859 curr
860 }
861}
862
863fn reduce_maybe_optional_chains(
872 nodes: &mut BTreeSet<usize>,
873 registry: &mut PropertyPathRegistry,
874) {
875 let mut optional_chain_nodes: BTreeSet<usize> = nodes
877 .iter()
878 .copied()
879 .filter(|&idx| registry.nodes[idx].has_optional)
880 .collect();
881
882 if optional_chain_nodes.is_empty() {
883 return;
884 }
885
886 loop {
887 let mut changed = false;
888
889 let to_process: Vec<usize> = optional_chain_nodes.iter().copied().collect();
891
892 for original_idx in to_process {
893 let full_path = registry.nodes[original_idx].full_path.clone();
894
895 let mut curr_node = registry.get_or_create_identifier(
896 full_path.identifier,
897 full_path.reactive,
898 full_path.loc,
899 );
900
901 for entry in &full_path.path {
902 let next_entry = if entry.optional && nodes.contains(&curr_node) {
904 DependencyPathEntry {
905 property: entry.property.clone(),
906 optional: false,
907 loc: entry.loc,
908 }
909 } else {
910 entry.clone()
911 };
912 curr_node = registry.get_or_create_property_entry(curr_node, &next_entry);
913 }
914
915 if curr_node != original_idx {
916 changed = true;
917 optional_chain_nodes.remove(&original_idx);
918 optional_chain_nodes.insert(curr_node);
919 nodes.remove(&original_idx);
920 nodes.insert(curr_node);
921 }
922 }
923
924 if !changed {
925 break;
926 }
927 }
928}
929
930#[derive(Debug, Clone)]
931struct BlockInfo {
932 assumed_non_null_objects: BTreeSet<usize>, }
934
935#[allow(dead_code)]
936fn collect_hoistable_property_loads(
937 func: &HirFunction,
938 env: &Environment,
939 temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
940 hoistable_from_optionals: &HashMap<BlockId, ReactiveScopeDependency>,
941) -> HashMap<BlockId, BlockInfo> {
942 let mut registry = PropertyPathRegistry::new();
943 let known_immutable_identifiers: HashSet<IdentifierId> = if func.fn_type == ReactFunctionType::Component
944 || func.fn_type == ReactFunctionType::Hook
945 {
946 func.params
947 .iter()
948 .filter_map(|p| match p {
949 ParamPattern::Place(place) => Some(place.identifier),
950 _ => None,
951 })
952 .collect()
953 } else {
954 HashSet::new()
955 };
956
957 let assumed_invoked_fns = get_assumed_invoked_functions(func, env);
958 let ctx = CollectHoistableContext {
959 temporaries,
960 known_immutable_identifiers: &known_immutable_identifiers,
961 hoistable_from_optionals,
962 nested_fn_immutable_context: None,
963 assumed_invoked_fns: &assumed_invoked_fns,
964 };
965
966 collect_hoistable_property_loads_impl(func, env, &ctx, &mut registry)
967}
968
969struct CollectHoistableContext<'a> {
970 temporaries: &'a HashMap<IdentifierId, ReactiveScopeDependency>,
971 known_immutable_identifiers: &'a HashSet<IdentifierId>,
972 hoistable_from_optionals: &'a HashMap<BlockId, ReactiveScopeDependency>,
973 nested_fn_immutable_context: Option<&'a HashSet<IdentifierId>>,
974 assumed_invoked_fns: &'a HashSet<FunctionId>,
975}
976
977fn is_immutable_at_instr(
978 identifier_id: IdentifierId,
979 instr_id: EvaluationOrder,
980 env: &Environment,
981 ctx: &CollectHoistableContext,
982) -> bool {
983 if let Some(nested_ctx) = ctx.nested_fn_immutable_context {
984 return nested_ctx.contains(&identifier_id);
985 }
986 let ident = &env.identifiers[identifier_id.0 as usize];
987 let mutable_at_instr = ident.mutable_range.end > EvaluationOrder(ident.mutable_range.start.0 + 1)
988 && ident.scope.is_some()
989 && {
990 let scope = &env.scopes[ident.scope.unwrap().0 as usize];
991 in_range(instr_id, &scope.range)
992 };
993 !mutable_at_instr || ctx.known_immutable_identifiers.contains(&identifier_id)
994}
995
996fn in_range(id: EvaluationOrder, range: &MutableRange) -> bool {
997 id >= range.start && id < range.end
998}
999
1000fn get_maybe_non_null_in_instruction(
1001 value: &InstructionValue,
1002 temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
1003) -> Option<ReactiveScopeDependency> {
1004 match value {
1005 InstructionValue::PropertyLoad { object, .. } => {
1006 Some(
1007 temporaries
1008 .get(&object.identifier)
1009 .cloned()
1010 .unwrap_or_else(|| ReactiveScopeDependency {
1011 identifier: object.identifier,
1012 reactive: object.reactive,
1013 path: vec![],
1014 loc: object.loc,
1015 }),
1016 )
1017 }
1018 InstructionValue::Destructure { value: val, .. } => {
1019 temporaries.get(&val.identifier).cloned()
1020 }
1021 InstructionValue::ComputedLoad { object, .. } => {
1022 temporaries.get(&object.identifier).cloned()
1023 }
1024 _ => None,
1025 }
1026}
1027
1028#[allow(dead_code)]
1029fn collect_hoistable_property_loads_impl(
1030 func: &HirFunction,
1031 env: &Environment,
1032 ctx: &CollectHoistableContext,
1033 registry: &mut PropertyPathRegistry,
1034) -> HashMap<BlockId, BlockInfo> {
1035 let nodes = collect_non_nulls_in_blocks(func, env, ctx, registry);
1036 let working = propagate_non_null(func, &nodes, registry);
1037 working
1039 .into_iter()
1040 .map(|(k, v)| (k, BlockInfo { assumed_non_null_objects: v }))
1041 .collect()
1042}
1043
1044fn get_assumed_invoked_functions(
1049 func: &HirFunction,
1050 env: &Environment,
1051) -> HashSet<FunctionId> {
1052 let mut temporaries: HashMap<IdentifierId, (FunctionId, HashSet<FunctionId>)> = HashMap::new();
1053 get_assumed_invoked_functions_impl(func, env, &mut temporaries)
1054}
1055
1056fn get_assumed_invoked_functions_impl(
1057 func: &HirFunction,
1058 env: &Environment,
1059 temporaries: &mut HashMap<IdentifierId, (FunctionId, HashSet<FunctionId>)>,
1060) -> HashSet<FunctionId> {
1061 let mut hoistable: HashSet<FunctionId> = HashSet::new();
1062
1063 for (_block_id, block) in &func.body.blocks {
1065 for &instr_id in &block.instructions {
1066 let instr = &func.instructions[instr_id.0 as usize];
1067 match &instr.value {
1068 InstructionValue::FunctionExpression { lowered_func, .. } => {
1069 temporaries.insert(
1070 instr.lvalue.identifier,
1071 (lowered_func.func, HashSet::new()),
1072 );
1073 }
1074 InstructionValue::StoreLocal { value: val, lvalue, .. } => {
1075 if let Some(entry) = temporaries.get(&val.identifier).cloned() {
1076 temporaries.insert(lvalue.place.identifier, entry);
1077 }
1078 }
1079 InstructionValue::LoadLocal { place, .. } => {
1080 if let Some(entry) = temporaries.get(&place.identifier).cloned() {
1081 temporaries.insert(instr.lvalue.identifier, entry);
1082 }
1083 }
1084 _ => {}
1085 }
1086 }
1087 }
1088
1089 for (_block_id, block) in &func.body.blocks {
1091 for &instr_id in &block.instructions {
1092 let instr = &func.instructions[instr_id.0 as usize];
1093 match &instr.value {
1094 InstructionValue::CallExpression { callee, args, .. } => {
1095 let callee_ty = &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
1096 let maybe_hook = env.get_hook_kind_for_type(callee_ty).ok().flatten();
1097 if let Some(entry) = temporaries.get(&callee.identifier) {
1098 hoistable.insert(entry.0);
1100 } else if maybe_hook.is_some() {
1101 for arg in args {
1103 if let PlaceOrSpread::Place(p) = arg {
1104 if let Some(entry) = temporaries.get(&p.identifier) {
1105 hoistable.insert(entry.0);
1106 }
1107 }
1108 }
1109 }
1110 }
1111 InstructionValue::JsxExpression { props, children, .. } => {
1112 for prop in props {
1114 if let react_compiler_hir::JsxAttribute::Attribute { place, .. } = prop {
1115 if let Some(entry) = temporaries.get(&place.identifier) {
1116 hoistable.insert(entry.0);
1117 }
1118 }
1119 }
1120 if let Some(children) = children {
1121 for child in children {
1122 if let Some(entry) = temporaries.get(&child.identifier) {
1123 hoistable.insert(entry.0);
1124 }
1125 }
1126 }
1127 }
1128 InstructionValue::JsxFragment { children, .. } => {
1129 for child in children {
1130 if let Some(entry) = temporaries.get(&child.identifier) {
1131 hoistable.insert(entry.0);
1132 }
1133 }
1134 }
1135 InstructionValue::FunctionExpression { lowered_func, .. } => {
1136 let inner_func = &env.functions[lowered_func.func.0 as usize];
1139 let lambdas_called = get_assumed_invoked_functions_impl(inner_func, env, temporaries);
1140 if let Some(entry) = temporaries.get_mut(&instr.lvalue.identifier) {
1141 for called in lambdas_called {
1142 entry.1.insert(called);
1143 }
1144 }
1145 }
1146 _ => {}
1147 }
1148 }
1149
1150 if let Terminal::Return { value, .. } = &block.terminal {
1152 if let Some(entry) = temporaries.get(&value.identifier) {
1153 hoistable.insert(entry.0);
1154 }
1155 }
1156 }
1157
1158 let mut changed = true;
1160 while changed {
1161 changed = false;
1162 let mut to_add = Vec::new();
1164 for (_, (func_id, may_invoke)) in temporaries.iter() {
1165 if hoistable.contains(func_id) {
1166 for &called in may_invoke {
1167 if !hoistable.contains(&called) {
1168 to_add.push(called);
1169 }
1170 }
1171 }
1172 }
1173 for id in to_add {
1174 changed = true;
1175 hoistable.insert(id);
1176 }
1177 if !changed { break; }
1178 }
1179
1180 hoistable
1181}
1182
1183fn collect_non_nulls_in_blocks(
1184 func: &HirFunction,
1185 env: &Environment,
1186 ctx: &CollectHoistableContext,
1187 registry: &mut PropertyPathRegistry,
1188) -> HashMap<BlockId, BlockInfo> {
1189 let mut known_non_null: BTreeSet<usize> = BTreeSet::new();
1191 if func.fn_type == ReactFunctionType::Component
1192 && !func.params.is_empty()
1193 {
1194 if let ParamPattern::Place(place) = &func.params[0] {
1195 let node_idx = registry.get_or_create_identifier(
1196 place.identifier,
1197 true,
1198 place.loc,
1199 );
1200 known_non_null.insert(node_idx);
1201 }
1202 }
1203
1204 let mut nodes: HashMap<BlockId, BlockInfo> = HashMap::new();
1205
1206 for (block_id, block) in &func.body.blocks {
1207 let mut assumed = known_non_null.clone();
1208
1209 if let Some(optional_chain) = ctx.hoistable_from_optionals.get(block_id) {
1211 let node_idx = registry.get_or_create_property(optional_chain);
1212 assumed.insert(node_idx);
1213 }
1214
1215 for &instr_id in &block.instructions {
1216 let instr = &func.instructions[instr_id.0 as usize];
1217 if let Some(path) = get_maybe_non_null_in_instruction(&instr.value, ctx.temporaries) {
1218 let path_ident = path.identifier;
1219 if is_immutable_at_instr(path_ident, instr.id, env, ctx) {
1220 let node_idx = registry.get_or_create_property(&path);
1221 assumed.insert(node_idx);
1222 }
1223 }
1224
1225 if env.enable_preserve_existing_memoization_guarantees {
1227 if let InstructionValue::StartMemoize { deps: Some(deps), .. } = &instr.value {
1228 for dep in deps {
1229 if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal { value: val, .. } = &dep.root {
1230 if !is_immutable_at_instr(val.identifier, instr.id, env, ctx) {
1231 continue;
1232 }
1233 for i in 0..dep.path.len() {
1234 if dep.path[i].optional {
1235 break;
1236 }
1237 let sub_dep = ReactiveScopeDependency {
1238 identifier: val.identifier,
1239 reactive: val.reactive,
1240 path: dep.path[..i].to_vec(),
1241 loc: dep.loc,
1242 };
1243 let node_idx = registry.get_or_create_property(&sub_dep);
1244 assumed.insert(node_idx);
1245 }
1246 }
1247 }
1248 }
1249 }
1250
1251 if let InstructionValue::FunctionExpression { lowered_func, .. } = &instr.value {
1253 if ctx.assumed_invoked_fns.contains(&lowered_func.func) {
1254 let inner_func = &env.functions[lowered_func.func.0 as usize];
1255 let nested_fn_immutable_context: HashSet<IdentifierId> = if ctx.nested_fn_immutable_context.is_some() {
1257 ctx.nested_fn_immutable_context.unwrap().clone()
1259 } else {
1260 inner_func
1261 .context
1262 .iter()
1263 .filter(|place| is_immutable_at_instr(place.identifier, instr.id, env, ctx))
1264 .map(|place| place.identifier)
1265 .collect()
1266 };
1267 let inner_assumed = get_assumed_invoked_functions(inner_func, env);
1268 let inner_ctx = CollectHoistableContext {
1269 temporaries: ctx.temporaries,
1270 known_immutable_identifiers: &HashSet::new(),
1271 hoistable_from_optionals: ctx.hoistable_from_optionals,
1272 nested_fn_immutable_context: Some(&nested_fn_immutable_context),
1273 assumed_invoked_fns: &inner_assumed,
1274 };
1275 let inner_nodes = collect_non_nulls_in_blocks(inner_func, env, &inner_ctx, registry);
1276 let inner_working = propagate_non_null(inner_func, &inner_nodes, registry);
1278 let inner_entry = inner_func.body.entry;
1280 if let Some(inner_set) = inner_working.get(&inner_entry) {
1281 for &node_idx in inner_set {
1282 assumed.insert(node_idx);
1283 }
1284 }
1285 }
1286 }
1287 }
1288
1289 nodes.insert(
1290 *block_id,
1291 BlockInfo {
1292 assumed_non_null_objects: assumed,
1293 },
1294 );
1295 }
1296
1297 nodes
1298}
1299
1300fn propagate_non_null(
1308 func: &HirFunction,
1309 nodes: &HashMap<BlockId, BlockInfo>,
1310 registry: &mut PropertyPathRegistry,
1311) -> HashMap<BlockId, BTreeSet<usize>> {
1312 let mut block_successors: HashMap<BlockId, BTreeSet<BlockId>> = HashMap::new();
1316 for (block_id, block) in &func.body.blocks {
1317 for pred in &block.preds {
1318 block_successors
1319 .entry(*pred)
1320 .or_default()
1321 .insert(*block_id);
1322 }
1323 }
1324
1325 let mut working: HashMap<BlockId, BTreeSet<usize>> = nodes
1327 .iter()
1328 .map(|(k, v)| (*k, v.assumed_non_null_objects.clone()))
1329 .collect();
1330
1331 let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
1332 let mut reversed_block_ids = block_ids.clone();
1333 reversed_block_ids.reverse();
1334
1335 for _ in 0..100 {
1336 let mut changed = false;
1337
1338 let mut traversal_state: HashMap<BlockId, TraversalState> = HashMap::new();
1340 for &block_id in &block_ids {
1341 let block_changed = recursively_propagate_non_null(
1342 block_id,
1343 PropagationDirection::Forward,
1344 &mut traversal_state,
1345 &mut working,
1346 func,
1347 &block_successors,
1348 registry,
1349 );
1350 changed |= block_changed;
1351 }
1352
1353 traversal_state.clear();
1355 for &block_id in &reversed_block_ids {
1356 let block_changed = recursively_propagate_non_null(
1357 block_id,
1358 PropagationDirection::Backward,
1359 &mut traversal_state,
1360 &mut working,
1361 func,
1362 &block_successors,
1363 registry,
1364 );
1365 changed |= block_changed;
1366 }
1367
1368 if !changed {
1369 break;
1370 }
1371 }
1372
1373 working
1374}
1375
1376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1377enum TraversalState {
1378 Active,
1379 Done,
1380}
1381
1382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1383enum PropagationDirection {
1384 Forward,
1385 Backward,
1386}
1387
1388fn recursively_propagate_non_null(
1389 node_id: BlockId,
1390 direction: PropagationDirection,
1391 traversal_state: &mut HashMap<BlockId, TraversalState>,
1392 working: &mut HashMap<BlockId, BTreeSet<usize>>,
1393 func: &HirFunction,
1394 block_successors: &HashMap<BlockId, BTreeSet<BlockId>>,
1395 registry: &mut PropertyPathRegistry,
1396) -> bool {
1397 if traversal_state.contains_key(&node_id) {
1399 return false;
1400 }
1401 traversal_state.insert(node_id, TraversalState::Active);
1402
1403 let neighbors: Vec<BlockId> = match direction {
1404 PropagationDirection::Backward => {
1405 block_successors
1406 .get(&node_id)
1407 .map(|s| s.iter().copied().collect())
1408 .unwrap_or_default()
1409 }
1410 PropagationDirection::Forward => {
1411 func.body
1412 .blocks
1413 .get(&node_id)
1414 .map(|b| b.preds.iter().copied().collect())
1415 .unwrap_or_default()
1416 }
1417 };
1418
1419 let mut changed = false;
1420 for &neighbor in &neighbors {
1421 if !traversal_state.contains_key(&neighbor) {
1422 let neighbor_changed = recursively_propagate_non_null(
1423 neighbor,
1424 direction,
1425 traversal_state,
1426 working,
1427 func,
1428 block_successors,
1429 registry,
1430 );
1431 changed |= neighbor_changed;
1432 }
1433 }
1434
1435 let done_neighbor_sets: Vec<BTreeSet<usize>> = neighbors
1437 .iter()
1438 .filter(|n| traversal_state.get(n) == Some(&TraversalState::Done))
1439 .filter_map(|n| working.get(n).cloned())
1440 .collect();
1441
1442 let neighbor_intersection = if done_neighbor_sets.is_empty() {
1443 BTreeSet::new()
1444 } else {
1445 let mut iter = done_neighbor_sets.into_iter();
1446 let first = iter.next().unwrap();
1447 iter.fold(first, |acc, s| acc.intersection(&s).copied().collect())
1448 };
1449
1450 let prev_objects = working.get(&node_id).cloned().unwrap_or_default();
1451 let mut merged: BTreeSet<usize> = prev_objects.union(&neighbor_intersection).copied().collect();
1452 reduce_maybe_optional_chains(&mut merged, registry);
1453
1454 working.insert(node_id, merged.clone());
1455 traversal_state.insert(node_id, TraversalState::Done);
1456
1457 changed |= prev_objects != merged;
1459 changed
1460}
1461
1462fn collect_hoistable_and_propagate(
1463 func: &HirFunction,
1464 env: &Environment,
1465 temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
1466 hoistable_from_optionals: &HashMap<BlockId, ReactiveScopeDependency>,
1467) -> (HashMap<BlockId, BTreeSet<usize>>, PropertyPathRegistry) {
1468 let mut registry = PropertyPathRegistry::new();
1469 let assumed_invoked_fns = get_assumed_invoked_functions(func, env);
1470 let known_immutable_identifiers: HashSet<IdentifierId> = if func.fn_type == ReactFunctionType::Component
1471 || func.fn_type == ReactFunctionType::Hook
1472 {
1473 func.params
1474 .iter()
1475 .filter_map(|p| match p {
1476 ParamPattern::Place(place) => Some(place.identifier),
1477 _ => None,
1478 })
1479 .collect()
1480 } else {
1481 HashSet::new()
1482 };
1483
1484 let ctx = CollectHoistableContext {
1485 temporaries,
1486 known_immutable_identifiers: &known_immutable_identifiers,
1487 hoistable_from_optionals,
1488 nested_fn_immutable_context: None,
1489 assumed_invoked_fns: &assumed_invoked_fns,
1490 };
1491
1492 let nodes = collect_non_nulls_in_blocks(func, env, &ctx, &mut registry);
1493 let working = propagate_non_null(func, &nodes, &mut registry);
1494
1495 (working, registry)
1496}
1497
1498#[allow(dead_code)]
1500fn key_by_scope_id(
1501 func: &HirFunction,
1502 block_keyed: &HashMap<BlockId, BlockInfo>,
1503) -> HashMap<ScopeId, BlockInfo> {
1504 let mut keyed: HashMap<ScopeId, BlockInfo> = HashMap::new();
1505 for (_block_id, block) in &func.body.blocks {
1506 if let Terminal::Scope {
1507 scope, block: inner_block, ..
1508 } = &block.terminal
1509 {
1510 if let Some(info) = block_keyed.get(inner_block) {
1511 keyed.insert(*scope, info.clone());
1512 }
1513 }
1514 }
1515 keyed
1516}
1517
1518#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1523enum PropertyAccessType {
1524 OptionalAccess,
1525 UnconditionalAccess,
1526 OptionalDependency,
1527 UnconditionalDependency,
1528}
1529
1530fn is_optional_access(access: PropertyAccessType) -> bool {
1531 matches!(
1532 access,
1533 PropertyAccessType::OptionalAccess | PropertyAccessType::OptionalDependency
1534 )
1535}
1536
1537fn is_dependency_access(access: PropertyAccessType) -> bool {
1538 matches!(
1539 access,
1540 PropertyAccessType::OptionalDependency | PropertyAccessType::UnconditionalDependency
1541 )
1542}
1543
1544fn merge_access(a: PropertyAccessType, b: PropertyAccessType) -> PropertyAccessType {
1545 let is_unconditional = !(is_optional_access(a) && is_optional_access(b));
1546 let is_dep = is_dependency_access(a) || is_dependency_access(b);
1547 match (is_unconditional, is_dep) {
1548 (true, true) => PropertyAccessType::UnconditionalDependency,
1549 (true, false) => PropertyAccessType::UnconditionalAccess,
1550 (false, true) => PropertyAccessType::OptionalDependency,
1551 (false, false) => PropertyAccessType::OptionalAccess,
1552 }
1553}
1554
1555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1556enum HoistableAccessType {
1557 Optional,
1558 NonNull,
1559}
1560
1561struct HoistableNode {
1562 properties: HashMap<PropertyLiteral, Box<HoistableNodeEntry>>,
1563 access_type: HoistableAccessType,
1564}
1565
1566struct HoistableNodeEntry {
1567 node: HoistableNode,
1568}
1569
1570struct DependencyNode {
1571 properties: IndexMap<PropertyLiteral, Box<DependencyNodeEntry>>,
1572 access_type: PropertyAccessType,
1573 loc: Option<react_compiler_hir::SourceLocation>,
1574}
1575
1576struct DependencyNodeEntry {
1577 node: DependencyNode,
1578}
1579
1580struct ReactiveScopeDependencyTreeHIR {
1581 hoistable_roots: HashMap<IdentifierId, (HoistableNode, bool)>, dep_roots: IndexMap<IdentifierId, (DependencyNode, bool)>, }
1584
1585impl ReactiveScopeDependencyTreeHIR {
1586 fn new<'a>(
1587 hoistable_objects: impl Iterator<Item = &'a ReactiveScopeDependency>,
1588 _env: &Environment,
1589 ) -> Self {
1590 let mut hoistable_roots: HashMap<IdentifierId, (HoistableNode, bool)> = HashMap::new();
1591
1592 let mut sorted_deps: Vec<&ReactiveScopeDependency> = hoistable_objects.collect();
1598 sorted_deps.sort_by(|a, b| {
1599 let a_optional = !a.path.is_empty() && a.path[0].optional;
1600 let b_optional = !b.path.is_empty() && b.path[0].optional;
1601 b_optional.cmp(&a_optional)
1602 });
1603
1604 for dep in sorted_deps {
1605 let root = hoistable_roots
1606 .entry(dep.identifier)
1607 .or_insert_with(|| {
1608 let access_type = if !dep.path.is_empty() && dep.path[0].optional {
1609 HoistableAccessType::Optional
1610 } else {
1611 HoistableAccessType::NonNull
1612 };
1613 (
1614 HoistableNode {
1615 properties: HashMap::new(),
1616 access_type,
1617 },
1618 dep.reactive,
1619 )
1620 });
1621
1622 let mut curr = &mut root.0;
1623 for i in 0..dep.path.len() {
1624 let access_type = if i + 1 < dep.path.len() && dep.path[i + 1].optional {
1625 HoistableAccessType::Optional
1626 } else {
1627 HoistableAccessType::NonNull
1628 };
1629 let entry = curr
1630 .properties
1631 .entry(dep.path[i].property.clone())
1632 .or_insert_with(|| {
1633 Box::new(HoistableNodeEntry {
1634 node: HoistableNode {
1635 properties: HashMap::new(),
1636 access_type,
1637 },
1638 })
1639 });
1640 curr = &mut entry.node;
1641 }
1642 }
1643
1644 Self {
1645 hoistable_roots,
1646 dep_roots: IndexMap::new(),
1647 }
1648 }
1649
1650 fn add_dependency(&mut self, dep: ReactiveScopeDependency, _env: &Environment) {
1651 let root = self
1652 .dep_roots
1653 .entry(dep.identifier)
1654 .or_insert_with(|| {
1655 (
1656 DependencyNode {
1657 properties: IndexMap::new(),
1658 access_type: PropertyAccessType::UnconditionalAccess,
1659 loc: dep.loc,
1660 },
1661 dep.reactive,
1662 )
1663 });
1664
1665 let mut dep_cursor = &mut root.0;
1666 let hoistable_cursor_root = self.hoistable_roots.get(&dep.identifier);
1667 let mut hoistable_ptr: Option<&HoistableNode> = hoistable_cursor_root.map(|(n, _)| n);
1668
1669 for entry in &dep.path {
1670 let next_hoistable: Option<&HoistableNode>;
1671 let access_type: PropertyAccessType;
1672
1673 if entry.optional {
1674 next_hoistable = hoistable_ptr.and_then(|h| {
1675 h.properties.get(&entry.property).map(|e| &e.node)
1676 });
1677
1678 if hoistable_ptr.is_some()
1679 && hoistable_ptr.unwrap().access_type == HoistableAccessType::NonNull
1680 {
1681 access_type = PropertyAccessType::UnconditionalAccess;
1682 } else {
1683 access_type = PropertyAccessType::OptionalAccess;
1684 }
1685 } else if hoistable_ptr.is_some()
1686 && hoistable_ptr.unwrap().access_type == HoistableAccessType::NonNull
1687 {
1688 next_hoistable = hoistable_ptr.and_then(|h| {
1689 h.properties.get(&entry.property).map(|e| &e.node)
1690 });
1691 access_type = PropertyAccessType::UnconditionalAccess;
1692 } else {
1693 break;
1695 }
1696
1697 let child = dep_cursor
1699 .properties
1700 .entry(entry.property.clone())
1701 .or_insert_with(|| {
1702 Box::new(DependencyNodeEntry {
1703 node: DependencyNode {
1704 properties: IndexMap::new(),
1705 access_type,
1706 loc: entry.loc,
1707 },
1708 })
1709 });
1710 child.node.access_type = merge_access(child.node.access_type, access_type);
1711
1712 dep_cursor = &mut child.node;
1713 hoistable_ptr = next_hoistable;
1714 }
1715
1716 dep_cursor.access_type =
1718 merge_access(dep_cursor.access_type, PropertyAccessType::OptionalDependency);
1719 }
1720
1721 fn derive_minimal_dependencies(&self, _env: &Environment) -> Vec<ReactiveScopeDependency> {
1722 let mut results = Vec::new();
1723 for (&root_id, (root_node, reactive)) in &self.dep_roots {
1724 collect_minimal_deps_in_subtree(
1725 root_node,
1726 *reactive,
1727 root_id,
1728 &[],
1729 &mut results,
1730 );
1731 }
1732 results
1733 }
1734}
1735
1736fn collect_minimal_deps_in_subtree(
1737 node: &DependencyNode,
1738 reactive: bool,
1739 root_id: IdentifierId,
1740 path: &[DependencyPathEntry],
1741 results: &mut Vec<ReactiveScopeDependency>,
1742) {
1743 if is_dependency_access(node.access_type) {
1744 results.push(ReactiveScopeDependency {
1745 identifier: root_id,
1746 reactive,
1747 path: path.to_vec(),
1748 loc: node.loc,
1749 });
1750 } else {
1751 for (child_name, child_entry) in &node.properties {
1752 let mut new_path = path.to_vec();
1753 new_path.push(DependencyPathEntry {
1754 property: child_name.clone(),
1755 optional: is_optional_access(child_entry.node.access_type),
1756 loc: child_entry.node.loc,
1757 });
1758 collect_minimal_deps_in_subtree(
1759 &child_entry.node,
1760 reactive,
1761 root_id,
1762 &new_path,
1763 results,
1764 );
1765 }
1766 }
1767}
1768
1769#[derive(Clone)]
1775struct Decl {
1776 id: EvaluationOrder,
1777 scope_stack: Vec<ScopeId>, }
1779
1780struct DependencyCollectionContext<'a> {
1782 declarations: HashMap<DeclarationId, Decl>,
1783 reassignments: HashMap<IdentifierId, Decl>,
1784 scope_stack: Vec<ScopeId>,
1785 dep_stack: Vec<Vec<ReactiveScopeDependency>>,
1786 deps: IndexMap<ScopeId, Vec<ReactiveScopeDependency>>,
1787 temporaries: &'a HashMap<IdentifierId, ReactiveScopeDependency>,
1788 #[allow(dead_code)]
1789 temporaries_used_outside_scope: &'a HashSet<DeclarationId>,
1790 processed_instrs_in_optional: &'a HashSet<ProcessedInstr>,
1791 inner_fn_context: Option<EvaluationOrder>,
1792}
1793
1794impl<'a> DependencyCollectionContext<'a> {
1795 fn new(
1796 temporaries_used_outside_scope: &'a HashSet<DeclarationId>,
1797 temporaries: &'a HashMap<IdentifierId, ReactiveScopeDependency>,
1798 processed_instrs_in_optional: &'a HashSet<ProcessedInstr>,
1799 ) -> Self {
1800 Self {
1801 declarations: HashMap::new(),
1802 reassignments: HashMap::new(),
1803 scope_stack: Vec::new(),
1804 dep_stack: Vec::new(),
1805 deps: IndexMap::new(),
1806 temporaries,
1807 temporaries_used_outside_scope,
1808 processed_instrs_in_optional,
1809 inner_fn_context: None,
1810 }
1811 }
1812
1813 fn enter_scope(&mut self, scope_id: ScopeId) {
1814 self.dep_stack.push(Vec::new());
1815 self.scope_stack.push(scope_id);
1816 }
1817
1818 fn exit_scope(&mut self, scope_id: ScopeId, pruned: bool, env: &mut Environment) {
1819 let scoped_deps = self.dep_stack.pop().expect(
1820 "[PropagateScopeDeps]: Unexpected scope mismatch",
1821 );
1822 self.scope_stack.pop();
1823
1824 for dep in &scoped_deps {
1826 if self.check_valid_dependency(dep, env) {
1827 if let Some(top) = self.dep_stack.last_mut() {
1828 top.push(dep.clone());
1829 }
1830 }
1831 }
1832
1833 if !pruned {
1834 self.deps.insert(scope_id, scoped_deps);
1835 }
1836 }
1837
1838 fn current_scope(&self) -> Option<ScopeId> {
1839 self.scope_stack.last().copied()
1840 }
1841
1842 fn declare(&mut self, identifier_id: IdentifierId, decl: Decl, env: &Environment) {
1843 if self.inner_fn_context.is_some() {
1844 return;
1845 }
1846 let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id;
1847 if !self.declarations.contains_key(&decl_id) {
1848 self.declarations.insert(decl_id, decl.clone());
1849 }
1850 self.reassignments.insert(identifier_id, decl);
1851 }
1852
1853 fn has_declared(&self, identifier_id: IdentifierId, env: &Environment) -> bool {
1854 let decl_id = env.identifiers[identifier_id.0 as usize].declaration_id;
1855 self.declarations.contains_key(&decl_id)
1856 }
1857
1858 fn check_valid_dependency(&self, dep: &ReactiveScopeDependency, env: &Environment) -> bool {
1859 let ty = &env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize];
1861 if react_compiler_hir::is_ref_value_type(ty) {
1862 return false;
1863 }
1864 if matches!(ty, Type::ObjectMethod) {
1866 return false;
1867 }
1868
1869 let ident = &env.identifiers[dep.identifier.0 as usize];
1870 let current_declaration = self
1871 .reassignments
1872 .get(&dep.identifier)
1873 .or_else(|| self.declarations.get(&ident.declaration_id));
1874
1875 if let Some(current_scope) = self.current_scope() {
1876 if let Some(decl) = current_declaration {
1877 let scope_range_start = env.scopes[current_scope.0 as usize].range.start;
1878 return decl.id < scope_range_start;
1879 }
1880 }
1881 false
1882 }
1883
1884 fn visit_operand(&mut self, place: &Place, env: &mut Environment) {
1885 let dep = self
1886 .temporaries
1887 .get(&place.identifier)
1888 .cloned()
1889 .unwrap_or_else(|| ReactiveScopeDependency {
1890 identifier: place.identifier,
1891 reactive: place.reactive,
1892 path: vec![],
1893 loc: place.loc,
1894 });
1895 self.visit_dependency(dep, env);
1896 }
1897
1898 fn visit_property(
1899 &mut self,
1900 object: &Place,
1901 property: &PropertyLiteral,
1902 optional: bool,
1903 loc: Option<react_compiler_hir::SourceLocation>,
1904 env: &mut Environment,
1905 ) {
1906 let dep = get_property(object, property, optional, loc, self.temporaries, env);
1907 self.visit_dependency(dep, env);
1908 }
1909
1910 fn visit_dependency(&mut self, dep: ReactiveScopeDependency, env: &mut Environment) {
1911 let ident = &env.identifiers[dep.identifier.0 as usize];
1912 let decl_id = ident.declaration_id;
1913
1914 if let Some(original_decl) = self.declarations.get(&decl_id) {
1916 if !original_decl.scope_stack.is_empty() {
1917 let orig_scope_stack = original_decl.scope_stack.clone();
1918 for &scope_id in &orig_scope_stack {
1919 if !self.scope_stack.contains(&scope_id) {
1920 let scope = &env.scopes[scope_id.0 as usize];
1922 let already_declared = scope.declarations.iter().any(|(_, d)| {
1923 env.identifiers[d.identifier.0 as usize].declaration_id == decl_id
1924 });
1925 if !already_declared {
1926 let orig_scope_id = *orig_scope_stack.last().unwrap();
1927 let new_decl = react_compiler_hir::ReactiveScopeDeclaration {
1928 identifier: dep.identifier,
1929 scope: orig_scope_id,
1930 };
1931 env.scopes[scope_id.0 as usize]
1932 .declarations
1933 .push((dep.identifier, new_decl));
1934 }
1935 }
1936 }
1937 }
1938 }
1939
1940 let dep = if react_compiler_hir::is_use_ref_type(
1942 &env.types[env.identifiers[dep.identifier.0 as usize].type_.0 as usize],
1943 ) && dep
1944 .path
1945 .first()
1946 .map(|p| p.property == PropertyLiteral::String("current".to_string()))
1947 .unwrap_or(false)
1948 {
1949 ReactiveScopeDependency {
1950 identifier: dep.identifier,
1951 reactive: dep.reactive,
1952 path: vec![],
1953 loc: dep.loc,
1954 }
1955 } else {
1956 dep
1957 };
1958
1959 if self.check_valid_dependency(&dep, env) {
1960 if let Some(top) = self.dep_stack.last_mut() {
1961 top.push(dep);
1962 }
1963 }
1964 }
1965
1966 fn visit_reassignment(&mut self, place: &Place, env: &mut Environment) {
1967 if let Some(current_scope) = self.current_scope() {
1968 let scope = &env.scopes[current_scope.0 as usize];
1969 let already = scope.reassignments.iter().any(|id| {
1970 env.identifiers[id.0 as usize].declaration_id
1971 == env.identifiers[place.identifier.0 as usize].declaration_id
1972 });
1973 if !already
1974 && self.check_valid_dependency(
1975 &ReactiveScopeDependency {
1976 identifier: place.identifier,
1977 reactive: place.reactive,
1978 path: vec![],
1979 loc: place.loc,
1980 },
1981 env,
1982 )
1983 {
1984 env.scopes[current_scope.0 as usize]
1985 .reassignments
1986 .push(place.identifier);
1987 }
1988 }
1989 }
1990
1991 fn is_deferred_dependency_instr(&self, instr: &Instruction) -> bool {
1992 self.processed_instrs_in_optional
1993 .contains(&ProcessedInstr::Instruction(instr.lvalue.identifier))
1994 || self.temporaries.contains_key(&instr.lvalue.identifier)
1995 }
1996
1997 fn is_deferred_dependency_terminal(&self, block_id: BlockId) -> bool {
1998 self.processed_instrs_in_optional
1999 .contains(&ProcessedInstr::Terminal(block_id))
2000 }
2001}
2002
2003fn visit_inner_function_blocks(
2007 func_id: FunctionId,
2008 ctx: &mut DependencyCollectionContext,
2009 env: &mut Environment,
2010) {
2011 let inner_instrs: Vec<Instruction> = env.functions[func_id.0 as usize]
2014 .instructions
2015 .clone();
2016 let inner_blocks: Vec<(BlockId, Vec<InstructionId>, Vec<(BlockId, IdentifierId)>, Terminal)> =
2017 env.functions[func_id.0 as usize]
2018 .body
2019 .blocks
2020 .iter()
2021 .map(|(bid, blk)| {
2022 let phi_ops: Vec<(BlockId, IdentifierId)> = blk
2023 .phis
2024 .iter()
2025 .flat_map(|phi| {
2026 phi.operands
2027 .iter()
2028 .map(|(pred, place)| (*pred, place.identifier))
2029 })
2030 .collect();
2031 (*bid, blk.instructions.clone(), phi_ops, blk.terminal.clone())
2032 })
2033 .collect();
2034
2035 for (inner_bid, inner_instr_ids, inner_phis, inner_terminal) in &inner_blocks {
2036 for &(_pred_id, op_id) in inner_phis {
2037 if let Some(maybe_optional) = ctx.temporaries.get(&op_id) {
2038 ctx.visit_dependency(maybe_optional.clone(), env);
2039 }
2040 }
2041
2042 for &iid in inner_instr_ids {
2043 let inner_instr = &inner_instrs[iid.0 as usize];
2044 match &inner_instr.value {
2045 InstructionValue::FunctionExpression { lowered_func, .. }
2046 | InstructionValue::ObjectMethod { lowered_func, .. } => {
2047 let scope_stack_copy = ctx.scope_stack.clone();
2049 ctx.declare(
2050 inner_instr.lvalue.identifier,
2051 Decl {
2052 id: inner_instr.id,
2053 scope_stack: scope_stack_copy,
2054 },
2055 env,
2056 );
2057 visit_inner_function_blocks(lowered_func.func, ctx, env);
2058 }
2059 _ => {
2060 handle_instruction(inner_instr, ctx, env);
2061 }
2062 }
2063 }
2064
2065 if !ctx.is_deferred_dependency_terminal(*inner_bid) {
2066 let terminal_ops = visitors::each_terminal_operand(inner_terminal);
2067 for op in &terminal_ops {
2068 ctx.visit_operand(op, env);
2069 }
2070 }
2071 }
2072}
2073
2074fn handle_instruction(
2075 instr: &Instruction,
2076 ctx: &mut DependencyCollectionContext,
2077 env: &mut Environment,
2078) {
2079 let id = instr.id;
2080 let scope_stack_copy = ctx.scope_stack.clone();
2081 ctx.declare(
2082 instr.lvalue.identifier,
2083 Decl {
2084 id,
2085 scope_stack: scope_stack_copy,
2086 },
2087 env,
2088 );
2089
2090 if ctx.is_deferred_dependency_instr(instr) {
2091 return;
2092 }
2093
2094 match &instr.value {
2095 InstructionValue::PropertyLoad {
2096 object,
2097 property,
2098 loc,
2099 ..
2100 } => {
2101 ctx.visit_property(object, property, false, *loc, env);
2102 }
2103 InstructionValue::StoreLocal {
2104 value: val,
2105 lvalue,
2106 ..
2107 } => {
2108 ctx.visit_operand(val, env);
2109 if lvalue.kind == InstructionKind::Reassign {
2110 ctx.visit_reassignment(&lvalue.place, env);
2111 }
2112 let scope_stack_copy = ctx.scope_stack.clone();
2113 ctx.declare(
2114 lvalue.place.identifier,
2115 Decl {
2116 id,
2117 scope_stack: scope_stack_copy,
2118 },
2119 env,
2120 );
2121 }
2122 InstructionValue::DeclareLocal { lvalue, .. }
2123 | InstructionValue::DeclareContext { lvalue, .. } => {
2124 if convert_hoisted_lvalue_kind(lvalue.kind).is_none() {
2125 let scope_stack_copy = ctx.scope_stack.clone();
2126 ctx.declare(
2127 lvalue.place.identifier,
2128 Decl {
2129 id,
2130 scope_stack: scope_stack_copy,
2131 },
2132 env,
2133 );
2134 }
2135 }
2136 InstructionValue::Destructure {
2137 value: val,
2138 lvalue,
2139 ..
2140 } => {
2141 ctx.visit_operand(val, env);
2142 let pattern_places = visitors::each_pattern_operand(&lvalue.pattern);
2143 for place in &pattern_places {
2144 if lvalue.kind == InstructionKind::Reassign {
2145 ctx.visit_reassignment(place, env);
2146 }
2147 let scope_stack_copy = ctx.scope_stack.clone();
2148 ctx.declare(
2149 place.identifier,
2150 Decl {
2151 id,
2152 scope_stack: scope_stack_copy,
2153 },
2154 env,
2155 );
2156 }
2157 }
2158 InstructionValue::StoreContext {
2159 lvalue,
2160 value: val,
2161 ..
2162 } => {
2163 if !ctx.has_declared(lvalue.place.identifier, env)
2164 || lvalue.kind != InstructionKind::Reassign
2165 {
2166 let scope_stack_copy = ctx.scope_stack.clone();
2167 ctx.declare(
2168 lvalue.place.identifier,
2169 Decl {
2170 id,
2171 scope_stack: scope_stack_copy,
2172 },
2173 env,
2174 );
2175 }
2176 ctx.visit_operand(&lvalue.place, env);
2178 ctx.visit_operand(val, env);
2179 }
2180 _ => {
2181 let operands = visitors::each_instruction_value_operand(&instr.value, env);
2183 for operand in &operands {
2184 ctx.visit_operand(operand, env);
2185 }
2186 }
2187 }
2188}
2189
2190fn collect_dependencies(
2191 func: &HirFunction,
2192 env: &mut Environment,
2193 used_outside_declaring_scope: &HashSet<DeclarationId>,
2194 temporaries: &HashMap<IdentifierId, ReactiveScopeDependency>,
2195 processed_instrs_in_optional: &HashSet<ProcessedInstr>,
2196) -> IndexMap<ScopeId, Vec<ReactiveScopeDependency>> {
2197 let mut ctx = DependencyCollectionContext::new(
2198 used_outside_declaring_scope,
2199 temporaries,
2200 processed_instrs_in_optional,
2201 );
2202
2203 for param in &func.params {
2205 match param {
2206 ParamPattern::Place(place) => {
2207 ctx.declare(
2208 place.identifier,
2209 Decl {
2210 id: EvaluationOrder(0),
2211 scope_stack: vec![],
2212 },
2213 env,
2214 );
2215 }
2216 ParamPattern::Spread(spread) => {
2217 ctx.declare(
2218 spread.place.identifier,
2219 Decl {
2220 id: EvaluationOrder(0),
2221 scope_stack: vec![],
2222 },
2223 env,
2224 );
2225 }
2226 }
2227 }
2228
2229 let mut traversal = ScopeBlockTraversal::new();
2230
2231 handle_function_deps(func, env, &mut ctx, &mut traversal);
2232
2233 ctx.deps
2234}
2235
2236fn handle_function_deps(
2237 func: &HirFunction,
2238 env: &mut Environment,
2239 ctx: &mut DependencyCollectionContext,
2240 traversal: &mut ScopeBlockTraversal,
2241) {
2242 for (block_id, block) in &func.body.blocks {
2243 traversal.record_scopes(block);
2245
2246 let scope_block_info = traversal.block_infos.get(block_id).cloned();
2247 match &scope_block_info {
2248 Some(ScopeBlockInfo::Begin { scope, .. }) => {
2249 ctx.enter_scope(*scope);
2250 }
2251 Some(ScopeBlockInfo::End { scope, pruned, .. }) => {
2252 ctx.exit_scope(*scope, *pruned, env);
2253 }
2254 None => {}
2255 }
2256
2257 for phi in &block.phis {
2259 for (_pred_id, operand) in &phi.operands {
2260 if let Some(maybe_optional_chain) = ctx.temporaries.get(&operand.identifier) {
2261 ctx.visit_dependency(maybe_optional_chain.clone(), env);
2262 }
2263 }
2264 }
2265
2266 for &instr_id in &block.instructions {
2267 let instr = &func.instructions[instr_id.0 as usize];
2268 match &instr.value {
2269 InstructionValue::FunctionExpression { lowered_func, .. }
2270 | InstructionValue::ObjectMethod { lowered_func, .. } => {
2271 let scope_stack_copy = ctx.scope_stack.clone();
2272 ctx.declare(
2273 instr.lvalue.identifier,
2274 Decl {
2275 id: instr.id,
2276 scope_stack: scope_stack_copy,
2277 },
2278 env,
2279 );
2280
2281 let inner_func_id = lowered_func.func;
2283 let prev_inner = ctx.inner_fn_context;
2284 if ctx.inner_fn_context.is_none() {
2285 ctx.inner_fn_context = Some(instr.id);
2286 }
2287
2288 visit_inner_function_blocks(inner_func_id, ctx, env);
2289
2290 ctx.inner_fn_context = prev_inner;
2291 }
2292 _ => {
2293 handle_instruction(instr, ctx, env);
2294 }
2295 }
2296 }
2297
2298 if !ctx.is_deferred_dependency_terminal(*block_id) {
2300 let terminal_ops = visitors::each_terminal_operand(&block.terminal);
2301 for op in &terminal_ops {
2302 ctx.visit_operand(op, env);
2303 }
2304 }
2305 }
2306}
2307