Skip to main content

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