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