Skip to main content

react_compiler_inference/
propagate_scope_dependencies_hir.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2//
3// This source code is licensed under the MIT license found in the
4// LICENSE file in the root directory of this source tree.
5
6//! 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
15use 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
28// =============================================================================
29// Public entry point
30// =============================================================================
31
32/// Main entry point: propagate scope dependencies through the HIR.
33/// Corresponds to TS `propagateScopeDependenciesHIR(fn)`.
34pub 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        // Convert to scope-keyed map with full dependency paths
47        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    // Merge temporaries + temporariesReadInOptional
63    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    // Derive the minimal set of hoistable dependencies for each scope.
77    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        // Step 2: Calculate hoistable dependencies using the tree.
88        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        // Step 3: Reduce dependencies to a minimal set.
97        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
120// =============================================================================
121// findTemporariesUsedOutsideDeclaringScope
122// =============================================================================
123
124/// Corresponds to TS `findTemporariesUsedOutsideDeclaringScope`.
125fn 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        // recordScopes
150        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            // Handle operands
160            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            // Handle instruction (track declarations)
171            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        // Terminal operands
188        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
203// =============================================================================
204// collectTemporariesSidemap
205// =============================================================================
206
207/// Corresponds to TS `collectTemporariesSidemap`.
208fn 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
224/// Corresponds to TS `isLoadContextMutable`.
225fn 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
239/// Corresponds to TS `convertHoistedLValueKind` — returns None for non-hoisted kinds.
240fn 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
249/// Recursive implementation. Corresponds to TS `collectTemporariesSidemapImpl`.
250fn 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
342/// Corresponds to TS `getProperty`.
343fn 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
379// =============================================================================
380// CollectOptionalChainDependencies
381// =============================================================================
382
383struct 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/// We track processed instructions/terminals by their lvalue IdentifierId + block id.
390/// In TS this uses reference identity (Set<Instruction | Terminal>).
391/// We use IdentifierId for instructions (globally unique across functions) and
392/// BlockId for terminals. Note: EvaluationOrder (instruction id) is NOT unique
393/// across functions, so we cannot use it here.
394#[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            // Verify the store local's value matches the property load's lvalue
496            if value.identifier != instr0.lvalue.identifier {
497                return None;
498            }
499            &lvalue.place
500        }
501        _ => return None,
502    };
503
504    // Verify property load's object matches the test
505    if property_load_object.identifier != test_place.identifier {
506        return None;
507    }
508
509    // Check consequent block terminal is goto break
510    match &consequent_block.terminal {
511        Terminal::Goto {
512            variant: GotoVariant::Break,
513            block: goto_block,
514            ..
515        } => {
516            // Verify alternate block structure
517            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            // Base case: optional must be true
565            if !is_optional {
566                return None;
567            }
568            // Match base expression that is straightforward PropertyLoad chain
569            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            // Verify test expression matches last instruction's lvalue
597            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            // Recurse into inner optional
631            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            // Check that inner optional is part of the same chain
640            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                // Non-optional load: record that PropertyLoads from inner optional are hoistable
650                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    // Verify alternate matches outer_alternate if present
666    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            // Verify optional block has no instructions
673            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    // Verify consequent goto matches optional fallthrough
682    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                // Find the block ID for this terminal
707                // The terminal belongs to either maybe_test_block or the fallthrough block of inner optional
708                // We need to identify which block this terminal belongs to.
709                // For the base case, it's test_block_id.
710                // For nested optional, it's the fallthrough block.
711                // We'll use the block_id approach based on what we know.
712                // Actually, we tracked the terminal by its block, so we need to find which block
713                // contains this terminal. Let's use a pragmatic approach:
714                // The test terminal we matched was from maybe_test_block or from the inner fallthrough block.
715                // We'll search for it.
716
717                // For the base case (Branch terminal at maybe_test_block), block_id = test_block_id
718                // For the nested case, the test terminal is at the fallthrough block of inner optional
719                // In either case, we stored the terminal as test_terminal which comes from a known block.
720                // We need to find the block that owns this terminal.
721
722                // Let's take a simpler approach: find the block whose terminal matches
723                // This is the block we got test_terminal from.
724                // In the first branch of the match, test_terminal = &maybe_test_block.terminal
725                //   and maybe_test_block.id = test_block_id
726                // In the second branch, test_terminal = &test_block.terminal
727                //   and test_block = func.body.blocks.get(inner_fallthrough)
728                // We can't easily tell which case we're in here since we're past the match.
729
730                // Actually, since test_terminal is a reference to a terminal in a block,
731                // we can just look up which block it belongs to by finding blocks whose terminal
732                // pointer matches. But that's expensive. Instead, let's use the block approach
733                // and find the block from the terminal's properties.
734
735                // For simplicity, use a sentinel approach: just check all blocks.
736                // This is O(n) but only happens for optional chains.
737                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// =============================================================================
757// CollectHoistablePropertyLoads
758// =============================================================================
759
760#[derive(Debug, Clone)]
761struct PropertyPathNode {
762    properties: HashMap<PropertyLiteral, usize>,          // index into registry
763    optional_properties: HashMap<PropertyLiteral, usize>, // index into registry
764    #[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
863/// Reduces optional chains in a set of property path nodes.
864///
865/// Any two optional chains with different operations (`.` vs `?.`) but the same set
866/// of property string paths de-duplicates. If unconditional reads from `<base>` are
867/// hoistable (i.e., `<base>` is in the set), we replace `<base>?.PROPERTY` with
868/// `<base>.PROPERTY`.
869///
870/// Port of `reduceMaybeOptionalChains` from CollectHoistablePropertyLoads.ts.
871fn reduce_maybe_optional_chains(
872    nodes: &mut BTreeSet<usize>,
873    registry: &mut PropertyPathRegistry,
874) {
875    // Collect indices of nodes that have optional in their path
876    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        // Collect the indices to process (snapshot to avoid borrow issues)
890        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                // If the base is known to be non-null (in the set), replace optional with non-optional
903                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>, // indices into PropertyPathRegistry
933}
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    // Return the propagated results, converting HashSet<usize> back to BlockInfo
1038    working
1039        .into_iter()
1040        .map(|(k, v)| (k, BlockInfo { assumed_non_null_objects: v }))
1041        .collect()
1042}
1043
1044/// Corresponds to TS `getAssumedInvokedFunctions`.
1045/// Returns the set of LoweredFunction FunctionIds that are assumed to be invoked.
1046/// The `temporaries` map is shared across recursive calls (matching TS behavior where
1047/// the same Map is passed to recursive invocations for inner functions).
1048fn 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    // Step 1: Collect identifier to function expression mappings
1064    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    // Step 2: Forward pass to analyze assumed function calls
1090    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                        // Direct calls
1099                        hoistable.insert(entry.0);
1100                    } else if maybe_hook.is_some() {
1101                        // Assume arguments to all hooks are safe to invoke
1102                        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                    // Assume JSX attributes and children are safe to invoke
1113                    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                    // Recursively traverse into other function expressions
1137                    // TS passes the shared temporaries map to the recursive call
1138                    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        // Assume directly returned functions are safe to call
1151        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    // Step 3: Propagate assumed-invoked status through mayInvoke chains
1159    let mut changed = true;
1160    while changed {
1161        changed = false;
1162        // Two-phase: collect then insert
1163        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    // Known non-null identifiers (e.g. component props)
1190    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        // Check hoistable from optionals
1210        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            // Handle StartMemoize deps for enablePreserveExistingMemoizationGuarantees
1226            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            // Handle assumed-invoked inner functions
1252            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                    // Build nested fn immutable context
1256                    let nested_fn_immutable_context: HashSet<IdentifierId> = if ctx.nested_fn_immutable_context.is_some() {
1257                        // Already in a nested fn context, use existing
1258                        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                    // Propagate non-null from inner function
1277                    let inner_working = propagate_non_null(inner_func, &inner_nodes, registry);
1278                    // Get hoistables from inner function's entry block (after propagation)
1279                    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
1300/// Recursive DFS propagation of non-null information through the CFG.
1301/// Uses 'active'/'done' state tracking to correctly handle cycles (backedges in loops).
1302///
1303/// Port of TS `propagateNonNull` which uses `recursivelyPropagateNonNull`.
1304/// Key insight: when computing the intersection of neighbor sets, only include
1305/// neighbors that are 'done' (not 'active'). Active neighbors are part of a cycle
1306/// and should be filtered out, allowing non-null info to propagate through non-cyclic paths.
1307fn propagate_non_null(
1308    func: &HirFunction,
1309    nodes: &HashMap<BlockId, BlockInfo>,
1310    registry: &mut PropertyPathRegistry,
1311) -> HashMap<BlockId, BTreeSet<usize>> {
1312    // Build successor map. Use BTreeSet to iterate successors in sorted BlockId
1313    // order, matching the TS Set<BlockId> insertion order (blocks are created in
1314    // ascending BlockId order).
1315    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    // Clone nodes into mutable working set
1326    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        // Forward pass (using predecessors)
1339        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        // Backward pass (using successors)
1354        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    // Avoid re-visiting computed or currently active nodes
1398    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    // Compute intersection of 'done' neighbors only (filter out 'active' = cycle nodes)
1436    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    // Compare with previous value — can't just check size due to reduce_maybe_optional_chains
1458    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// Restructured version used by the main entry point
1499#[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// =============================================================================
1519// DeriveMinimalDependenciesHIR
1520// =============================================================================
1521
1522#[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)>, // node + reactive
1582    dep_roots: IndexMap<IdentifierId, (DependencyNode, bool)>,     // node + reactive (preserves insertion order like JS Map)
1583}
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        // Sort hoistable objects so that entries with optional first path come
1593        // before non-optional ones. This matches the TS behavior where
1594        // hoistableFromOptionals entries are inserted into the JS Set before
1595        // instruction-based entries, and the first insertion determines the
1596        // root access type.
1597        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: truncate dependency
1694                break;
1695            }
1696
1697            // make_or_merge_property
1698            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        // Mark final node as dependency
1717        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// =============================================================================
1770// collectDependencies
1771// =============================================================================
1772
1773/// A declaration record: instruction id + scope stack at declaration time.
1774#[derive(Clone)]
1775struct Decl {
1776    id: EvaluationOrder,
1777    scope_stack: Vec<ScopeId>, // copy of the scope stack at time of declaration
1778}
1779
1780/// Context for dependency collection.
1781struct 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        // Propagate dependencies upward
1825        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        // Ref value is not a valid dep
1860        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        // Object methods are not deps
1865        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        // Record scope declarations for values used outside their declaring scope
1915        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                        // Check if already declared in this scope
1921                        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        // Handle ref.current access
1941        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
2003/// Recursively visit an inner function's blocks, processing all instructions
2004/// including nested FunctionExpressions. This mirrors the TS pattern of
2005/// `context.enterInnerFn(instr, () => handleFunction(innerFn))`.
2006fn visit_inner_function_blocks(
2007    func_id: FunctionId,
2008    ctx: &mut DependencyCollectionContext,
2009    env: &mut Environment,
2010) {
2011    // Clone inner function's instructions and block structure to avoid
2012    // borrow conflicts when mutating env through handle_instruction.
2013    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                    // Recursively visit nested function expressions
2048                    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            // Visit all operands (lvalue.place AND value)
2177            ctx.visit_operand(&lvalue.place, env);
2178            ctx.visit_operand(val, env);
2179        }
2180        _ => {
2181            // Visit all value operands
2182            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    // Declare params
2204    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        // Record scopes
2244        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        // Record phi operands
2258        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                    // Recursively visit inner function
2282                    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        // Terminal operands
2299        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