Skip to main content

react_compiler_inference/
infer_reactive_places.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//! Infers which `Place`s are reactive.
7//!
8//! Ported from TypeScript `src/Inference/InferReactivePlaces.ts`.
9//!
10//! A place is reactive if it derives from any source of reactivity:
11//! 1. Props (component parameters may change between renders)
12//! 2. Hooks (can access state or context)
13//! 3. `use` operator (can access context)
14//! 4. Mutation with reactive operands
15//! 5. Conditional assignment based on reactive control flow
16
17use std::collections::{HashMap, HashSet};
18
19use react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory};
20use react_compiler_hir::dominator::post_dominator_frontier;
21use react_compiler_hir::environment::Environment;
22use react_compiler_hir::object_shape::HookKind;
23use react_compiler_hir::visitors;
24use react_compiler_hir::{
25    BlockId, Effect, FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern,
26    Terminal, Type,
27};
28
29use react_compiler_utils::DisjointSet;
30
31use crate::infer_reactive_scope_variables::find_disjoint_mutable_values;
32
33// =============================================================================
34// Public API
35// =============================================================================
36
37/// Infer which places in a function are reactive.
38///
39/// Corresponds to TS `inferReactivePlaces(fn: HIRFunction): void`.
40pub fn infer_reactive_places(
41    func: &mut HirFunction,
42    env: &mut Environment,
43) -> Result<(), CompilerDiagnostic> {
44    let mut aliased_identifiers = find_disjoint_mutable_values(func, env);
45    let mut reactive_map = ReactivityMap::new(&mut aliased_identifiers);
46    let mut stable_sidemap = StableSidemap::new();
47
48    // Mark all function parameters as reactive
49    for param in &func.params {
50        let place = match param {
51            ParamPattern::Place(p) => p,
52            ParamPattern::Spread(s) => &s.place,
53        };
54        reactive_map.mark_reactive(place.identifier);
55    }
56
57    // Compute control dominators
58    let post_dominators = react_compiler_hir::dominator::compute_post_dominator_tree(
59        func,
60        env.next_block_id().0,
61        false,
62    )?;
63
64    // Collect block IDs for iteration
65    let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
66
67    // Track phi operand reactive flags during fixpoint.
68    // In TS, isReactive() sets place.reactive as a side effect. But when a phi
69    // is already reactive, the TS `continue`s and skips operand processing.
70    // We track which phi operand Places should be marked reactive.
71    // Key: (block_id, phi_idx, operand_idx), Value: should be reactive
72    let mut phi_operand_reactive: HashMap<(BlockId, usize, usize), bool> = HashMap::new();
73
74    // Fixpoint iteration — compute reactive set
75    loop {
76        for block_id in &block_ids {
77            let block = func.body.blocks.get(block_id).unwrap();
78            let has_reactive_control = is_reactive_controlled_block(
79                block.id,
80                func,
81                &post_dominators,
82                &mut reactive_map,
83            );
84
85            // Process phi nodes
86            let block = func.body.blocks.get(block_id).unwrap();
87            for (phi_idx, phi) in block.phis.iter().enumerate() {
88                if reactive_map.is_reactive(phi.place.identifier) {
89                    // TS does `continue` here — skips operand isReactive calls.
90                    // phi operand reactive flags stay as they were from last visit.
91                    continue;
92                }
93                let mut is_phi_reactive = false;
94                for (op_idx, (_pred, operand)) in phi.operands.iter().enumerate() {
95                    let op_reactive = reactive_map.is_reactive(operand.identifier);
96                    // Record the reactive state for this operand at this point
97                    phi_operand_reactive.insert((*block_id, phi_idx, op_idx), op_reactive);
98                    if op_reactive {
99                        is_phi_reactive = true;
100                        break; // TS breaks here — remaining operands NOT visited
101                    }
102                }
103                if is_phi_reactive {
104                    reactive_map.mark_reactive(phi.place.identifier);
105                } else {
106                    for (pred, _operand) in &phi.operands {
107                        if is_reactive_controlled_block(
108                            *pred,
109                            func,
110                            &post_dominators,
111                            &mut reactive_map,
112                        ) {
113                            reactive_map.mark_reactive(phi.place.identifier);
114                            break;
115                        }
116                    }
117                }
118            }
119
120            // Process instructions
121            let block = func.body.blocks.get(block_id).unwrap();
122            for instr_id in &block.instructions {
123                let instr = &func.instructions[instr_id.0 as usize];
124
125                // Handle stable identifier sources
126                stable_sidemap.handle_instruction(instr, env);
127
128                let value = &instr.value;
129
130                // Check if any operand is reactive
131                let mut has_reactive_input = false;
132                let operands: Vec<IdentifierId> =
133                    visitors::each_instruction_value_operand(value, env)
134                        .into_iter()
135                        .map(|p| p.identifier)
136                        .collect();
137                for &op_id in &operands {
138                    let reactive = reactive_map.is_reactive(op_id);
139                    has_reactive_input = has_reactive_input || reactive;
140                }
141
142                // Hooks and `use` operator are sources of reactivity
143                match value {
144                    InstructionValue::CallExpression { callee, .. } => {
145                        let callee_ty = &env.types
146                            [env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
147                        if get_hook_kind_for_type(env, callee_ty)?.is_some()
148                            || is_use_operator_type(callee_ty)
149                        {
150                            has_reactive_input = true;
151                        }
152                    }
153                    InstructionValue::MethodCall { property, .. } => {
154                        let property_ty = &env.types
155                            [env.identifiers[property.identifier.0 as usize].type_.0 as usize];
156                        if get_hook_kind_for_type(env, property_ty)?.is_some()
157                            || is_use_operator_type(property_ty)
158                        {
159                            has_reactive_input = true;
160                        }
161                    }
162                    _ => {}
163                }
164
165                if has_reactive_input {
166                    // Mark lvalues reactive (unless stable)
167                    let lvalue_ids: Vec<IdentifierId> = visitors::each_instruction_lvalue(instr)
168                        .into_iter()
169                        .map(|p| p.identifier)
170                        .collect();
171                    for lvalue_id in lvalue_ids {
172                        if stable_sidemap.is_stable(lvalue_id) {
173                            continue;
174                        }
175                        reactive_map.mark_reactive(lvalue_id);
176                    }
177                }
178
179                if has_reactive_input || has_reactive_control {
180                    // Mark mutable operands reactive
181                    let operand_places = visitors::each_instruction_value_operand(value, env);
182                    for op_place in &operand_places {
183                        match op_place.effect {
184                            Effect::Capture
185                            | Effect::Store
186                            | Effect::ConditionallyMutate
187                            | Effect::ConditionallyMutateIterator
188                            | Effect::Mutate => {
189                                let op_range = &env.identifiers
190                                    [op_place.identifier.0 as usize]
191                                    .mutable_range;
192                                if op_range.contains(instr.id) {
193                                    reactive_map.mark_reactive(op_place.identifier);
194                                }
195                            }
196                            Effect::Freeze | Effect::Read => {
197                                // no-op
198                            }
199                            Effect::Unknown => {
200                                return Err(CompilerDiagnostic::new(
201                                    ErrorCategory::Invariant,
202                                    &format!(
203                                        "Unexpected unknown effect at {:?}",
204                                        op_place.loc
205                                    ),
206                                    None,
207                                ));
208                            }
209                        }
210                    }
211                }
212            }
213
214            // Process terminal operands (just to mark them reactive for output)
215            for op in visitors::each_terminal_operand(&block.terminal) {
216                reactive_map.is_reactive(op.identifier);
217            }
218        }
219
220        if !reactive_map.snapshot() {
221            break;
222        }
223    }
224
225    // Propagate reactivity to inner functions (read-only phase, just queries reactive_map)
226    propagate_reactivity_to_inner_functions_outer(func, env, &mut reactive_map);
227
228    // Now apply reactive flags by replaying the traversal pattern.
229    apply_reactive_flags_replay(
230        func,
231        env,
232        &mut reactive_map,
233        &mut stable_sidemap,
234        &phi_operand_reactive,
235    );
236
237    Ok(())
238}
239
240// =============================================================================
241// ReactivityMap
242// =============================================================================
243
244struct ReactivityMap<'a> {
245    has_changes: bool,
246    reactive: HashSet<IdentifierId>,
247    aliased_identifiers: &'a mut DisjointSet<IdentifierId>,
248}
249
250impl<'a> ReactivityMap<'a> {
251    fn new(aliased_identifiers: &'a mut DisjointSet<IdentifierId>) -> Self {
252        ReactivityMap {
253            has_changes: false,
254            reactive: HashSet::new(),
255            aliased_identifiers,
256        }
257    }
258
259    fn is_reactive(&mut self, id: IdentifierId) -> bool {
260        let canonical = self.aliased_identifiers.find_opt(id).unwrap_or(id);
261        self.reactive.contains(&canonical)
262    }
263
264    fn mark_reactive(&mut self, id: IdentifierId) {
265        let canonical = self.aliased_identifiers.find_opt(id).unwrap_or(id);
266        if self.reactive.insert(canonical) {
267            self.has_changes = true;
268        }
269    }
270
271    /// Reset change tracking, returns true if there were changes.
272    fn snapshot(&mut self) -> bool {
273        let had_changes = self.has_changes;
274        self.has_changes = false;
275        had_changes
276    }
277}
278
279// =============================================================================
280// StableSidemap
281// =============================================================================
282
283struct StableSidemap {
284    map: HashMap<IdentifierId, bool>,
285}
286
287impl StableSidemap {
288    fn new() -> Self {
289        StableSidemap {
290            map: HashMap::new(),
291        }
292    }
293
294    fn handle_instruction(
295        &mut self,
296        instr: &react_compiler_hir::Instruction,
297        env: &Environment,
298    ) {
299        let lvalue_id = instr.lvalue.identifier;
300        let value = &instr.value;
301
302        match value {
303            InstructionValue::CallExpression { callee, .. } => {
304                let callee_ty =
305                    &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
306                if evaluates_to_stable_type_or_container(env, callee_ty) {
307                    let lvalue_ty =
308                        &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize];
309                    if is_stable_type(lvalue_ty) {
310                        self.map.insert(lvalue_id, true);
311                    } else {
312                        self.map.insert(lvalue_id, false);
313                    }
314                }
315            }
316            InstructionValue::MethodCall { property, .. } => {
317                let property_ty = &env.types
318                    [env.identifiers[property.identifier.0 as usize].type_.0 as usize];
319                if evaluates_to_stable_type_or_container(env, property_ty) {
320                    let lvalue_ty =
321                        &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize];
322                    if is_stable_type(lvalue_ty) {
323                        self.map.insert(lvalue_id, true);
324                    } else {
325                        self.map.insert(lvalue_id, false);
326                    }
327                }
328            }
329            InstructionValue::PropertyLoad { object, .. } => {
330                let source_id = object.identifier;
331                if self.map.contains_key(&source_id) {
332                    let lvalue_ty =
333                        &env.types[env.identifiers[lvalue_id.0 as usize].type_.0 as usize];
334                    if is_stable_type_container(lvalue_ty) {
335                        self.map.insert(lvalue_id, false);
336                    } else if is_stable_type(lvalue_ty) {
337                        self.map.insert(lvalue_id, true);
338                    }
339                }
340            }
341            InstructionValue::Destructure { value: val, .. } => {
342                let source_id = val.identifier;
343                if self.map.contains_key(&source_id) {
344                    let lvalue_ids: Vec<IdentifierId> = visitors::each_instruction_lvalue(instr)
345                        .into_iter()
346                        .map(|p| p.identifier)
347                        .collect();
348                    for lid in lvalue_ids {
349                        let lid_ty =
350                            &env.types[env.identifiers[lid.0 as usize].type_.0 as usize];
351                        if is_stable_type_container(lid_ty) {
352                            self.map.insert(lid, false);
353                        } else if is_stable_type(lid_ty) {
354                            self.map.insert(lid, true);
355                        }
356                    }
357                }
358            }
359            InstructionValue::StoreLocal {
360                lvalue, value: val, ..
361            } => {
362                if let Some(&entry) = self.map.get(&val.identifier) {
363                    self.map.insert(lvalue_id, entry);
364                    self.map.insert(lvalue.place.identifier, entry);
365                }
366            }
367            InstructionValue::LoadLocal { place, .. } => {
368                if let Some(&entry) = self.map.get(&place.identifier) {
369                    self.map.insert(lvalue_id, entry);
370                }
371            }
372            _ => {}
373        }
374    }
375
376    fn is_stable(&self, id: IdentifierId) -> bool {
377        self.map.get(&id).copied().unwrap_or(false)
378    }
379}
380
381// =============================================================================
382// Control dominators (ported from ControlDominators.ts)
383// =============================================================================
384
385fn is_reactive_controlled_block(
386    block_id: BlockId,
387    func: &HirFunction,
388    post_dominators: &react_compiler_hir::dominator::PostDominator,
389    reactive_map: &mut ReactivityMap,
390) -> bool {
391    let frontier = post_dominator_frontier(func, post_dominators, block_id);
392    for frontier_block_id in &frontier {
393        let control_block = func.body.blocks.get(frontier_block_id).unwrap();
394        match &control_block.terminal {
395            Terminal::If { test, .. } | Terminal::Branch { test, .. } => {
396                if reactive_map.is_reactive(test.identifier) {
397                    return true;
398                }
399            }
400            Terminal::Switch { test, cases, .. } => {
401                if reactive_map.is_reactive(test.identifier) {
402                    return true;
403                }
404                for case in cases {
405                    if let Some(ref case_test) = case.test {
406                        if reactive_map.is_reactive(case_test.identifier) {
407                            return true;
408                        }
409                    }
410                }
411            }
412            _ => {}
413        }
414    }
415    false
416}
417
418// =============================================================================
419// Type helpers (ported from HIR.ts)
420// =============================================================================
421
422use react_compiler_hir::is_use_operator_type;
423
424fn get_hook_kind_for_type<'a>(
425    env: &'a Environment,
426    ty: &Type,
427) -> Result<Option<&'a HookKind>, CompilerDiagnostic> {
428    env.get_hook_kind_for_type(ty)
429}
430
431fn is_stable_type(ty: &Type) -> bool {
432    match ty {
433        Type::Function {
434            shape_id: Some(id), ..
435        } => {
436            matches!(
437                id.as_str(),
438                "BuiltInSetState"
439                    | "BuiltInSetActionState"
440                    | "BuiltInDispatch"
441                    | "BuiltInStartTransition"
442                    | "BuiltInSetOptimistic"
443            )
444        }
445        Type::Object {
446            shape_id: Some(id),
447        } => {
448            matches!(id.as_str(), "BuiltInUseRefId")
449        }
450        _ => false,
451    }
452}
453
454fn is_stable_type_container(ty: &Type) -> bool {
455    match ty {
456        Type::Object {
457            shape_id: Some(id),
458        } => {
459            matches!(
460                id.as_str(),
461                "BuiltInUseState"
462                    | "BuiltInUseActionState"
463                    | "BuiltInUseReducer"
464                    | "BuiltInUseOptimistic"
465                    | "BuiltInUseTransition"
466            )
467        }
468        _ => false,
469    }
470}
471
472fn evaluates_to_stable_type_or_container(env: &Environment, callee_ty: &Type) -> bool {
473    if let Some(hook_kind) = get_hook_kind_for_type(env, callee_ty).ok().flatten() {
474        matches!(
475            hook_kind,
476            HookKind::UseState
477                | HookKind::UseReducer
478                | HookKind::UseActionState
479                | HookKind::UseRef
480                | HookKind::UseTransition
481                | HookKind::UseOptimistic
482        )
483    } else {
484        false
485    }
486}
487
488// =============================================================================
489// Propagate reactivity to inner functions
490// =============================================================================
491
492fn propagate_reactivity_to_inner_functions_outer(
493    func: &HirFunction,
494    env: &Environment,
495    reactive_map: &mut ReactivityMap,
496) {
497    for (_block_id, block) in &func.body.blocks {
498        for instr_id in &block.instructions {
499            let instr = &func.instructions[instr_id.0 as usize];
500            match &instr.value {
501                InstructionValue::FunctionExpression { lowered_func, .. }
502                | InstructionValue::ObjectMethod { lowered_func, .. } => {
503                    propagate_reactivity_to_inner_functions_inner(
504                        lowered_func.func,
505                        env,
506                        reactive_map,
507                    );
508                }
509                _ => {}
510            }
511        }
512    }
513}
514
515fn propagate_reactivity_to_inner_functions_inner(
516    func_id: FunctionId,
517    env: &Environment,
518    reactive_map: &mut ReactivityMap,
519) {
520    let inner_func = &env.functions[func_id.0 as usize];
521
522    for (_block_id, block) in &inner_func.body.blocks {
523        for instr_id in &block.instructions {
524            let instr = &inner_func.instructions[instr_id.0 as usize];
525
526            for op in visitors::each_instruction_value_operand(&instr.value, env) {
527                reactive_map.is_reactive(op.identifier);
528            }
529
530            match &instr.value {
531                InstructionValue::FunctionExpression { lowered_func, .. }
532                | InstructionValue::ObjectMethod { lowered_func, .. } => {
533                    propagate_reactivity_to_inner_functions_inner(
534                        lowered_func.func,
535                        env,
536                        reactive_map,
537                    );
538                }
539                _ => {}
540            }
541        }
542
543        for op in visitors::each_terminal_operand(&block.terminal) {
544            reactive_map.is_reactive(op.identifier);
545        }
546    }
547}
548
549// =============================================================================
550// Apply reactive flags to the HIR (replay pass)
551// =============================================================================
552
553fn apply_reactive_flags_replay(
554    func: &mut HirFunction,
555    env: &mut Environment,
556    reactive_map: &mut ReactivityMap,
557    stable_sidemap: &mut StableSidemap,
558    phi_operand_reactive: &HashMap<(BlockId, usize, usize), bool>,
559) {
560    let reactive_ids = build_reactive_id_set(reactive_map);
561
562    // 1. Mark params
563    for param in &mut func.params {
564        let place = match param {
565            ParamPattern::Place(p) => p,
566            ParamPattern::Spread(s) => &mut s.place,
567        };
568        place.reactive = true;
569    }
570
571    // 2. Walk blocks
572    let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
573
574    for block_id in &block_ids {
575        let block = func.body.blocks.get(block_id).unwrap();
576
577        // 2a. Phi nodes
578        let phi_count = block.phis.len();
579        for phi_idx in 0..phi_count {
580            let block = func.body.blocks.get_mut(block_id).unwrap();
581            let phi = &mut block.phis[phi_idx];
582
583            if reactive_ids.contains(&phi.place.identifier) {
584                phi.place.reactive = true;
585            }
586
587            for (op_idx, (_pred, operand)) in phi.operands.iter_mut().enumerate() {
588                if let Some(&is_reactive) =
589                    phi_operand_reactive.get(&(*block_id, phi_idx, op_idx))
590                {
591                    if is_reactive {
592                        operand.reactive = true;
593                    }
594                }
595            }
596        }
597
598        // 2b. Instructions
599        let block = func.body.blocks.get(block_id).unwrap();
600        let instr_ids: Vec<react_compiler_hir::InstructionId> = block.instructions.clone();
601
602        for instr_id in &instr_ids {
603            let instr = &func.instructions[instr_id.0 as usize];
604
605            // Compute hasReactiveInput by checking value operands
606            let value_operand_ids: Vec<IdentifierId> =
607                visitors::each_instruction_value_operand(&instr.value, env)
608                    .into_iter()
609                    .map(|p| p.identifier)
610                    .collect();
611            let mut has_reactive_input = false;
612            for &op_id in &value_operand_ids {
613                if reactive_ids.contains(&op_id) {
614                    has_reactive_input = true;
615                }
616            }
617
618            // Check hooks/use
619            match &instr.value {
620                InstructionValue::CallExpression { callee, .. } => {
621                    let callee_ty = &env.types
622                        [env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
623                    if get_hook_kind_for_type(env, callee_ty).ok().flatten().is_some()
624                        || is_use_operator_type(callee_ty)
625                    {
626                        has_reactive_input = true;
627                    }
628                }
629                InstructionValue::MethodCall { property, .. } => {
630                    let property_ty = &env.types
631                        [env.identifiers[property.identifier.0 as usize].type_.0 as usize];
632                    if get_hook_kind_for_type(env, property_ty)
633                        .ok()
634                        .flatten()
635                        .is_some()
636                        || is_use_operator_type(property_ty)
637                    {
638                        has_reactive_input = true;
639                    }
640                }
641                _ => {}
642            }
643
644            // Value operands: set reactive flag using canonical visitor
645            let instr = &mut func.instructions[instr_id.0 as usize];
646            visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| {
647                if reactive_ids.contains(&place.identifier) {
648                    place.reactive = true;
649                }
650            });
651            // FunctionExpression/ObjectMethod context variables require env access
652            if let InstructionValue::FunctionExpression { lowered_func, .. }
653            | InstructionValue::ObjectMethod { lowered_func, .. } = &mut instr.value
654            {
655                let inner_func = &mut env.functions[lowered_func.func.0 as usize];
656                for ctx in &mut inner_func.context {
657                    if reactive_ids.contains(&ctx.identifier) {
658                        ctx.reactive = true;
659                    }
660                }
661            }
662
663            // Lvalues: markReactive is called only when hasReactiveInput
664            if has_reactive_input {
665                let lvalue_id = instr.lvalue.identifier;
666                if !stable_sidemap.is_stable(lvalue_id) && reactive_ids.contains(&lvalue_id) {
667                    instr.lvalue.reactive = true;
668                }
669                // Handle value lvalues — includes DeclareContext/StoreContext which
670                // for_each_instruction_lvalue_mut skips, so we use a direct match.
671                match &mut instr.value {
672                    InstructionValue::DeclareLocal { lvalue, .. }
673                    | InstructionValue::DeclareContext { lvalue, .. }
674                    | InstructionValue::StoreLocal { lvalue, .. }
675                    | InstructionValue::StoreContext { lvalue, .. } => {
676                        let id = lvalue.place.identifier;
677                        if !stable_sidemap.is_stable(id) && reactive_ids.contains(&id) {
678                            lvalue.place.reactive = true;
679                        }
680                    }
681                    InstructionValue::Destructure { lvalue, .. } => {
682                        visitors::for_each_pattern_operand_mut(
683                            &mut lvalue.pattern,
684                            &mut |place| {
685                                if !stable_sidemap.is_stable(place.identifier)
686                                    && reactive_ids.contains(&place.identifier)
687                                {
688                                    place.reactive = true;
689                                }
690                            },
691                        );
692                    }
693                    InstructionValue::PrefixUpdate { lvalue, .. }
694                    | InstructionValue::PostfixUpdate { lvalue, .. } => {
695                        let id = lvalue.identifier;
696                        if !stable_sidemap.is_stable(id) && reactive_ids.contains(&id) {
697                            lvalue.reactive = true;
698                        }
699                    }
700                    _ => {}
701                }
702            }
703        }
704
705        // 2c. Terminal operands
706        let block = func.body.blocks.get_mut(block_id).unwrap();
707        visitors::for_each_terminal_operand_mut(&mut block.terminal, &mut |place| {
708            if reactive_ids.contains(&place.identifier) {
709                place.reactive = true;
710            }
711        });
712    }
713
714    // 3. Apply to inner functions
715    apply_reactive_flags_to_inner_functions(func, env, &reactive_ids);
716}
717
718fn build_reactive_id_set(reactive_map: &mut ReactivityMap) -> HashSet<IdentifierId> {
719    let mut result = HashSet::new();
720    for &id in &reactive_map.reactive {
721        result.insert(id);
722    }
723    let reactive = &reactive_map.reactive;
724    reactive_map.aliased_identifiers.for_each(|id, canonical| {
725        if reactive.contains(&canonical) {
726            result.insert(id);
727        }
728    });
729    result
730}
731
732fn apply_reactive_flags_to_inner_functions(
733    func: &HirFunction,
734    env: &mut Environment,
735    reactive_ids: &HashSet<IdentifierId>,
736) {
737    for (_block_id, block) in &func.body.blocks {
738        for instr_id in &block.instructions {
739            let instr = &func.instructions[instr_id.0 as usize];
740            match &instr.value {
741                InstructionValue::FunctionExpression { lowered_func, .. }
742                | InstructionValue::ObjectMethod { lowered_func, .. } => {
743                    apply_reactive_flags_to_inner_func(lowered_func.func, env, reactive_ids);
744                }
745                _ => {}
746            }
747        }
748    }
749}
750
751fn apply_reactive_flags_to_inner_func(
752    func_id: FunctionId,
753    env: &mut Environment,
754    reactive_ids: &HashSet<IdentifierId>,
755) {
756    // Collect nested function IDs first to avoid borrow issues
757    let nested_func_ids: Vec<FunctionId> = {
758        let func = &env.functions[func_id.0 as usize];
759        let mut ids = Vec::new();
760        for (_block_id, block) in &func.body.blocks {
761            for instr_id in &block.instructions {
762                let instr = &func.instructions[instr_id.0 as usize];
763                match &instr.value {
764                    InstructionValue::FunctionExpression { lowered_func, .. }
765                    | InstructionValue::ObjectMethod { lowered_func, .. } => {
766                        ids.push(lowered_func.func);
767                    }
768                    _ => {}
769                }
770            }
771        }
772        ids
773    };
774
775    // Apply reactive flags using canonical visitors
776    let inner_func = &mut env.functions[func_id.0 as usize];
777    for (_block_id, block) in &mut inner_func.body.blocks {
778        for instr_id in &block.instructions {
779            let instr = &mut inner_func.instructions[instr_id.0 as usize];
780            visitors::for_each_instruction_value_operand_mut(&mut instr.value, &mut |place| {
781                if reactive_ids.contains(&place.identifier) {
782                    place.reactive = true;
783                }
784            });
785        }
786        visitors::for_each_terminal_operand_mut(&mut block.terminal, &mut |place| {
787            if reactive_ids.contains(&place.identifier) {
788                place.reactive = true;
789            }
790        });
791    }
792
793    // Recurse into nested functions, and set reactive on their context variables
794    for nested_id in nested_func_ids {
795        let nested_func = &mut env.functions[nested_id.0 as usize];
796        for ctx in &mut nested_func.context {
797            if reactive_ids.contains(&ctx.identifier) {
798                ctx.reactive = true;
799            }
800        }
801        apply_reactive_flags_to_inner_func(nested_id, env, reactive_ids);
802    }
803}