main
rs 1,183 lines 43.6 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 mutable ranges for identifiers and populates Place effects.
7 //!
8 //! Ported from TypeScript `src/Inference/InferMutationAliasingRanges.ts`.
9 //!
10 //! This pass builds an abstract model of the heap and interprets the effects of
11 //! the given function in order to determine:
12 //! - The mutable ranges of all identifiers in the function
13 //! - The externally-visible effects of the function (mutations of params/context
14 //! vars, aliasing between params/context-vars/return-value)
15 //! - The legacy `Effect` to store on each Place
16
17 use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
18
19 use indexmap::IndexMap;
20
21 use react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory};
22 use react_compiler_hir::environment::Environment;
23 use react_compiler_hir::type_config::{ValueKind, ValueReason};
24 use react_compiler_hir::visitors::{
25 each_instruction_value_lvalue, for_each_instruction_value_lvalue_mut,
26 for_each_instruction_value_operand_mut, for_each_terminal_operand_mut,
27 };
28 use react_compiler_hir::{
29 AliasingEffect, BlockId, Effect, EvaluationOrder, FunctionId, HirFunction, IdentifierId,
30 InstructionValue, MutationReason, Place, SourceLocation, is_jsx_type, is_primitive_type,
31 };
32
33 // =============================================================================
34 // MutationKind
35 // =============================================================================
36
37 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38 #[allow(dead_code)]
39 enum MutationKind {
40 None = 0,
41 Conditional = 1,
42 Definite = 2,
43 }
44
45 // =============================================================================
46 // Node and AliasingState
47 // =============================================================================
48
49 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
50 enum EdgeKind {
51 Capture,
52 Alias,
53 MaybeAlias,
54 }
55
56 #[derive(Debug, Clone)]
57 struct Edge {
58 index: usize,
59 node: IdentifierId,
60 kind: EdgeKind,
61 }
62
63 #[derive(Debug, Clone)]
64 struct MutationInfo {
65 kind: MutationKind,
66 loc: Option<SourceLocation>,
67 }
68
69 #[derive(Debug, Clone)]
70 enum NodeValue {
71 Object,
72 Phi,
73 Function { function_id: FunctionId },
74 }
75
76 #[derive(Debug, Clone)]
77 struct Node {
78 id: IdentifierId,
79 created_from: IndexMap<IdentifierId, usize, FxBuildHasher>,
80 captures: IndexMap<IdentifierId, usize, FxBuildHasher>,
81 aliases: IndexMap<IdentifierId, usize, FxBuildHasher>,
82 maybe_aliases: IndexMap<IdentifierId, usize, FxBuildHasher>,
83 edges: Vec<Edge>,
84 transitive: Option<MutationInfo>,
85 local: Option<MutationInfo>,
86 last_mutated: usize,
87 mutation_reason: Option<MutationReason>,
88 value: NodeValue,
89 }
90
91 impl Node {
92 fn new(id: IdentifierId, value: NodeValue) -> Self {
93 Node {
94 id,
95 created_from: IndexMap::default(),
96 captures: IndexMap::default(),
97 aliases: IndexMap::default(),
98 maybe_aliases: IndexMap::default(),
99 edges: Vec::new(),
100 transitive: None,
101 local: None,
102 last_mutated: 0,
103 mutation_reason: None,
104 value,
105 }
106 }
107 }
108
109 struct AliasingState {
110 nodes: IndexMap<IdentifierId, Node, FxBuildHasher>,
111 }
112
113 impl AliasingState {
114 fn new() -> Self {
115 AliasingState {
116 nodes: IndexMap::default(),
117 }
118 }
119
120 fn create(&mut self, place: &Place, value: NodeValue) {
121 self.nodes
122 .insert(place.identifier, Node::new(place.identifier, value));
123 }
124
125 fn create_from(&mut self, index: usize, from: &Place, into: &Place) {
126 self.create(into, NodeValue::Object);
127 let from_id = from.identifier;
128 let into_id = into.identifier;
129 // Add forward edge from -> into on the from node
130 if let Some(from_node) = self.nodes.get_mut(&from_id) {
131 from_node.edges.push(Edge {
132 index,
133 node: into_id,
134 kind: EdgeKind::Alias,
135 });
136 }
137 // Add created_from on the into node
138 if let Some(to_node) = self.nodes.get_mut(&into_id) {
139 to_node.created_from.entry(from_id).or_insert(index);
140 }
141 }
142
143 fn capture(&mut self, index: usize, from: &Place, into: &Place) {
144 let from_id = from.identifier;
145 let into_id = into.identifier;
146 if !self.nodes.contains_key(&from_id) || !self.nodes.contains_key(&into_id) {
147 return;
148 }
149 self.nodes.get_mut(&from_id).unwrap().edges.push(Edge {
150 index,
151 node: into_id,
152 kind: EdgeKind::Capture,
153 });
154 self.nodes
155 .get_mut(&into_id)
156 .unwrap()
157 .captures
158 .entry(from_id)
159 .or_insert(index);
160 }
161
162 fn assign(&mut self, index: usize, from: &Place, into: &Place) {
163 let from_id = from.identifier;
164 let into_id = into.identifier;
165 if !self.nodes.contains_key(&from_id) || !self.nodes.contains_key(&into_id) {
166 return;
167 }
168 self.nodes.get_mut(&from_id).unwrap().edges.push(Edge {
169 index,
170 node: into_id,
171 kind: EdgeKind::Alias,
172 });
173 self.nodes
174 .get_mut(&into_id)
175 .unwrap()
176 .aliases
177 .entry(from_id)
178 .or_insert(index);
179 }
180
181 fn maybe_alias(&mut self, index: usize, from: &Place, into: &Place) {
182 let from_id = from.identifier;
183 let into_id = into.identifier;
184 if !self.nodes.contains_key(&from_id) || !self.nodes.contains_key(&into_id) {
185 return;
186 }
187 self.nodes.get_mut(&from_id).unwrap().edges.push(Edge {
188 index,
189 node: into_id,
190 kind: EdgeKind::MaybeAlias,
191 });
192 self.nodes
193 .get_mut(&into_id)
194 .unwrap()
195 .maybe_aliases
196 .entry(from_id)
197 .or_insert(index);
198 }
199
200 fn render(&self, index: usize, start: IdentifierId, env: &mut Environment) {
201 let mut seen = FxHashSet::default();
202 let mut queue: Vec<IdentifierId> = vec![start];
203 while let Some(current) = queue.pop() {
204 if !seen.insert(current) {
205 continue;
206 }
207 let node = match self.nodes.get(&current) {
208 Some(n) => n,
209 None => continue,
210 };
211 if node.transitive.is_some() || node.local.is_some() {
212 continue;
213 }
214 if let NodeValue::Function { function_id } = &node.value {
215 append_function_errors(env, *function_id);
216 }
217 for (&alias, &when) in &node.created_from {
218 if when >= index {
219 continue;
220 }
221 queue.push(alias);
222 }
223 for (&alias, &when) in &node.aliases {
224 if when >= index {
225 continue;
226 }
227 queue.push(alias);
228 }
229 for (&capture, &when) in &node.captures {
230 if when >= index {
231 continue;
232 }
233 queue.push(capture);
234 }
235 }
236 }
237
238 fn mutate(
239 &mut self,
240 index: usize,
241 start: IdentifierId,
242 end: Option<EvaluationOrder>, // None for simulated mutations
243 transitive: bool,
244 start_kind: MutationKind,
245 loc: Option<SourceLocation>,
246 reason: Option<MutationReason>,
247 env: &mut Environment,
248 should_record_errors: bool,
249 ) {
250 #[derive(Clone)]
251 struct QueueEntry {
252 place: IdentifierId,
253 transitive: bool,
254 direction: Direction,
255 kind: MutationKind,
256 }
257 #[derive(Clone, Copy, PartialEq)]
258 enum Direction {
259 Backwards,
260 Forwards,
261 }
262
263 let mut seen: FxHashMap<IdentifierId, MutationKind> = FxHashMap::default();
264 let mut queue: Vec<QueueEntry> = vec![QueueEntry {
265 place: start,
266 transitive,
267 direction: Direction::Backwards,
268 kind: start_kind,
269 }];
270
271 while let Some(entry) = queue.pop() {
272 let current = entry.place;
273 let previous_kind = seen.get(&current).copied();
274 if let Some(prev) = previous_kind {
275 if prev >= entry.kind {
276 continue;
277 }
278 }
279 seen.insert(current, entry.kind);
280
281 let node = match self.nodes.get_mut(&current) {
282 Some(n) => n,
283 None => continue,
284 };
285
286 if node.mutation_reason.is_none() {
287 node.mutation_reason = reason.clone();
288 }
289 node.last_mutated = node.last_mutated.max(index);
290
291 if let Some(end_val) = end {
292 let ident = &mut env.identifiers[node.id.0 as usize];
293 ident.mutable_range.end = EvaluationOrder(ident.mutable_range.end.0.max(end_val.0));
294 }
295
296 if let NodeValue::Function { function_id } = &node.value {
297 if node.transitive.is_none() && node.local.is_none() {
298 if should_record_errors {
299 append_function_errors(env, *function_id);
300 }
301 }
302 }
303
304 if entry.transitive {
305 match &node.transitive {
306 None => {
307 node.transitive = Some(MutationInfo {
308 kind: entry.kind,
309 loc,
310 });
311 }
312 Some(existing) if existing.kind < entry.kind => {
313 node.transitive = Some(MutationInfo {
314 kind: entry.kind,
315 loc,
316 });
317 }
318 _ => {}
319 }
320 } else {
321 match &node.local {
322 None => {
323 node.local = Some(MutationInfo {
324 kind: entry.kind,
325 loc,
326 });
327 }
328 Some(existing) if existing.kind < entry.kind => {
329 node.local = Some(MutationInfo {
330 kind: entry.kind,
331 loc,
332 });
333 }
334 _ => {}
335 }
336 }
337
338 // Forward edges: Capture a -> b, Alias a -> b: mutate(a) => mutate(b)
339 // Collect edges to avoid borrow conflict
340 let edges: Vec<Edge> = node.edges.clone();
341 let node_value_kind = match &node.value {
342 NodeValue::Phi => "Phi",
343 _ => "Other",
344 };
345 let node_aliases: Vec<(IdentifierId, usize)> =
346 node.aliases.iter().map(|(&k, &v)| (k, v)).collect();
347 let node_maybe_aliases: Vec<(IdentifierId, usize)> =
348 node.maybe_aliases.iter().map(|(&k, &v)| (k, v)).collect();
349 let node_captures: Vec<(IdentifierId, usize)> =
350 node.captures.iter().map(|(&k, &v)| (k, v)).collect();
351 let node_created_from: Vec<(IdentifierId, usize)> =
352 node.created_from.iter().map(|(&k, &v)| (k, v)).collect();
353
354 for edge in &edges {
355 if edge.index >= index {
356 break;
357 }
358 queue.push(QueueEntry {
359 place: edge.node,
360 transitive: entry.transitive,
361 direction: Direction::Forwards,
362 // MaybeAlias edges downgrade to conditional mutation
363 kind: if edge.kind == EdgeKind::MaybeAlias {
364 MutationKind::Conditional
365 } else {
366 entry.kind
367 },
368 });
369 }
370
371 for (alias, when) in &node_created_from {
372 if *when >= index {
373 continue;
374 }
375 queue.push(QueueEntry {
376 place: *alias,
377 transitive: true,
378 direction: Direction::Backwards,
379 kind: entry.kind,
380 });
381 }
382
383 if entry.direction == Direction::Backwards || node_value_kind != "Phi" {
384 // Backward alias edges
385 for (alias, when) in &node_aliases {
386 if *when >= index {
387 continue;
388 }
389 queue.push(QueueEntry {
390 place: *alias,
391 transitive: entry.transitive,
392 direction: Direction::Backwards,
393 kind: entry.kind,
394 });
395 }
396 // MaybeAlias backward edges (downgrade to conditional)
397 for (alias, when) in &node_maybe_aliases {
398 if *when >= index {
399 continue;
400 }
401 queue.push(QueueEntry {
402 place: *alias,
403 transitive: entry.transitive,
404 direction: Direction::Backwards,
405 kind: MutationKind::Conditional,
406 });
407 }
408 }
409
410 // Only transitive mutations affect captures backward
411 if entry.transitive {
412 for (capture, when) in &node_captures {
413 if *when >= index {
414 continue;
415 }
416 queue.push(QueueEntry {
417 place: *capture,
418 transitive: entry.transitive,
419 direction: Direction::Backwards,
420 kind: entry.kind,
421 });
422 }
423 }
424 }
425 }
426 }
427
428 // =============================================================================
429 // Helper: append function errors
430 // =============================================================================
431
432 fn append_function_errors(env: &mut Environment, function_id: FunctionId) {
433 let func = &env.functions[function_id.0 as usize];
434 if let Some(ref effects) = func.aliasing_effects {
435 // Collect errors first to avoid borrow conflict
436 let errors: Vec<_> = effects
437 .iter()
438 .filter_map(|effect| match effect {
439 AliasingEffect::Impure { error, .. }
440 | AliasingEffect::MutateFrozen { error, .. }
441 | AliasingEffect::MutateGlobal { error, .. } => Some(error.clone()),
442 _ => None,
443 })
444 .collect();
445 for error in errors {
446 env.record_diagnostic(error);
447 }
448 }
449 }
450
451 // =============================================================================
452 // Public entry point
453 // =============================================================================
454
455 /// Infers mutable ranges for identifiers and populates Place effects.
456 ///
457 /// Returns the externally-visible effects of the function (mutations of
458 /// params/context-vars, aliasing between params/context-vars/return).
459 ///
460 /// Corresponds to TS `inferMutationAliasingRanges(fn, {isFunctionExpression})`.
461 pub fn infer_mutation_aliasing_ranges(
462 func: &mut HirFunction,
463 env: &mut Environment,
464 is_function_expression: bool,
465 ) -> Result<Vec<AliasingEffect>, CompilerDiagnostic> {
466 let mut function_effects: Vec<AliasingEffect> = Vec::new();
467
468 // =========================================================================
469 // Part 1: Build data flow graph and infer mutable ranges
470 // =========================================================================
471 let mut state = AliasingState::new();
472
473 struct PendingPhiOperand {
474 from: Place,
475 into: Place,
476 index: usize,
477 }
478 let mut pending_phis: FxHashMap<BlockId, Vec<PendingPhiOperand>> = FxHashMap::default();
479
480 struct PendingMutation {
481 index: usize,
482 id: EvaluationOrder,
483 transitive: bool,
484 kind: MutationKind,
485 place: Place,
486 reason: Option<MutationReason>,
487 }
488 let mut mutations: Vec<PendingMutation> = Vec::new();
489
490 struct PendingRender {
491 index: usize,
492 place: Place,
493 }
494 let mut renders: Vec<PendingRender> = Vec::new();
495
496 let mut index: usize = 0;
497
498 let should_record_errors = !is_function_expression && env.enable_validations();
499
500 // Create nodes for params, context vars, and return
501 for param in &func.params {
502 let place = match param {
503 react_compiler_hir::ParamPattern::Place(p) => p,
504 react_compiler_hir::ParamPattern::Spread(s) => &s.place,
505 };
506 state.create(place, NodeValue::Object);
507 }
508 for ctx in &func.context {
509 state.create(ctx, NodeValue::Object);
510 }
511 state.create(&func.returns, NodeValue::Object);
512
513 let mut seen_blocks: FxHashSet<BlockId> = FxHashSet::default();
514
515 // Collect block iteration data to avoid borrow conflicts
516 let block_order: Vec<BlockId> = func.body.blocks.keys().cloned().collect();
517
518 for &block_id in &block_order {
519 let block = &func.body.blocks[&block_id];
520
521 // Process phis
522 for phi in &block.phis {
523 state.create(&phi.place, NodeValue::Phi);
524 for (&pred, operand) in &phi.operands {
525 if !seen_blocks.contains(&pred) {
526 pending_phis
527 .entry(pred)
528 .or_insert_with(Vec::new)
529 .push(PendingPhiOperand {
530 from: operand.clone(),
531 into: phi.place.clone(),
532 index: index,
533 });
534 index += 1;
535 } else {
536 state.assign(index, operand, &phi.place);
537 index += 1;
538 }
539 }
540 }
541 seen_blocks.insert(block_id);
542
543 // Process instruction effects
544 let instr_ids: Vec<_> = block.instructions.clone();
545 for instr_id in &instr_ids {
546 let instr = &func.instructions[instr_id.0 as usize];
547 let instr_eval_order = instr.id;
548 let effects = match &instr.effects {
549 Some(e) => e.clone(),
550 None => continue,
551 };
552 for effect in &effects {
553 match effect {
554 AliasingEffect::Create { into, .. } => {
555 state.create(into, NodeValue::Object);
556 }
557 AliasingEffect::CreateFunction {
558 into, function_id, ..
559 } => {
560 state.create(
561 into,
562 NodeValue::Function {
563 function_id: *function_id,
564 },
565 );
566 }
567 AliasingEffect::CreateFrom { from, into } => {
568 state.create_from(index, from, into);
569 index += 1;
570 }
571 AliasingEffect::Assign { from, into } => {
572 if !state.nodes.contains_key(&into.identifier) {
573 state.create(into, NodeValue::Object);
574 }
575 state.assign(index, from, into);
576 index += 1;
577 }
578 AliasingEffect::Alias { from, into } => {
579 state.assign(index, from, into);
580 index += 1;
581 }
582 AliasingEffect::MaybeAlias { from, into } => {
583 state.maybe_alias(index, from, into);
584 index += 1;
585 }
586 AliasingEffect::Capture { from, into } => {
587 state.capture(index, from, into);
588 index += 1;
589 }
590 AliasingEffect::MutateTransitive { value }
591 | AliasingEffect::MutateTransitiveConditionally { value } => {
592 let is_transitive_conditional =
593 matches!(effect, AliasingEffect::MutateTransitiveConditionally { .. });
594 mutations.push(PendingMutation {
595 index: index,
596 id: instr_eval_order,
597 transitive: true,
598 kind: if is_transitive_conditional {
599 MutationKind::Conditional
600 } else {
601 MutationKind::Definite
602 },
603 reason: None,
604 place: value.clone(),
605 });
606 index += 1;
607 }
608 AliasingEffect::Mutate { value, reason } => {
609 mutations.push(PendingMutation {
610 index: index,
611 id: instr_eval_order,
612 transitive: false,
613 kind: MutationKind::Definite,
614 reason: reason.clone(),
615 place: value.clone(),
616 });
617 index += 1;
618 }
619 AliasingEffect::MutateConditionally { value } => {
620 mutations.push(PendingMutation {
621 index: index,
622 id: instr_eval_order,
623 transitive: false,
624 kind: MutationKind::Conditional,
625 reason: None,
626 place: value.clone(),
627 });
628 index += 1;
629 }
630 AliasingEffect::MutateFrozen { .. }
631 | AliasingEffect::MutateGlobal { .. }
632 | AliasingEffect::Impure { .. } => {
633 if should_record_errors {
634 match effect {
635 AliasingEffect::MutateFrozen { error, .. }
636 | AliasingEffect::MutateGlobal { error, .. }
637 | AliasingEffect::Impure { error, .. } => {
638 env.record_diagnostic(error.clone());
639 }
640 _ => unreachable!(),
641 }
642 }
643 function_effects.push(effect.clone());
644 }
645 AliasingEffect::Render { place } => {
646 renders.push(PendingRender {
647 index: index,
648 place: place.clone(),
649 });
650 index += 1;
651 function_effects.push(effect.clone());
652 }
653 // Other effects (Freeze, ImmutableCapture, Apply) are no-ops here
654 _ => {}
655 }
656 }
657 }
658
659 // Process pending phis for this block
660 let block = &func.body.blocks[&block_id];
661 if let Some(block_phis) = pending_phis.remove(&block_id) {
662 for pending in block_phis {
663 state.assign(pending.index, &pending.from, &pending.into);
664 }
665 }
666
667 // Handle return terminal
668 let terminal = &block.terminal;
669 if let react_compiler_hir::Terminal::Return { value, .. } = terminal {
670 state.assign(index, value, &func.returns);
671 index += 1;
672 }
673
674 // Handle terminal effects (MaybeThrow and Return)
675 let terminal_effects = match terminal {
676 react_compiler_hir::Terminal::MaybeThrow { effects, .. }
677 | react_compiler_hir::Terminal::Return { effects, .. } => effects.clone(),
678 _ => None,
679 };
680 if let Some(effects) = terminal_effects {
681 for effect in &effects {
682 match effect {
683 AliasingEffect::Alias { from, into } => {
684 state.assign(index, from, into);
685 index += 1;
686 }
687 AliasingEffect::Freeze { .. } => {
688 // Expected for MaybeThrow terminals, skip
689 }
690 _ => {
691 // TS: CompilerError.invariant(effect.kind === 'Freeze', ...)
692 // We skip non-Alias, non-Freeze effects
693 }
694 }
695 }
696 }
697 }
698
699 // Process mutations
700 for mutation in &mutations {
701 state.mutate(
702 mutation.index,
703 mutation.place.identifier,
704 Some(EvaluationOrder(mutation.id.0 + 1)),
705 mutation.transitive,
706 mutation.kind,
707 mutation.place.loc,
708 mutation.reason.clone(),
709 env,
710 should_record_errors,
711 );
712 }
713
714 // Process renders
715 for render in &renders {
716 if should_record_errors {
717 state.render(render.index, render.place.identifier, env);
718 }
719 }
720
721 // Collect function effects for context vars and params
722 // NOTE: TS iterates [...fn.context, ...fn.params] — context first, then params
723 for ctx in &func.context {
724 collect_param_effects(&state, ctx, &mut function_effects);
725 }
726 for param in &func.params {
727 let place = match param {
728 react_compiler_hir::ParamPattern::Place(p) => p,
729 react_compiler_hir::ParamPattern::Spread(s) => &s.place,
730 };
731 collect_param_effects(&state, place, &mut function_effects);
732 }
733
734 // Set effect on mutated params/context vars
735 // We need to do this in a separate pass because we need to know which params
736 // were mutated before setting effects
737 let mut captured_params: FxHashSet<IdentifierId> = FxHashSet::default();
738 for param in &func.params {
739 let place = match param {
740 react_compiler_hir::ParamPattern::Place(p) => p,
741 react_compiler_hir::ParamPattern::Spread(s) => &s.place,
742 };
743 if let Some(node) = state.nodes.get(&place.identifier) {
744 if node.local.is_some() || node.transitive.is_some() {
745 captured_params.insert(place.identifier);
746 }
747 }
748 }
749 for ctx in &func.context {
750 if let Some(node) = state.nodes.get(&ctx.identifier) {
751 if node.local.is_some() || node.transitive.is_some() {
752 captured_params.insert(ctx.identifier);
753 }
754 }
755 }
756
757 // Now mutate the effects on params/context in place
758 for param in &mut func.params {
759 let place = match param {
760 react_compiler_hir::ParamPattern::Place(p) => p,
761 react_compiler_hir::ParamPattern::Spread(s) => &mut s.place,
762 };
763 if captured_params.contains(&place.identifier) {
764 place.effect = Effect::Capture;
765 }
766 }
767 for ctx in &mut func.context {
768 if captured_params.contains(&ctx.identifier) {
769 ctx.effect = Effect::Capture;
770 }
771 }
772
773 // =========================================================================
774 // Part 2: Add legacy operand-specific effects based on instruction effects
775 // and mutable ranges. Also fix up mutable range start values.
776 // =========================================================================
777 // Part 2 loop
778 for &block_id in &block_order {
779 let block = &func.body.blocks[&block_id];
780
781 // Process phis
782 let phi_data: Vec<_> = block
783 .phis
784 .iter()
785 .map(|phi| {
786 let first_instr_id = block
787 .instructions
788 .first()
789 .map(|id| func.instructions[id.0 as usize].id)
790 .unwrap_or_else(|| block.terminal.evaluation_order());
791
792 let is_mutated_after_creation = env.identifiers[phi.place.identifier.0 as usize]
793 .mutable_range
794 .end
795 > first_instr_id;
796
797 (
798 phi.place.identifier,
799 phi.operands
800 .values()
801 .map(|o| o.identifier)
802 .collect::<Vec<_>>(),
803 is_mutated_after_creation,
804 first_instr_id,
805 )
806 })
807 .collect();
808
809 for (phi_id, _operand_ids, is_mutated_after_creation, first_instr_id) in &phi_data {
810 // Set phi place effect to Store
811 // We need to find this phi in the block and set it
812 let block = func.body.blocks.get_mut(&block_id).unwrap();
813 for phi in &mut block.phis {
814 if phi.place.identifier == *phi_id {
815 phi.place.effect = Effect::Store;
816 for operand in phi.operands.values_mut() {
817 operand.effect = if *is_mutated_after_creation {
818 Effect::Capture
819 } else {
820 Effect::Read
821 };
822 }
823 break;
824 }
825 }
826
827 if *is_mutated_after_creation {
828 let ident = &mut env.identifiers[phi_id.0 as usize];
829 if ident.mutable_range.start == EvaluationOrder(0) {
830 ident.mutable_range.start = EvaluationOrder(first_instr_id.0.saturating_sub(1));
831 }
832 }
833 }
834
835 let block = &func.body.blocks[&block_id];
836 let instr_ids: Vec<_> = block.instructions.clone();
837
838 for instr_id in &instr_ids {
839 let instr = &func.instructions[instr_id.0 as usize];
840 let eval_order = instr.id;
841
842 // Set lvalue effect to ConditionallyMutate and fix up mutable range
843 // This covers the top-level lvalue
844 let lvalue_id = instr.lvalue.identifier;
845 {
846 let ident = &mut env.identifiers[lvalue_id.0 as usize];
847 if ident.mutable_range.start == EvaluationOrder(0) {
848 ident.mutable_range.start = eval_order;
849 }
850 if ident.mutable_range.end == EvaluationOrder(0) {
851 ident.mutable_range.end =
852 EvaluationOrder((eval_order.0 + 1).max(ident.mutable_range.end.0));
853 }
854 }
855 func.instructions[instr_id.0 as usize].lvalue.effect = Effect::ConditionallyMutate;
856
857 // Also handle value-level lvalues (DeclareLocal, StoreLocal, etc.)
858 let value_lvalue_ids: Vec<IdentifierId> =
859 each_instruction_value_lvalue(&func.instructions[instr_id.0 as usize].value)
860 .into_iter()
861 .map(|p| p.identifier)
862 .collect();
863 for vlid in &value_lvalue_ids {
864 let ident = &mut env.identifiers[vlid.0 as usize];
865 if ident.mutable_range.start == EvaluationOrder(0) {
866 ident.mutable_range.start = eval_order;
867 }
868 if ident.mutable_range.end == EvaluationOrder(0) {
869 ident.mutable_range.end =
870 EvaluationOrder((eval_order.0 + 1).max(ident.mutable_range.end.0));
871 }
872 }
873 for_each_instruction_value_lvalue_mut(
874 &mut func.instructions[instr_id.0 as usize].value,
875 &mut |place| {
876 place.effect = Effect::ConditionallyMutate;
877 },
878 );
879
880 // Set operand effects to Read
881 for_each_instruction_value_operand_mut(
882 &mut func.instructions[instr_id.0 as usize].value,
883 &mut |place| {
884 place.effect = Effect::Read;
885 },
886 );
887
888 let instr = &func.instructions[instr_id.0 as usize];
889 if instr.effects.is_none() {
890 continue;
891 }
892
893 // Compute operand effects from instruction effects
894 let effects = instr.effects.as_ref().unwrap().clone();
895 let mut operand_effects: FxHashMap<IdentifierId, Effect> = FxHashMap::default();
896
897 for effect in &effects {
898 match effect {
899 AliasingEffect::Assign { from, into, .. }
900 | AliasingEffect::Alias { from, into }
901 | AliasingEffect::Capture { from, into }
902 | AliasingEffect::CreateFrom { from, into }
903 | AliasingEffect::MaybeAlias { from, into } => {
904 let is_mutated_or_reassigned = env.identifiers[into.identifier.0 as usize]
905 .mutable_range
906 .end
907 > eval_order;
908 if is_mutated_or_reassigned {
909 operand_effects.insert(from.identifier, Effect::Capture);
910 operand_effects.insert(into.identifier, Effect::Store);
911 } else {
912 operand_effects.insert(from.identifier, Effect::Read);
913 operand_effects.insert(into.identifier, Effect::Store);
914 }
915 }
916 AliasingEffect::CreateFunction { .. } | AliasingEffect::Create { .. } => {
917 // no-op
918 }
919 AliasingEffect::Mutate { value, .. } => {
920 operand_effects.insert(value.identifier, Effect::Store);
921 }
922 AliasingEffect::Apply { .. } => {
923 return Err(CompilerDiagnostic::new(
924 ErrorCategory::Invariant,
925 "[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects",
926 None,
927 ));
928 }
929 AliasingEffect::MutateTransitive { value, .. }
930 | AliasingEffect::MutateConditionally { value }
931 | AliasingEffect::MutateTransitiveConditionally { value } => {
932 operand_effects.insert(value.identifier, Effect::ConditionallyMutate);
933 }
934 AliasingEffect::Freeze { value, .. } => {
935 operand_effects.insert(value.identifier, Effect::Freeze);
936 }
937 AliasingEffect::ImmutableCapture { .. } => {
938 // no-op, Read is the default
939 }
940 AliasingEffect::Impure { .. }
941 | AliasingEffect::Render { .. }
942 | AliasingEffect::MutateFrozen { .. }
943 | AliasingEffect::MutateGlobal { .. } => {
944 // no-op
945 }
946 }
947 }
948
949 // Apply operand effects to top-level lvalue
950 let instr = &mut func.instructions[instr_id.0 as usize];
951 let lvalue_id = instr.lvalue.identifier;
952 if let Some(&effect) = operand_effects.get(&lvalue_id) {
953 instr.lvalue.effect = effect;
954 }
955 // Apply operand effects to value-level lvalues
956 for_each_instruction_value_lvalue_mut(&mut instr.value, &mut |place| {
957 if let Some(&effect) = operand_effects.get(&place.identifier) {
958 place.effect = effect;
959 }
960 });
961
962 // Apply operand effects to value operands and fix up mutable ranges
963 {
964 let mut apply = |place: &mut Place| {
965 // Fix up mutable range start
966 let ident = &env.identifiers[place.identifier.0 as usize];
967 if ident.mutable_range.end > eval_order
968 && ident.mutable_range.start == EvaluationOrder(0)
969 {
970 env.identifiers[place.identifier.0 as usize]
971 .mutable_range
972 .start = eval_order;
973 }
974 // Apply effect
975 if let Some(&effect) = operand_effects.get(&place.identifier) {
976 place.effect = effect;
977 }
978 };
979 for_each_instruction_value_operand_mut(&mut instr.value, &mut apply);
980
981 // FunctionExpression/ObjectMethod context variables are operands that
982 // require env access (they live in env.functions[func_id].context).
983 if let InstructionValue::FunctionExpression { lowered_func, .. }
984 | InstructionValue::ObjectMethod { lowered_func, .. } = &instr.value
985 {
986 let func_id = lowered_func.func;
987 let ctx_ids: Vec<IdentifierId> = env.functions[func_id.0 as usize]
988 .context
989 .iter()
990 .map(|c| c.identifier)
991 .collect();
992 for ctx_id in &ctx_ids {
993 let ident = &env.identifiers[ctx_id.0 as usize];
994 if ident.mutable_range.end > eval_order
995 && ident.mutable_range.start == EvaluationOrder(0)
996 {
997 env.identifiers[ctx_id.0 as usize].mutable_range.start = eval_order;
998 }
999 let effect = operand_effects.get(ctx_id).copied().unwrap_or(Effect::Read);
1000 let inner_func = &mut env.functions[func_id.0 as usize];
1001 for ctx_place in &mut inner_func.context {
1002 if ctx_place.identifier == *ctx_id {
1003 ctx_place.effect = effect;
1004 }
1005 }
1006 }
1007 }
1008 }
1009
1010 // Handle StoreContext case: extend rvalue range if needed
1011 let instr = &func.instructions[instr_id.0 as usize];
1012 if let InstructionValue::StoreContext { value, .. } = &instr.value {
1013 let val_id = value.identifier;
1014 let val_range_end = env.identifiers[val_id.0 as usize].mutable_range.end;
1015 if val_range_end <= eval_order {
1016 env.identifiers[val_id.0 as usize].mutable_range.end =
1017 EvaluationOrder(eval_order.0 + 1);
1018 }
1019 }
1020 }
1021
1022 // Set terminal operand effects
1023 let block = func.body.blocks.get_mut(&block_id).unwrap();
1024 match &mut block.terminal {
1025 react_compiler_hir::Terminal::Return { value, .. } => {
1026 value.effect = if is_function_expression {
1027 Effect::Read
1028 } else {
1029 Effect::Freeze
1030 };
1031 }
1032 terminal => {
1033 for_each_terminal_operand_mut(terminal, &mut |place| {
1034 place.effect = Effect::Read;
1035 });
1036 }
1037 }
1038 }
1039
1040 // =========================================================================
1041 // Part 3: Finish populating the externally visible effects
1042 // =========================================================================
1043 let returns_id = func.returns.identifier;
1044 let returns_type_id = env.identifiers[returns_id.0 as usize].type_;
1045 let returns_type = &env.types[returns_type_id.0 as usize];
1046 let return_value_kind = if is_primitive_type(returns_type) {
1047 ValueKind::Primitive
1048 } else if is_jsx_type(returns_type) {
1049 ValueKind::Frozen
1050 } else {
1051 ValueKind::Mutable
1052 };
1053
1054 function_effects.push(AliasingEffect::Create {
1055 into: func.returns.clone(),
1056 value: return_value_kind,
1057 reason: ValueReason::KnownReturnSignature,
1058 });
1059
1060 // Determine precise data-flow effects by simulating transitive mutations
1061 let mut tracked: Vec<Place> = Vec::new();
1062 for param in &func.params {
1063 let place = match param {
1064 react_compiler_hir::ParamPattern::Place(p) => p.clone(),
1065 react_compiler_hir::ParamPattern::Spread(s) => s.place.clone(),
1066 };
1067 tracked.push(place);
1068 }
1069 for ctx in &func.context {
1070 tracked.push(ctx.clone());
1071 }
1072 tracked.push(func.returns.clone());
1073
1074 let returns_identifier_id = func.returns.identifier;
1075
1076 for i in 0..tracked.len() {
1077 let into = tracked[i].clone();
1078 let mutation_index = index;
1079 index += 1;
1080
1081 state.mutate(
1082 mutation_index,
1083 into.identifier,
1084 None, // simulated mutation
1085 true,
1086 MutationKind::Conditional,
1087 into.loc,
1088 None,
1089 env,
1090 false, // never record errors for simulated mutations
1091 );
1092
1093 for j in 0..tracked.len() {
1094 let from = &tracked[j];
1095 if from.identifier == into.identifier || from.identifier == returns_identifier_id {
1096 continue;
1097 }
1098
1099 let from_node = state.nodes.get(&from.identifier);
1100 assert!(
1101 from_node.is_some(),
1102 "Expected a node to exist for all parameters and context variables"
1103 );
1104 let from_node = from_node.unwrap();
1105
1106 if from_node.last_mutated == mutation_index {
1107 if into.identifier == returns_identifier_id {
1108 function_effects.push(AliasingEffect::Alias {
1109 from: from.clone(),
1110 into: into.clone(),
1111 });
1112 } else {
1113 function_effects.push(AliasingEffect::Capture {
1114 from: from.clone(),
1115 into: into.clone(),
1116 });
1117 }
1118 }
1119 }
1120 }
1121
1122 Ok(function_effects)
1123 }
1124
1125 // =============================================================================
1126 // Helper: collect param/context mutation effects
1127 // =============================================================================
1128
1129 fn collect_param_effects(
1130 state: &AliasingState,
1131 place: &Place,
1132 function_effects: &mut Vec<AliasingEffect>,
1133 ) {
1134 let node = match state.nodes.get(&place.identifier) {
1135 Some(n) => n,
1136 None => return,
1137 };
1138
1139 if let Some(ref local) = node.local {
1140 match local.kind {
1141 MutationKind::Conditional => {
1142 function_effects.push(AliasingEffect::MutateConditionally {
1143 value: Place {
1144 loc: local.loc,
1145 ..place.clone()
1146 },
1147 });
1148 }
1149 MutationKind::Definite => {
1150 function_effects.push(AliasingEffect::Mutate {
1151 value: Place {
1152 loc: local.loc,
1153 ..place.clone()
1154 },
1155 reason: node.mutation_reason.clone(),
1156 });
1157 }
1158 MutationKind::None => {}
1159 }
1160 }
1161
1162 if let Some(ref transitive) = node.transitive {
1163 match transitive.kind {
1164 MutationKind::Conditional => {
1165 function_effects.push(AliasingEffect::MutateTransitiveConditionally {
1166 value: Place {
1167 loc: transitive.loc,
1168 ..place.clone()
1169 },
1170 });
1171 }
1172 MutationKind::Definite => {
1173 function_effects.push(AliasingEffect::MutateTransitive {
1174 value: Place {
1175 loc: transitive.loc,
1176 ..place.clone()
1177 },
1178 });
1179 }
1180 MutationKind::None => {}
1181 }
1182 }
1183 }