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