Skip to main content

react_compiler_inference/
infer_mutation_aliasing_effects.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 the mutation/aliasing effects for instructions and terminals.
7//!
8//! Ported from TypeScript `src/Inference/InferMutationAliasingEffects.ts`.
9//!
10//! This pass uses abstract interpretation to compute effects describing
11//! creation, aliasing, mutation, freezing, and error conditions for each
12//! instruction and terminal in the HIR.
13
14use std::collections::HashMap;
15use std::collections::HashSet;
16
17use indexmap::IndexSet;
18use react_compiler_diagnostics::CompilerDiagnostic;
19use react_compiler_diagnostics::CompilerDiagnosticDetail;
20use react_compiler_diagnostics::ErrorCategory;
21use react_compiler_hir::AliasingEffect;
22use react_compiler_hir::AliasingSignature;
23use react_compiler_hir::BlockId;
24use react_compiler_hir::DeclarationId;
25use react_compiler_hir::Effect;
26use react_compiler_hir::FunctionId;
27use react_compiler_hir::HirFunction;
28use react_compiler_hir::IdentifierId;
29use react_compiler_hir::InstructionKind;
30use react_compiler_hir::InstructionValue;
31use react_compiler_hir::MutationReason;
32use react_compiler_hir::ParamPattern;
33use react_compiler_hir::Place;
34use react_compiler_hir::PlaceOrSpread;
35use react_compiler_hir::PlaceOrSpreadOrHole;
36use react_compiler_hir::ReactFunctionType;
37use react_compiler_hir::SourceLocation;
38use react_compiler_hir::Type;
39use react_compiler_hir::environment::Environment;
40use react_compiler_hir::object_shape::BUILT_IN_ARRAY_ID;
41use react_compiler_hir::object_shape::BUILT_IN_MAP_ID;
42use react_compiler_hir::object_shape::BUILT_IN_SET_ID;
43use react_compiler_hir::object_shape::FunctionSignature;
44use react_compiler_hir::object_shape::HookKind;
45use react_compiler_hir::type_config::ValueKind;
46use react_compiler_hir::type_config::ValueReason;
47use react_compiler_hir::visitors;
48
49// =============================================================================
50// Public entry point
51// =============================================================================
52
53/// Infers mutation/aliasing effects for all instructions and terminals in `func`.
54///
55/// Corresponds to TS `inferMutationAliasingEffects(fn, {isFunctionExpression})`.
56pub fn infer_mutation_aliasing_effects(
57    func: &mut HirFunction,
58    env: &mut Environment,
59    is_function_expression: bool,
60) -> Result<(), CompilerDiagnostic> {
61    let mut initial_state = InferenceState::empty(env, is_function_expression);
62
63    // Map of blocks to the last (merged) incoming state that was processed
64    let mut states_by_block: HashMap<BlockId, InferenceState> = HashMap::new();
65
66    // Initialize context variables
67    for ctx_place in &func.context {
68        let value_id = ValueId::new();
69        initial_state.initialize(
70            value_id,
71            AbstractValue {
72                kind: ValueKind::Context,
73                reason: hashset_of(ValueReason::Other),
74            },
75        );
76        initial_state.define(ctx_place.identifier, value_id);
77    }
78
79    let param_kind: AbstractValue = if is_function_expression {
80        AbstractValue {
81            kind: ValueKind::Mutable,
82            reason: hashset_of(ValueReason::Other),
83        }
84    } else {
85        AbstractValue {
86            kind: ValueKind::Frozen,
87            reason: hashset_of(ValueReason::ReactiveFunctionArgument),
88        }
89    };
90
91    if func.fn_type == ReactFunctionType::Component {
92        // Component: at most 2 params (props, ref)
93        let params_len = func.params.len();
94        if params_len > 0 {
95            infer_param(&func.params[0], &mut initial_state, &param_kind);
96        }
97        if params_len > 1 {
98            let ref_place = match &func.params[1] {
99                ParamPattern::Place(p) => p,
100                ParamPattern::Spread(s) => &s.place,
101            };
102            let value_id = ValueId::new();
103            initial_state.initialize(
104                value_id,
105                AbstractValue {
106                    kind: ValueKind::Mutable,
107                    reason: hashset_of(ValueReason::Other),
108                },
109            );
110            initial_state.define(ref_place.identifier, value_id);
111        }
112    } else {
113        for param in &func.params {
114            infer_param(param, &mut initial_state, &param_kind);
115        }
116    }
117
118    let mut queued_states: indexmap::IndexMap<BlockId, InferenceState> = indexmap::IndexMap::new();
119
120    // Queue helper
121    fn queue(
122        queued_states: &mut indexmap::IndexMap<BlockId, InferenceState>,
123        states_by_block: &HashMap<BlockId, InferenceState>,
124        block_id: BlockId,
125        state: InferenceState,
126    ) {
127        if let Some(queued_state) = queued_states.get(&block_id) {
128            let merged = queued_state.merge(&state);
129            let new_state = merged.unwrap_or_else(|| queued_state.clone());
130            queued_states.insert(block_id, new_state);
131        } else {
132            let prev_state = states_by_block.get(&block_id);
133            if let Some(prev) = prev_state {
134                let next_state = prev.merge(&state);
135                if let Some(next) = next_state {
136                    queued_states.insert(block_id, next);
137                }
138            } else {
139                queued_states.insert(block_id, state);
140            }
141        }
142    }
143
144    queue(
145        &mut queued_states,
146        &states_by_block,
147        func.body.entry,
148        initial_state,
149    );
150
151    let hoisted_context_declarations = find_hoisted_context_declarations(func, env);
152    let non_mutating_spreads = find_non_mutated_destructure_spreads(func, env);
153
154    let mut context = Context {
155        interned_effects: HashMap::new(),
156        instruction_signature_cache: HashMap::new(),
157        catch_handlers: HashMap::new(),
158        is_function_expression,
159        hoisted_context_declarations,
160        non_mutating_spreads,
161        effect_value_id_cache: HashMap::new(),
162        function_values: HashMap::new(),
163        function_signature_cache: HashMap::new(),
164        aliasing_config_temp_cache: HashMap::new(),
165    };
166
167    let mut iteration_count = 0;
168
169    while !queued_states.is_empty() {
170        iteration_count += 1;
171        if iteration_count > 100 {
172            return Err(CompilerDiagnostic::new(
173                ErrorCategory::Invariant,
174                "[InferMutationAliasingEffects] Potential infinite loop: \
175                 A value, temporary place, or effect was not cached properly",
176                None,
177            ));
178        }
179
180        // Collect block IDs to process in order
181        let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
182        for block_id in block_ids {
183            let incoming_state = match queued_states.swap_remove(&block_id) {
184                Some(s) => s,
185                None => continue,
186            };
187
188            states_by_block.insert(block_id, incoming_state.clone());
189            let mut state = incoming_state.clone();
190
191            infer_block(&mut context, &mut state, block_id, func, env)?;
192
193            // Check for uninitialized identifier access (matches TS invariant:
194            // "Expected value kind to be initialized")
195            if let Some((uninitialized_id, usage_loc)) = state.uninitialized_access.get() {
196                let ident_info = env.identifiers.get(uninitialized_id.0 as usize);
197                let name = ident_info
198                    .and_then(|ident| ident.name.as_ref())
199                    .map(|n| n.value().to_string())
200                    .unwrap_or_else(|| "".to_string());
201                // Use usage_loc if available, otherwise fall back to identifier's own loc
202                let error_loc = usage_loc.or_else(|| ident_info.and_then(|i| i.loc));
203                // Match TS printPlace format: "<unknown> name$id:type"
204                let type_str = ident_info
205                    .map(|ident| {
206                        let ty = &env.types[ident.type_.0 as usize];
207                        format_type_for_print(ty)
208                    })
209                    .unwrap_or_default();
210                let description = format!("<unknown> {}${}{}", name, uninitialized_id.0, type_str);
211                let diag = CompilerDiagnostic::new(
212                    ErrorCategory::Invariant,
213                    "[InferMutationAliasingEffects] Expected value kind to be initialized",
214                    Some(description),
215                )
216                .with_detail(CompilerDiagnosticDetail::Error {
217                    loc: error_loc,
218                    message: Some("this is uninitialized".to_string()),
219                    identifier_name: None,
220                });
221                return Err(diag);
222            }
223
224            // Queue successors
225            let successors = terminal_successors(&func.body.blocks[&block_id].terminal);
226            for next_block_id in successors {
227                queue(
228                    &mut queued_states,
229                    &states_by_block,
230                    next_block_id,
231                    state.clone(),
232                );
233            }
234        }
235    }
236
237    Ok(())
238}
239
240// =============================================================================
241// ValueId: replaces InstructionValue identity as allocation-site key
242// =============================================================================
243
244/// Unique allocation-site identifier, replacing TS's object-identity on InstructionValue.
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
246struct ValueId(u32);
247
248use std::sync::atomic::AtomicU32;
249use std::sync::atomic::Ordering;
250static NEXT_VALUE_ID: AtomicU32 = AtomicU32::new(1);
251
252impl ValueId {
253    fn new() -> Self {
254        ValueId(NEXT_VALUE_ID.fetch_add(1, Ordering::Relaxed))
255    }
256}
257
258// =============================================================================
259// AbstractValue
260// =============================================================================
261
262#[derive(Debug, Clone)]
263struct AbstractValue {
264    kind: ValueKind,
265    reason: IndexSet<ValueReason>,
266}
267
268fn hashset_of(r: ValueReason) -> IndexSet<ValueReason> {
269    let mut s = IndexSet::new();
270    s.insert(r);
271    s
272}
273
274// =============================================================================
275// InferenceState
276// =============================================================================
277
278/// The abstract state tracked during inference.
279/// Uses interior mutability via a struct with direct fields (no Rc needed since
280/// we always have exclusive access in the pass).
281#[derive(Debug, Clone)]
282struct InferenceState {
283    is_function_expression: bool,
284    /// The kind of each value, based on its allocation site
285    values: HashMap<ValueId, AbstractValue>,
286    /// The set of values pointed to by each identifier
287    variables: HashMap<IdentifierId, HashSet<ValueId>>,
288    /// Tracks uninitialized identifier access errors (matches TS invariant).
289    /// Uses Cell so it can be set from `&self` methods like `kind()`.
290    /// Stores (IdentifierId, usage_loc) where usage_loc is the source location
291    /// of the Place that triggered the uninitialized access.
292    uninitialized_access: std::cell::Cell<Option<(IdentifierId, Option<SourceLocation>)>>,
293}
294
295impl InferenceState {
296    fn empty(_env: &Environment, is_function_expression: bool) -> Self {
297        InferenceState {
298            is_function_expression,
299            values: HashMap::new(),
300            variables: HashMap::new(),
301            uninitialized_access: std::cell::Cell::new(None),
302        }
303    }
304
305    /// Check the kind of a place, recording the usage location for error reporting.
306    fn kind_with_loc(
307        &self,
308        place_id: IdentifierId,
309        usage_loc: Option<SourceLocation>,
310    ) -> AbstractValue {
311        let values = match self.variables.get(&place_id) {
312            Some(v) => v,
313            None => {
314                if self.uninitialized_access.get().is_none() {
315                    self.uninitialized_access.set(Some((place_id, usage_loc)));
316                }
317                return AbstractValue {
318                    kind: ValueKind::Mutable,
319                    reason: hashset_of(ValueReason::Other),
320                };
321            }
322        };
323        let mut merged_kind: Option<AbstractValue> = None;
324        for value_id in values {
325            let kind = match self.values.get(value_id) {
326                Some(k) => k,
327                None => continue,
328            };
329            merged_kind = Some(match merged_kind {
330                Some(prev) => merge_abstract_values(&prev, kind),
331                None => kind.clone(),
332            });
333        }
334        merged_kind.unwrap_or_else(|| AbstractValue {
335            kind: ValueKind::Mutable,
336            reason: hashset_of(ValueReason::Other),
337        })
338    }
339
340    fn initialize(&mut self, value_id: ValueId, kind: AbstractValue) {
341        self.values.insert(value_id, kind);
342    }
343
344    fn define(&mut self, place_id: IdentifierId, value_id: ValueId) {
345        let mut set = HashSet::new();
346        set.insert(value_id);
347        self.variables.insert(place_id, set);
348    }
349
350    fn assign(&mut self, into: IdentifierId, from: IdentifierId) {
351        let values = match self.variables.get(&from) {
352            Some(v) => v.clone(),
353            None => {
354                // Create a stable value for uninitialized identifiers
355                // Use a deterministic ID based on the from identifier
356                let vid = ValueId(from.0 | 0x80000000);
357                let mut set = HashSet::new();
358                set.insert(vid);
359                if !self.values.contains_key(&vid) {
360                    self.values.insert(
361                        vid,
362                        AbstractValue {
363                            kind: ValueKind::Mutable,
364                            reason: hashset_of(ValueReason::Other),
365                        },
366                    );
367                }
368                set
369            }
370        };
371        self.variables.insert(into, values);
372    }
373
374    fn append_alias(&mut self, place: IdentifierId, value: IdentifierId) {
375        let new_values = match self.variables.get(&value) {
376            Some(v) => v.clone(),
377            None => return,
378        };
379        let prev_values = match self.variables.get(&place) {
380            Some(v) => v.clone(),
381            None => return,
382        };
383        let merged: HashSet<ValueId> = prev_values.union(&new_values).copied().collect();
384        self.variables.insert(place, merged);
385    }
386
387    fn is_defined(&self, place_id: IdentifierId) -> bool {
388        self.variables.contains_key(&place_id)
389    }
390
391    fn values_for(&self, place_id: IdentifierId) -> Vec<ValueId> {
392        match self.variables.get(&place_id) {
393            Some(values) => values.iter().copied().collect(),
394            None => Vec::new(),
395        }
396    }
397
398    #[allow(dead_code)]
399    fn kind_opt(&self, place_id: IdentifierId) -> Option<AbstractValue> {
400        let values = self.variables.get(&place_id)?;
401        let mut merged_kind: Option<AbstractValue> = None;
402        for value_id in values {
403            let kind = self.values.get(value_id)?;
404            merged_kind = Some(match merged_kind {
405                Some(prev) => merge_abstract_values(&prev, kind),
406                None => kind.clone(),
407            });
408        }
409        merged_kind
410    }
411
412    fn kind(&self, place_id: IdentifierId) -> AbstractValue {
413        self.kind_with_loc(place_id, None)
414    }
415
416    fn freeze(&mut self, place_id: IdentifierId, reason: ValueReason) -> bool {
417        // Check if defined first to avoid recording uninitialized access error.
418        // Freeze on undefined identifiers is a no-op — this matches the TS
419        // behavior where freeze() is never called on undefined identifiers
420        // (the invariant in kind() catches this before freeze is reached).
421        if !self.variables.contains_key(&place_id) {
422            return false;
423        }
424        let value = self.kind(place_id);
425        match value.kind {
426            ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => {
427                let value_ids: Vec<ValueId> = self.values_for(place_id);
428                for vid in value_ids {
429                    self.freeze_value(vid, reason);
430                }
431                true
432            }
433            ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => false,
434        }
435    }
436
437    fn freeze_value(&mut self, value_id: ValueId, reason: ValueReason) {
438        self.values.insert(
439            value_id,
440            AbstractValue {
441                kind: ValueKind::Frozen,
442                reason: hashset_of(reason),
443            },
444        );
445        // Note: In TS, this also transitively freezes FunctionExpression captures
446        // if enableTransitivelyFreezeFunctionExpressions is set. We skip that here
447        // since we don't have access to the function arena from within state.
448    }
449
450    #[allow(dead_code)]
451    fn mutate(
452        &self,
453        variant: MutateVariant,
454        place_id: IdentifierId,
455        env: &Environment,
456    ) -> MutationResult {
457        self.mutate_with_loc(variant, place_id, env, None)
458    }
459
460    fn mutate_with_loc(
461        &self,
462        variant: MutateVariant,
463        place_id: IdentifierId,
464        env: &Environment,
465        usage_loc: Option<SourceLocation>,
466    ) -> MutationResult {
467        let ty = &env.types[env.identifiers[place_id.0 as usize].type_.0 as usize];
468        if react_compiler_hir::is_ref_or_ref_value(ty) {
469            return MutationResult::MutateRef;
470        }
471        let kind = self.kind_with_loc(place_id, usage_loc).kind;
472        match variant {
473            MutateVariant::MutateConditionally | MutateVariant::MutateTransitiveConditionally => {
474                match kind {
475                    ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate,
476                    _ => MutationResult::None,
477                }
478            }
479            MutateVariant::Mutate | MutateVariant::MutateTransitive => match kind {
480                ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate,
481                ValueKind::Primitive => MutationResult::None,
482                ValueKind::Frozen | ValueKind::MaybeFrozen => MutationResult::MutateFrozen,
483                ValueKind::Global => MutationResult::MutateGlobal,
484            },
485        }
486    }
487
488    fn merge(&self, other: &InferenceState) -> Option<InferenceState> {
489        let mut next_values: Option<HashMap<ValueId, AbstractValue>> = None;
490        let mut next_variables: Option<HashMap<IdentifierId, HashSet<ValueId>>> = None;
491
492        // Merge values present in both
493        for (id, this_value) in &self.values {
494            if let Some(other_value) = other.values.get(id) {
495                let merged = merge_abstract_values(this_value, other_value);
496                if merged.kind != this_value.kind
497                    || !is_superset(&this_value.reason, &merged.reason)
498                {
499                    let nv = next_values.get_or_insert_with(|| self.values.clone());
500                    nv.insert(*id, merged);
501                }
502            }
503        }
504        // Add values only in other
505        for (id, other_value) in &other.values {
506            if !self.values.contains_key(id) {
507                let nv = next_values.get_or_insert_with(|| self.values.clone());
508                nv.insert(*id, other_value.clone());
509            }
510        }
511
512        // Merge variables present in both
513        for (id, this_values) in &self.variables {
514            if let Some(other_values) = other.variables.get(id) {
515                let mut has_new = false;
516                for ov in other_values {
517                    if !this_values.contains(ov) {
518                        has_new = true;
519                        break;
520                    }
521                }
522                if has_new {
523                    let nvars = next_variables.get_or_insert_with(|| self.variables.clone());
524                    let merged: HashSet<ValueId> =
525                        this_values.union(other_values).copied().collect();
526                    nvars.insert(*id, merged);
527                }
528            }
529        }
530        // Add variables only in other
531        for (id, other_values) in &other.variables {
532            if !self.variables.contains_key(id) {
533                let nvars = next_variables.get_or_insert_with(|| self.variables.clone());
534                nvars.insert(*id, other_values.clone());
535            }
536        }
537
538        if next_variables.is_none() && next_values.is_none() {
539            None
540        } else {
541            Some(InferenceState {
542                is_function_expression: self.is_function_expression,
543                values: next_values.unwrap_or_else(|| self.values.clone()),
544                variables: next_variables.unwrap_or_else(|| self.variables.clone()),
545                uninitialized_access: std::cell::Cell::new(None),
546            })
547        }
548    }
549
550    fn infer_phi(
551        &mut self,
552        phi_place_id: IdentifierId,
553        phi_operands: &indexmap::IndexMap<BlockId, Place>,
554    ) {
555        let mut values: HashSet<ValueId> = HashSet::new();
556        for (_, operand) in phi_operands {
557            if let Some(operand_values) = self.variables.get(&operand.identifier) {
558                for v in operand_values {
559                    values.insert(*v);
560                }
561            }
562            // If not found, it's a backedge that will be handled later by merge
563        }
564        if !values.is_empty() {
565            self.variables.insert(phi_place_id, values);
566        }
567    }
568}
569
570fn is_superset(a: &IndexSet<ValueReason>, b: &IndexSet<ValueReason>) -> bool {
571    b.iter().all(|x| a.contains(x))
572}
573
574#[derive(Debug, Clone, Copy)]
575enum MutateVariant {
576    Mutate,
577    MutateConditionally,
578    MutateTransitive,
579    MutateTransitiveConditionally,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583enum MutationResult {
584    None,
585    Mutate,
586    MutateFrozen,
587    MutateGlobal,
588    MutateRef,
589}
590
591// =============================================================================
592// Context
593// =============================================================================
594
595struct Context {
596    interned_effects: HashMap<String, AliasingEffect>,
597    instruction_signature_cache: HashMap<u32, InstructionSignature>,
598    catch_handlers: HashMap<BlockId, Place>,
599    is_function_expression: bool,
600    hoisted_context_declarations: HashMap<DeclarationId, Option<Place>>,
601    non_mutating_spreads: HashSet<IdentifierId>,
602    /// Cache of ValueIds keyed by effect hash, ensuring stable allocation-site identity
603    /// across fixpoint iterations. Mirrors TS `effectInstructionValueCache`.
604    effect_value_id_cache: HashMap<String, ValueId>,
605    /// Maps ValueId to FunctionId for function expressions, so we can look up
606    /// locally-declared functions when processing Apply effects.
607    function_values: HashMap<ValueId, FunctionId>,
608    /// Cache of function expression signatures, keyed by FunctionId
609    function_signature_cache: HashMap<FunctionId, AliasingSignature>,
610    /// Cache of temporary places created for aliasing signature config temporaries.
611    /// Keyed by (lvalue_identifier_id, temp_name) to ensure stable allocation
612    /// across fixpoint iterations.
613    aliasing_config_temp_cache: HashMap<(IdentifierId, String), Place>,
614}
615
616impl Context {
617    fn intern_effect(&mut self, effect: AliasingEffect) -> AliasingEffect {
618        let hash = hash_effect(&effect);
619        self.interned_effects.entry(hash).or_insert(effect).clone()
620    }
621
622    /// Get or create a stable ValueId for a given effect, ensuring fixpoint convergence.
623    fn get_or_create_value_id(&mut self, effect: &AliasingEffect) -> ValueId {
624        let hash = hash_effect(effect);
625        *self
626            .effect_value_id_cache
627            .entry(hash)
628            .or_insert_with(ValueId::new)
629    }
630}
631
632struct InstructionSignature {
633    effects: Vec<AliasingEffect>,
634}
635
636// =============================================================================
637// Helper: hash_effect
638// =============================================================================
639
640fn hash_effect(effect: &AliasingEffect) -> String {
641    match effect {
642        AliasingEffect::Apply {
643            receiver,
644            function,
645            mutates_function,
646            args,
647            into,
648            ..
649        } => {
650            let args_str: Vec<String> = args
651                .iter()
652                .map(|a| match a {
653                    PlaceOrSpreadOrHole::Hole => String::new(),
654                    PlaceOrSpreadOrHole::Place(p) => format!("{}", p.identifier.0),
655                    PlaceOrSpreadOrHole::Spread(s) => format!("...{}", s.place.identifier.0),
656                })
657                .collect();
658            format!(
659                "Apply:{}:{}:{}:{}:{}",
660                receiver.identifier.0,
661                function.identifier.0,
662                mutates_function,
663                args_str.join(","),
664                into.identifier.0
665            )
666        }
667        AliasingEffect::CreateFrom { from, into } => {
668            format!("CreateFrom:{}:{}", from.identifier.0, into.identifier.0)
669        }
670        AliasingEffect::ImmutableCapture { from, into } => format!(
671            "ImmutableCapture:{}:{}",
672            from.identifier.0, into.identifier.0
673        ),
674        AliasingEffect::Assign { from, into } => {
675            format!("Assign:{}:{}", from.identifier.0, into.identifier.0)
676        }
677        AliasingEffect::Alias { from, into } => {
678            format!("Alias:{}:{}", from.identifier.0, into.identifier.0)
679        }
680        AliasingEffect::Capture { from, into } => {
681            format!("Capture:{}:{}", from.identifier.0, into.identifier.0)
682        }
683        AliasingEffect::MaybeAlias { from, into } => {
684            format!("MaybeAlias:{}:{}", from.identifier.0, into.identifier.0)
685        }
686        AliasingEffect::Create {
687            into,
688            value,
689            reason,
690        } => format!("Create:{}:{:?}:{:?}", into.identifier.0, value, reason),
691        AliasingEffect::Freeze { value, reason } => {
692            format!("Freeze:{}:{:?}", value.identifier.0, reason)
693        }
694        AliasingEffect::Impure { place, .. } => format!("Impure:{}", place.identifier.0),
695        AliasingEffect::Render { place } => format!("Render:{}", place.identifier.0),
696        AliasingEffect::MutateFrozen { place, error } => format!(
697            "MutateFrozen:{}:{}:{:?}",
698            place.identifier.0, error.reason, error.description
699        ),
700        AliasingEffect::MutateGlobal { place, error } => format!(
701            "MutateGlobal:{}:{}:{:?}",
702            place.identifier.0, error.reason, error.description
703        ),
704        AliasingEffect::Mutate { value, .. } => format!("Mutate:{}", value.identifier.0),
705        AliasingEffect::MutateConditionally { value } => {
706            format!("MutateConditionally:{}", value.identifier.0)
707        }
708        AliasingEffect::MutateTransitive { value } => {
709            format!("MutateTransitive:{}", value.identifier.0)
710        }
711        AliasingEffect::MutateTransitiveConditionally { value } => {
712            format!("MutateTransitiveConditionally:{}", value.identifier.0)
713        }
714        AliasingEffect::CreateFunction {
715            into,
716            function_id,
717            captures,
718        } => {
719            let cap_str: Vec<String> = captures
720                .iter()
721                .map(|p| format!("{}", p.identifier.0))
722                .collect();
723            format!(
724                "CreateFunction:{}:{}:{}",
725                into.identifier.0,
726                function_id.0,
727                cap_str.join(",")
728            )
729        }
730    }
731}
732
733// =============================================================================
734// merge helpers
735// =============================================================================
736
737fn merge_abstract_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
738    let kind = merge_value_kinds(a.kind, b.kind);
739    if kind == a.kind && kind == b.kind && is_superset(&a.reason, &b.reason) {
740        return a.clone();
741    }
742    let mut reason = a.reason.clone();
743    for r in &b.reason {
744        reason.insert(*r);
745    }
746    AbstractValue { kind, reason }
747}
748
749fn merge_value_kinds(a: ValueKind, b: ValueKind) -> ValueKind {
750    if a == b {
751        return a;
752    }
753    if a == ValueKind::MaybeFrozen || b == ValueKind::MaybeFrozen {
754        return ValueKind::MaybeFrozen;
755    }
756    if a == ValueKind::Mutable || b == ValueKind::Mutable {
757        if a == ValueKind::Frozen || b == ValueKind::Frozen {
758            return ValueKind::MaybeFrozen;
759        } else if a == ValueKind::Context || b == ValueKind::Context {
760            return ValueKind::Context;
761        } else {
762            return ValueKind::Mutable;
763        }
764    }
765    if a == ValueKind::Context || b == ValueKind::Context {
766        if a == ValueKind::Frozen || b == ValueKind::Frozen {
767            return ValueKind::MaybeFrozen;
768        } else {
769            return ValueKind::Context;
770        }
771    }
772    if a == ValueKind::Frozen || b == ValueKind::Frozen {
773        return ValueKind::Frozen;
774    }
775    if a == ValueKind::Global || b == ValueKind::Global {
776        return ValueKind::Global;
777    }
778    ValueKind::Primitive
779}
780
781// =============================================================================
782// Pre-passes
783// =============================================================================
784
785fn find_hoisted_context_declarations(
786    func: &HirFunction,
787    env: &Environment,
788) -> HashMap<DeclarationId, Option<Place>> {
789    let mut hoisted: HashMap<DeclarationId, Option<Place>> = HashMap::new();
790
791    fn visit(
792        hoisted: &mut HashMap<DeclarationId, Option<Place>>,
793        place: &Place,
794        env: &Environment,
795    ) {
796        let decl_id = env.identifiers[place.identifier.0 as usize].declaration_id;
797        if hoisted.contains_key(&decl_id) && hoisted.get(&decl_id).unwrap().is_none() {
798            hoisted.insert(decl_id, Some(place.clone()));
799        }
800    }
801
802    for (_block_id, block) in &func.body.blocks {
803        for instr_id in &block.instructions {
804            let instr = &func.instructions[instr_id.0 as usize];
805            match &instr.value {
806                InstructionValue::DeclareContext { lvalue, .. } => {
807                    let kind = lvalue.kind;
808                    if kind == InstructionKind::HoistedConst
809                        || kind == InstructionKind::HoistedFunction
810                        || kind == InstructionKind::HoistedLet
811                    {
812                        let decl_id =
813                            env.identifiers[lvalue.place.identifier.0 as usize].declaration_id;
814                        hoisted.insert(decl_id, None);
815                    }
816                }
817                _ => {
818                    for operand in visitors::each_instruction_value_operand(&instr.value, env) {
819                        visit(&mut hoisted, &operand, env);
820                    }
821                }
822            }
823        }
824        for operand in visitors::each_terminal_operand(&block.terminal) {
825            visit(&mut hoisted, &operand, env);
826        }
827    }
828    hoisted
829}
830
831fn find_non_mutated_destructure_spreads(
832    func: &HirFunction,
833    env: &Environment,
834) -> HashSet<IdentifierId> {
835    let mut known_frozen: HashSet<IdentifierId> = HashSet::new();
836    if func.fn_type == ReactFunctionType::Component {
837        if let Some(param) = func.params.first() {
838            if let ParamPattern::Place(p) = param {
839                known_frozen.insert(p.identifier);
840            }
841        }
842    } else {
843        for param in &func.params {
844            if let ParamPattern::Place(p) = param {
845                known_frozen.insert(p.identifier);
846            }
847        }
848    }
849
850    let mut candidate_non_mutating_spreads: HashMap<IdentifierId, IdentifierId> = HashMap::new();
851    for (_block_id, block) in &func.body.blocks {
852        if !candidate_non_mutating_spreads.is_empty() {
853            for phi in &block.phis {
854                for (_, operand) in &phi.operands {
855                    if let Some(spread) = candidate_non_mutating_spreads
856                        .get(&operand.identifier)
857                        .copied()
858                    {
859                        candidate_non_mutating_spreads.remove(&spread);
860                    }
861                }
862            }
863        }
864        for instr_id in &block.instructions {
865            let instr = &func.instructions[instr_id.0 as usize];
866            let lvalue_id = instr.lvalue.identifier;
867            match &instr.value {
868                InstructionValue::Destructure { lvalue, value, .. } => {
869                    if !known_frozen.contains(&value.identifier) {
870                        continue;
871                    }
872                    if !(lvalue.kind == InstructionKind::Let
873                        || lvalue.kind == InstructionKind::Const)
874                    {
875                        continue;
876                    }
877                    match &lvalue.pattern {
878                        react_compiler_hir::Pattern::Object(obj_pat) => {
879                            for prop in &obj_pat.properties {
880                                if let react_compiler_hir::ObjectPropertyOrSpread::Spread(s) = prop
881                                {
882                                    candidate_non_mutating_spreads
883                                        .insert(s.place.identifier, s.place.identifier);
884                                }
885                            }
886                        }
887                        _ => continue,
888                    }
889                }
890                InstructionValue::LoadLocal { place, .. } => {
891                    if let Some(spread) = candidate_non_mutating_spreads
892                        .get(&place.identifier)
893                        .copied()
894                    {
895                        candidate_non_mutating_spreads.insert(lvalue_id, spread);
896                    }
897                }
898                InstructionValue::StoreLocal {
899                    lvalue: sl,
900                    value: sv,
901                    ..
902                } => {
903                    if let Some(spread) =
904                        candidate_non_mutating_spreads.get(&sv.identifier).copied()
905                    {
906                        candidate_non_mutating_spreads.insert(lvalue_id, spread);
907                        candidate_non_mutating_spreads.insert(sl.place.identifier, spread);
908                    }
909                }
910                InstructionValue::JsxFragment { .. } | InstructionValue::JsxExpression { .. } => {
911                    // Passing objects created with spread to jsx can't mutate them
912                }
913                InstructionValue::PropertyLoad { .. } => {
914                    // Properties must be frozen since the original value was frozen
915                }
916                InstructionValue::CallExpression { callee, .. }
917                | InstructionValue::MethodCall {
918                    property: callee, ..
919                } => {
920                    let callee_ty =
921                        &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];
922                    if get_hook_kind_for_type(env, callee_ty)
923                        .ok()
924                        .flatten()
925                        .is_some()
926                    {
927                        if !is_ref_or_ref_value_for_id(env, lvalue_id) {
928                            known_frozen.insert(lvalue_id);
929                        }
930                    } else if !candidate_non_mutating_spreads.is_empty() {
931                        for operand in visitors::each_instruction_value_operand(&instr.value, env) {
932                            if let Some(spread) = candidate_non_mutating_spreads
933                                .get(&operand.identifier)
934                                .copied()
935                            {
936                                candidate_non_mutating_spreads.remove(&spread);
937                            }
938                        }
939                    }
940                }
941                _ => {
942                    if !candidate_non_mutating_spreads.is_empty() {
943                        for operand in visitors::each_instruction_value_operand(&instr.value, env) {
944                            if let Some(spread) = candidate_non_mutating_spreads
945                                .get(&operand.identifier)
946                                .copied()
947                            {
948                                candidate_non_mutating_spreads.remove(&spread);
949                            }
950                        }
951                    }
952                }
953            }
954        }
955    }
956
957    let mut non_mutating: HashSet<IdentifierId> = HashSet::new();
958    for (key, value) in &candidate_non_mutating_spreads {
959        if key == value {
960            non_mutating.insert(*key);
961        }
962    }
963    non_mutating
964}
965
966// =============================================================================
967// inferParam
968// =============================================================================
969
970fn infer_param(param: &ParamPattern, state: &mut InferenceState, param_kind: &AbstractValue) {
971    let place = match param {
972        ParamPattern::Place(p) => p,
973        ParamPattern::Spread(s) => &s.place,
974    };
975    let value_id = ValueId::new();
976    state.initialize(value_id, param_kind.clone());
977    state.define(place.identifier, value_id);
978}
979
980// =============================================================================
981// inferBlock
982// =============================================================================
983
984fn infer_block(
985    context: &mut Context,
986    state: &mut InferenceState,
987    block_id: BlockId,
988    func: &mut HirFunction,
989    env: &mut Environment,
990) -> Result<(), CompilerDiagnostic> {
991    let block = &func.body.blocks[&block_id];
992
993    // Process phis
994    let phis: Vec<(IdentifierId, indexmap::IndexMap<BlockId, Place>)> = block
995        .phis
996        .iter()
997        .map(|phi| (phi.place.identifier, phi.operands.clone()))
998        .collect();
999    for (place_id, operands) in &phis {
1000        state.infer_phi(*place_id, operands);
1001    }
1002
1003    // Process instructions
1004    let instr_ids: Vec<u32> = block.instructions.iter().map(|id| id.0).collect();
1005    for instr_idx in &instr_ids {
1006        let instr_index = *instr_idx as usize;
1007
1008        // Compute signature if not cached
1009        if !context.instruction_signature_cache.contains_key(instr_idx) {
1010            let sig = compute_signature_for_instruction(
1011                context,
1012                env,
1013                &func.instructions[instr_index],
1014                func,
1015            );
1016            context.instruction_signature_cache.insert(*instr_idx, sig);
1017        }
1018
1019        // Apply signature
1020        let effects = apply_signature(
1021            context,
1022            state,
1023            *instr_idx,
1024            &func.instructions[instr_index],
1025            env,
1026            func,
1027        )?;
1028        func.instructions[instr_index].effects = effects;
1029    }
1030
1031    // Process terminal
1032    // Determine what terminal action to take without holding borrows
1033    enum TerminalAction {
1034        Try { handler: BlockId, binding: Place },
1035        MaybeThrow { handler_id: BlockId },
1036        Return,
1037        None,
1038    }
1039    let action = {
1040        let block = &func.body.blocks[&block_id];
1041        match &block.terminal {
1042            react_compiler_hir::Terminal::Try {
1043                handler,
1044                handler_binding: Some(binding),
1045                ..
1046            } => TerminalAction::Try {
1047                handler: *handler,
1048                binding: binding.clone(),
1049            },
1050            react_compiler_hir::Terminal::MaybeThrow {
1051                handler: Some(handler_id),
1052                ..
1053            } => TerminalAction::MaybeThrow {
1054                handler_id: *handler_id,
1055            },
1056            react_compiler_hir::Terminal::Return { .. } => TerminalAction::Return,
1057            _ => TerminalAction::None,
1058        }
1059    };
1060
1061    match action {
1062        TerminalAction::Try { handler, binding } => {
1063            context.catch_handlers.insert(handler, binding);
1064        }
1065        TerminalAction::MaybeThrow { handler_id } => {
1066            if let Some(handler_param) = context.catch_handlers.get(&handler_id).cloned() {
1067                if state.is_defined(handler_param.identifier) {
1068                    let mut terminal_effects: Vec<AliasingEffect> = Vec::new();
1069                    for instr_idx in &instr_ids {
1070                        let instr = &func.instructions[*instr_idx as usize];
1071                        match &instr.value {
1072                            InstructionValue::CallExpression { .. }
1073                            | InstructionValue::MethodCall { .. } => {
1074                                state.append_alias(
1075                                    handler_param.identifier,
1076                                    instr.lvalue.identifier,
1077                                );
1078                                let kind = state.kind(instr.lvalue.identifier).kind;
1079                                if kind == ValueKind::Mutable || kind == ValueKind::Context {
1080                                    terminal_effects.push(context.intern_effect(
1081                                        AliasingEffect::Alias {
1082                                            from: instr.lvalue.clone(),
1083                                            into: handler_param.clone(),
1084                                        },
1085                                    ));
1086                                }
1087                            }
1088                            _ => {}
1089                        }
1090                    }
1091                    let block_mut = func.body.blocks.get_mut(&block_id).unwrap();
1092                    if let react_compiler_hir::Terminal::MaybeThrow {
1093                        effects: ref mut term_effects,
1094                        ..
1095                    } = block_mut.terminal
1096                    {
1097                        *term_effects = if terminal_effects.is_empty() {
1098                            None
1099                        } else {
1100                            Some(terminal_effects)
1101                        };
1102                    }
1103                }
1104            }
1105        }
1106        TerminalAction::Return => {
1107            if !context.is_function_expression {
1108                let block_mut = func.body.blocks.get_mut(&block_id).unwrap();
1109                if let react_compiler_hir::Terminal::Return {
1110                    ref value,
1111                    effects: ref mut term_effects,
1112                    ..
1113                } = block_mut.terminal
1114                {
1115                    *term_effects = Some(vec![context.intern_effect(AliasingEffect::Freeze {
1116                        value: value.clone(),
1117                        reason: ValueReason::JsxCaptured,
1118                    })]);
1119                }
1120            }
1121        }
1122        TerminalAction::None => {}
1123    }
1124    Ok(())
1125}
1126
1127// =============================================================================
1128// applySignature
1129// =============================================================================
1130
1131fn apply_signature(
1132    context: &mut Context,
1133    state: &mut InferenceState,
1134    instr_idx: u32,
1135    instr: &react_compiler_hir::Instruction,
1136    env: &mut Environment,
1137    func: &HirFunction,
1138) -> Result<Option<Vec<AliasingEffect>>, CompilerDiagnostic> {
1139    let mut effects: Vec<AliasingEffect> = Vec::new();
1140
1141    // For function instructions, validate frozen mutation
1142    match &instr.value {
1143        InstructionValue::FunctionExpression { lowered_func, .. }
1144        | InstructionValue::ObjectMethod { lowered_func, .. } => {
1145            let inner_func = &env.functions[lowered_func.func.0 as usize];
1146            if let Some(ref aliasing_effects) = inner_func.aliasing_effects {
1147                let context_ids: HashSet<IdentifierId> =
1148                    inner_func.context.iter().map(|p| p.identifier).collect();
1149                for effect in aliasing_effects {
1150                    let (mutate_value, is_mutate) = match effect {
1151                        AliasingEffect::Mutate { value, .. } => (value, true),
1152                        AliasingEffect::MutateTransitive { value } => (value, false),
1153                        _ => continue,
1154                    };
1155                    if !context_ids.contains(&mutate_value.identifier) {
1156                        continue;
1157                    }
1158                    if !state.is_defined(mutate_value.identifier) {
1159                        continue;
1160                    }
1161                    let value_abstract = state.kind(mutate_value.identifier);
1162                    if value_abstract.kind == ValueKind::Frozen {
1163                        let reason_str = get_write_error_reason(&value_abstract);
1164                        let ident = &env.identifiers[mutate_value.identifier.0 as usize];
1165                        let variable = match &ident.name {
1166                            Some(react_compiler_hir::IdentifierName::Named(n)) => {
1167                                format!("`{}`", n)
1168                            }
1169                            _ => "value".to_string(),
1170                        };
1171                        let mut diagnostic = CompilerDiagnostic::new(
1172                            ErrorCategory::Immutability,
1173                            "This value cannot be modified",
1174                            Some(reason_str),
1175                        );
1176                        diagnostic.details.push(
1177                            react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
1178                                loc: mutate_value.loc,
1179                                message: Some(format!("{} cannot be modified", variable)),
1180                                identifier_name: None,
1181                            },
1182                        );
1183                        if is_mutate {
1184                            if let AliasingEffect::Mutate {
1185                                reason: Some(MutationReason::AssignCurrentProperty),
1186                                ..
1187                            } = effect
1188                            {
1189                                diagnostic.details.push(react_compiler_diagnostics::CompilerDiagnosticDetail::Hint {
1190                                    message: "Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in \"Ref\".".to_string()
1191                                });
1192                            }
1193                        }
1194                        effects.push(AliasingEffect::MutateFrozen {
1195                            place: mutate_value.clone(),
1196                            error: diagnostic,
1197                        });
1198                    }
1199                }
1200            }
1201        }
1202        _ => {}
1203    }
1204
1205    // Track which values we've already initialized
1206    let mut initialized: HashSet<IdentifierId> = HashSet::new();
1207
1208    // Get the cached signature effects
1209    let sig = context.instruction_signature_cache.get(&instr_idx).unwrap();
1210    let sig_effects: Vec<AliasingEffect> = sig.effects.clone();
1211
1212    for effect in &sig_effects {
1213        apply_effect(
1214            context,
1215            state,
1216            effect.clone(),
1217            &mut initialized,
1218            &mut effects,
1219            env,
1220            func,
1221        )?;
1222    }
1223
1224    // If lvalue is not yet defined, initialize it with a default value.
1225    // The TS version asserts this as an invariant, but the Rust port may have
1226    // edge cases where effects don't cover the lvalue (e.g. missing signature entries).
1227    if !state.is_defined(instr.lvalue.identifier) {
1228        let vid = ValueId(instr.lvalue.identifier.0 | 0x80000000);
1229        state.initialize(
1230            vid,
1231            AbstractValue {
1232                kind: ValueKind::Mutable,
1233                reason: hashset_of(ValueReason::Other),
1234            },
1235        );
1236        state.define(instr.lvalue.identifier, vid);
1237    }
1238
1239    Ok(if effects.is_empty() {
1240        None
1241    } else {
1242        Some(effects)
1243    })
1244}
1245
1246// =============================================================================
1247// Transitive freeze helper
1248// =============================================================================
1249
1250/// Recursively freeze through FunctionExpression captures. If `value_id`
1251/// corresponds to a FunctionExpression, freeze each of its context captures
1252/// and recurse into any that are themselves FunctionExpressions. This matches
1253/// the TS `freezeValue` → `freeze` → `freezeValue` recursion chain.
1254fn freeze_function_captures_transitive(
1255    state: &mut InferenceState,
1256    context: &Context,
1257    env: &Environment,
1258    value_id: ValueId,
1259    reason: ValueReason,
1260) {
1261    if let Some(&func_id) = context.function_values.get(&value_id) {
1262        let ctx_ids: Vec<IdentifierId> = env.functions[func_id.0 as usize]
1263            .context
1264            .iter()
1265            .map(|p| p.identifier)
1266            .collect();
1267        for ctx_id in ctx_ids {
1268            // Replicate InferenceState::freeze() logic inline —
1269            // we need to recurse with context/env which freeze() doesn't have.
1270            if !state.variables.contains_key(&ctx_id) {
1271                continue;
1272            }
1273            let kind = state.kind(ctx_id).kind;
1274            match kind {
1275                ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => {
1276                    let vids: Vec<ValueId> = state.values_for(ctx_id);
1277                    for vid in vids {
1278                        state.freeze_value(vid, reason);
1279                        // Recurse into nested function captures
1280                        freeze_function_captures_transitive(state, context, env, vid, reason);
1281                    }
1282                }
1283                ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => {
1284                    // Already frozen or immutable — no-op
1285                }
1286            }
1287        }
1288    }
1289}
1290
1291// =============================================================================
1292// applyEffect
1293// =============================================================================
1294
1295fn apply_effect(
1296    context: &mut Context,
1297    state: &mut InferenceState,
1298    effect: AliasingEffect,
1299    initialized: &mut HashSet<IdentifierId>,
1300    effects: &mut Vec<AliasingEffect>,
1301    env: &mut Environment,
1302    func: &HirFunction,
1303) -> Result<(), CompilerDiagnostic> {
1304    let effect = context.intern_effect(effect);
1305    match effect {
1306        AliasingEffect::Freeze { ref value, reason } => {
1307            let did_freeze = state.freeze(value.identifier, reason);
1308            if did_freeze {
1309                effects.push(effect.clone());
1310                // Transitively freeze FunctionExpression captures if enabled
1311                // (matches TS freezeValue which recurses into func.context)
1312                let enable_transitive = env.config.enable_preserve_existing_memoization_guarantees
1313                    || env.config.enable_transitively_freeze_function_expressions;
1314                if enable_transitive {
1315                    // Recursively freeze through function captures. The TS
1316                    // freezeValue() calls freeze() on each capture, which
1317                    // calls freezeValue() again — creating a transitive
1318                    // closure through arbitrarily nested function captures.
1319                    let value_ids: Vec<ValueId> = state.values_for(value.identifier);
1320                    for vid in &value_ids {
1321                        freeze_function_captures_transitive(state, context, env, *vid, reason);
1322                    }
1323                }
1324            }
1325        }
1326        AliasingEffect::Create {
1327            ref into,
1328            value: kind,
1329            reason,
1330        } => {
1331            assert!(
1332                !initialized.contains(&into.identifier),
1333                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
1334            );
1335            initialized.insert(into.identifier);
1336            let value_id = context.get_or_create_value_id(&effect);
1337            state.initialize(
1338                value_id,
1339                AbstractValue {
1340                    kind,
1341                    reason: hashset_of(reason),
1342                },
1343            );
1344            state.define(into.identifier, value_id);
1345            effects.push(effect.clone());
1346        }
1347        AliasingEffect::ImmutableCapture { ref from, .. } => {
1348            let kind = state.kind(from.identifier).kind;
1349            match kind {
1350                ValueKind::Global | ValueKind::Primitive => {
1351                    // no-op: don't track data flow for copy types
1352                }
1353                _ => {
1354                    effects.push(effect.clone());
1355                }
1356            }
1357        }
1358        AliasingEffect::CreateFrom { ref from, ref into } => {
1359            assert!(
1360                !initialized.contains(&into.identifier),
1361                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
1362            );
1363            initialized.insert(into.identifier);
1364            let from_value = state.kind(from.identifier);
1365            let value_id = context.get_or_create_value_id(&effect);
1366            state.initialize(
1367                value_id,
1368                AbstractValue {
1369                    kind: from_value.kind,
1370                    reason: from_value.reason.clone(),
1371                },
1372            );
1373            state.define(into.identifier, value_id);
1374            match from_value.kind {
1375                ValueKind::Primitive | ValueKind::Global => {
1376                    let first_reason = primary_reason(&from_value.reason);
1377                    effects.push(AliasingEffect::Create {
1378                        value: from_value.kind,
1379                        into: into.clone(),
1380                        reason: first_reason,
1381                    });
1382                }
1383                ValueKind::Frozen => {
1384                    let first_reason = primary_reason(&from_value.reason);
1385                    effects.push(AliasingEffect::Create {
1386                        value: from_value.kind,
1387                        into: into.clone(),
1388                        reason: first_reason,
1389                    });
1390                    apply_effect(
1391                        context,
1392                        state,
1393                        AliasingEffect::ImmutableCapture {
1394                            from: from.clone(),
1395                            into: into.clone(),
1396                        },
1397                        initialized,
1398                        effects,
1399                        env,
1400                        func,
1401                    )?;
1402                }
1403                _ => {
1404                    effects.push(effect.clone());
1405                }
1406            }
1407        }
1408        AliasingEffect::CreateFunction {
1409            ref captures,
1410            function_id,
1411            ref into,
1412        } => {
1413            assert!(
1414                !initialized.contains(&into.identifier),
1415                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
1416            );
1417            initialized.insert(into.identifier);
1418            effects.push(effect.clone());
1419
1420            // Check if function is mutable
1421            let has_captures = captures.iter().any(|capture| {
1422                if !state.is_defined(capture.identifier) {
1423                    return false;
1424                }
1425                let k = state.kind(capture.identifier).kind;
1426                k == ValueKind::Context || k == ValueKind::Mutable
1427            });
1428
1429            let inner_func = &env.functions[function_id.0 as usize];
1430            let has_tracked_side_effects = inner_func
1431                .aliasing_effects
1432                .as_ref()
1433                .map(|effs| {
1434                    effs.iter().any(|e| {
1435                        matches!(
1436                            e,
1437                            AliasingEffect::MutateFrozen { .. }
1438                                | AliasingEffect::MutateGlobal { .. }
1439                                | AliasingEffect::Impure { .. }
1440                        )
1441                    })
1442                })
1443                .unwrap_or(false);
1444
1445            let captures_ref = inner_func
1446                .context
1447                .iter()
1448                .any(|operand| is_ref_or_ref_value_for_id(env, operand.identifier));
1449
1450            let is_mutable = has_captures || has_tracked_side_effects || captures_ref;
1451
1452            // Update context variable effects
1453            let context_places: Vec<Place> = inner_func.context.clone();
1454            for operand in &context_places {
1455                if operand.effect != Effect::Capture {
1456                    continue;
1457                }
1458                if !state.is_defined(operand.identifier) {
1459                    continue;
1460                }
1461                let kind = state.kind(operand.identifier).kind;
1462                if kind == ValueKind::Primitive
1463                    || kind == ValueKind::Frozen
1464                    || kind == ValueKind::Global
1465                {
1466                    // Downgrade to Read - we need to mutate the inner function
1467                    let inner_func_mut = &mut env.functions[function_id.0 as usize];
1468                    for ctx in &mut inner_func_mut.context {
1469                        if ctx.identifier == operand.identifier && ctx.effect == Effect::Capture {
1470                            ctx.effect = Effect::Read;
1471                        }
1472                    }
1473                }
1474            }
1475
1476            let value_id = context.get_or_create_value_id(&effect);
1477            // Track this value as a function expression so Apply can look it up
1478            context.function_values.insert(value_id, function_id);
1479            state.initialize(
1480                value_id,
1481                AbstractValue {
1482                    kind: if is_mutable {
1483                        ValueKind::Mutable
1484                    } else {
1485                        ValueKind::Frozen
1486                    },
1487                    reason: IndexSet::new(),
1488                },
1489            );
1490            state.define(into.identifier, value_id);
1491
1492            for capture in captures {
1493                apply_effect(
1494                    context,
1495                    state,
1496                    AliasingEffect::Capture {
1497                        from: capture.clone(),
1498                        into: into.clone(),
1499                    },
1500                    initialized,
1501                    effects,
1502                    env,
1503                    func,
1504                )?;
1505            }
1506        }
1507        AliasingEffect::MaybeAlias { ref from, ref into }
1508        | AliasingEffect::Alias { ref from, ref into }
1509        | AliasingEffect::Capture { ref from, ref into } => {
1510            let is_capture = matches!(effect, AliasingEffect::Capture { .. });
1511            let is_maybe_alias = matches!(effect, AliasingEffect::MaybeAlias { .. });
1512            // For Alias, destination must already be initialized (Capture/MaybeAlias are exempt)
1513            assert!(
1514                is_capture || is_maybe_alias || initialized.contains(&into.identifier),
1515                "[InferMutationAliasingEffects] Expected destination to already be initialized within this instruction"
1516            );
1517
1518            // Check destination kind
1519            let into_kind = state.kind_with_loc(into.identifier, into.loc).kind;
1520            let destination_type = match into_kind {
1521                ValueKind::Context => Some("context"),
1522                ValueKind::Mutable | ValueKind::MaybeFrozen => Some("mutable"),
1523                _ => None,
1524            };
1525
1526            let from_kind = state.kind_with_loc(from.identifier, from.loc).kind;
1527            let source_type = match from_kind {
1528                ValueKind::Context => Some("context"),
1529                ValueKind::Global | ValueKind::Primitive => None,
1530                ValueKind::MaybeFrozen | ValueKind::Frozen => Some("frozen"),
1531                ValueKind::Mutable => Some("mutable"),
1532            };
1533
1534            if source_type == Some("frozen") {
1535                apply_effect(
1536                    context,
1537                    state,
1538                    AliasingEffect::ImmutableCapture {
1539                        from: from.clone(),
1540                        into: into.clone(),
1541                    },
1542                    initialized,
1543                    effects,
1544                    env,
1545                    func,
1546                )?;
1547            } else if (source_type == Some("mutable") && destination_type == Some("mutable"))
1548                || is_maybe_alias
1549            {
1550                effects.push(effect.clone());
1551            } else if (source_type == Some("context") && destination_type.is_some())
1552                || (source_type == Some("mutable") && destination_type == Some("context"))
1553            {
1554                apply_effect(
1555                    context,
1556                    state,
1557                    AliasingEffect::MaybeAlias {
1558                        from: from.clone(),
1559                        into: into.clone(),
1560                    },
1561                    initialized,
1562                    effects,
1563                    env,
1564                    func,
1565                )?;
1566            }
1567        }
1568        AliasingEffect::Assign { ref from, ref into } => {
1569            assert!(
1570                !initialized.contains(&into.identifier),
1571                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
1572            );
1573            initialized.insert(into.identifier);
1574            let from_value = state.kind_with_loc(from.identifier, from.loc);
1575            match from_value.kind {
1576                ValueKind::Frozen => {
1577                    apply_effect(
1578                        context,
1579                        state,
1580                        AliasingEffect::ImmutableCapture {
1581                            from: from.clone(),
1582                            into: into.clone(),
1583                        },
1584                        initialized,
1585                        effects,
1586                        env,
1587                        func,
1588                    )?;
1589                    let cache_key =
1590                        format!("Assign_frozen:{}:{}", from.identifier.0, into.identifier.0);
1591                    let value_id = *context
1592                        .effect_value_id_cache
1593                        .entry(cache_key)
1594                        .or_insert_with(ValueId::new);
1595                    state.initialize(
1596                        value_id,
1597                        AbstractValue {
1598                            kind: from_value.kind,
1599                            reason: from_value.reason.clone(),
1600                        },
1601                    );
1602                    state.define(into.identifier, value_id);
1603                }
1604                ValueKind::Global | ValueKind::Primitive => {
1605                    let cache_key =
1606                        format!("Assign_copy:{}:{}", from.identifier.0, into.identifier.0);
1607                    let value_id = *context
1608                        .effect_value_id_cache
1609                        .entry(cache_key)
1610                        .or_insert_with(ValueId::new);
1611                    state.initialize(
1612                        value_id,
1613                        AbstractValue {
1614                            kind: from_value.kind,
1615                            reason: from_value.reason.clone(),
1616                        },
1617                    );
1618                    state.define(into.identifier, value_id);
1619                }
1620                _ => {
1621                    state.assign(into.identifier, from.identifier);
1622                    effects.push(effect.clone());
1623                }
1624            }
1625        }
1626        AliasingEffect::Apply {
1627            ref receiver,
1628            ref function,
1629            mutates_function,
1630            ref args,
1631            ref into,
1632            ref signature,
1633            ref loc,
1634        } => {
1635            // First, check if the callee is a locally-declared function expression
1636            // whose aliasing effects we already know (TS lines 1016-1068)
1637            if state.is_defined(function.identifier) {
1638                let function_values = state.values_for(function.identifier);
1639                if function_values.len() == 1 {
1640                    let value_id = function_values[0];
1641                    if let Some(func_id) = context.function_values.get(&value_id).copied() {
1642                        let inner_func = &env.functions[func_id.0 as usize];
1643                        if inner_func.aliasing_effects.is_some() {
1644                            // Build or retrieve the signature from the function expression
1645                            if !context.function_signature_cache.contains_key(&func_id) {
1646                                let sig = build_signature_from_function_expression(env, func_id);
1647                                context.function_signature_cache.insert(func_id, sig);
1648                            }
1649                            let sig = context
1650                                .function_signature_cache
1651                                .get(&func_id)
1652                                .unwrap()
1653                                .clone();
1654                            let inner_func = &env.functions[func_id.0 as usize];
1655                            let context_places: Vec<Place> = inner_func.context.clone();
1656                            let sig_effects = compute_effects_for_aliasing_signature(
1657                                env,
1658                                &sig,
1659                                into,
1660                                receiver,
1661                                args,
1662                                &context_places,
1663                                loc.as_ref(),
1664                            )?;
1665                            if let Some(sig_effs) = sig_effects {
1666                                // Conditionally mutate the function itself first
1667                                apply_effect(
1668                                    context,
1669                                    state,
1670                                    AliasingEffect::MutateTransitiveConditionally {
1671                                        value: function.clone(),
1672                                    },
1673                                    initialized,
1674                                    effects,
1675                                    env,
1676                                    func,
1677                                )?;
1678                                for se in sig_effs {
1679                                    apply_effect(
1680                                        context,
1681                                        state,
1682                                        se,
1683                                        initialized,
1684                                        effects,
1685                                        env,
1686                                        func,
1687                                    )?;
1688                                }
1689                                return Ok(());
1690                            }
1691                        }
1692                    }
1693                }
1694            }
1695            if let Some(sig) = signature {
1696                // Check known_incompatible (TS line 2351-2370)
1697                if let Some(ref incompatible_msg) = sig.known_incompatible {
1698                    if env.enable_validations() {
1699                        let mut diagnostic = CompilerDiagnostic::new(
1700                            ErrorCategory::IncompatibleLibrary,
1701                            "Use of incompatible library",
1702                            Some(
1703                                "This API returns functions which cannot be memoized without leading to stale UI. \
1704                                 To prevent this, by default React Compiler will skip memoizing this component/hook. \
1705                                 However, you may see issues if values from this API are passed to other components/hooks that are \
1706                                 memoized".to_string(),
1707                            ),
1708                        );
1709                        diagnostic.details.push(CompilerDiagnosticDetail::Error {
1710                            loc: receiver.loc,
1711                            message: Some(incompatible_msg.clone()),
1712                            identifier_name: None,
1713                        });
1714                        // TS throws here, aborting compilation for this function
1715                        return Err(diagnostic);
1716                    }
1717                }
1718
1719                if let Some(ref aliasing) = sig.aliasing {
1720                    let sig_effects = compute_effects_for_aliasing_signature_config(
1721                        env,
1722                        aliasing,
1723                        into,
1724                        receiver,
1725                        args,
1726                        &[],
1727                        loc.as_ref(),
1728                        &mut context.aliasing_config_temp_cache,
1729                    )?;
1730                    if let Some(sig_effs) = sig_effects {
1731                        for se in sig_effs {
1732                            apply_effect(context, state, se, initialized, effects, env, func)?;
1733                        }
1734                        return Ok(());
1735                    }
1736                }
1737
1738                // Legacy signature
1739                let mut todo_errors: Vec<react_compiler_diagnostics::CompilerErrorDetail> =
1740                    Vec::new();
1741                let legacy_effects = compute_effects_for_legacy_signature(
1742                    state,
1743                    sig,
1744                    into,
1745                    receiver,
1746                    args,
1747                    loc.as_ref(),
1748                    env,
1749                    &context.function_values,
1750                    &mut todo_errors,
1751                );
1752                // Todo errors should short-circuit (TS throws throwTodo)
1753                if let Some(err_detail) = todo_errors.into_iter().next() {
1754                    return Err(CompilerDiagnostic::from_detail(err_detail));
1755                }
1756                for le in legacy_effects {
1757                    apply_effect(context, state, le, initialized, effects, env, func)?;
1758                }
1759            } else {
1760                // No signature: default behavior
1761                apply_effect(
1762                    context,
1763                    state,
1764                    AliasingEffect::Create {
1765                        into: into.clone(),
1766                        value: ValueKind::Mutable,
1767                        reason: ValueReason::Other,
1768                    },
1769                    initialized,
1770                    effects,
1771                    env,
1772                    func,
1773                )?;
1774
1775                let all_operands = build_apply_operands(receiver, function, args);
1776                for (operand, _is_function_operand, is_spread) in &all_operands {
1777                    // In TS, the check is `operand !== effect.function || effect.mutatesFunction`.
1778                    // This compares by reference identity, so for CallExpression/NewExpression
1779                    // where receiver === function, BOTH are skipped when !mutatesFunction.
1780                    if operand.identifier == function.identifier && !mutates_function {
1781                        // Don't mutate callee for non-mutating calls
1782                    } else {
1783                        apply_effect(
1784                            context,
1785                            state,
1786                            AliasingEffect::MutateTransitiveConditionally {
1787                                value: operand.clone(),
1788                            },
1789                            initialized,
1790                            effects,
1791                            env,
1792                            func,
1793                        )?;
1794                    }
1795
1796                    if *is_spread {
1797                        let ty = &env.types
1798                            [env.identifiers[operand.identifier.0 as usize].type_.0 as usize];
1799                        if let Some(mutate_iter) = conditionally_mutate_iterator(operand, ty) {
1800                            apply_effect(
1801                                context,
1802                                state,
1803                                mutate_iter,
1804                                initialized,
1805                                effects,
1806                                env,
1807                                func,
1808                            )?;
1809                        }
1810                    }
1811
1812                    apply_effect(
1813                        context,
1814                        state,
1815                        AliasingEffect::MaybeAlias {
1816                            from: operand.clone(),
1817                            into: into.clone(),
1818                        },
1819                        initialized,
1820                        effects,
1821                        env,
1822                        func,
1823                    )?;
1824
1825                    // In TS, `other === arg` compares the Place extracted from
1826                    // `otherArg` with the original `arg` element. For Identifier
1827                    // args, the extracted Place IS the arg, so this is a reference
1828                    // identity check. For Spread args, the extracted Place is
1829                    // `.place` which is never `===` the Spread wrapper object,
1830                    // so NO pairs are skipped when the outer arg is a Spread
1831                    // (including self-pairs, producing self-captures).
1832                    for (other, _other_is_func, _other_is_spread) in &all_operands {
1833                        if !is_spread && other.identifier == operand.identifier {
1834                            continue;
1835                        }
1836                        apply_effect(
1837                            context,
1838                            state,
1839                            AliasingEffect::Capture {
1840                                from: operand.clone(),
1841                                into: other.clone(),
1842                            },
1843                            initialized,
1844                            effects,
1845                            env,
1846                            func,
1847                        )?;
1848                    }
1849                }
1850            }
1851        }
1852        ref eff @ (AliasingEffect::Mutate { .. }
1853        | AliasingEffect::MutateConditionally { .. }
1854        | AliasingEffect::MutateTransitive { .. }
1855        | AliasingEffect::MutateTransitiveConditionally { .. }) => {
1856            let (mutate_place, variant) = match eff {
1857                AliasingEffect::Mutate { value, .. } => (value, MutateVariant::Mutate),
1858                AliasingEffect::MutateConditionally { value } => {
1859                    (value, MutateVariant::MutateConditionally)
1860                }
1861                AliasingEffect::MutateTransitive { value } => {
1862                    (value, MutateVariant::MutateTransitive)
1863                }
1864                AliasingEffect::MutateTransitiveConditionally { value } => {
1865                    (value, MutateVariant::MutateTransitiveConditionally)
1866                }
1867                _ => unreachable!(),
1868            };
1869            let value = mutate_place;
1870            let mutation_kind = state.mutate_with_loc(variant, value.identifier, env, value.loc);
1871            if mutation_kind == MutationResult::Mutate {
1872                effects.push(effect.clone());
1873            } else if mutation_kind == MutationResult::MutateRef {
1874                // no-op
1875            } else if mutation_kind != MutationResult::None
1876                && matches!(
1877                    variant,
1878                    MutateVariant::Mutate | MutateVariant::MutateTransitive
1879                )
1880            {
1881                let abstract_value = state.kind(value.identifier);
1882
1883                let ident = &env.identifiers[value.identifier.0 as usize];
1884                let decl_id = ident.declaration_id;
1885
1886                if mutation_kind == MutationResult::MutateFrozen
1887                    && context.hoisted_context_declarations.contains_key(&decl_id)
1888                {
1889                    let variable = match &ident.name {
1890                        Some(react_compiler_hir::IdentifierName::Named(n)) => {
1891                            Some(format!("`{}`", n))
1892                        }
1893                        _ => None,
1894                    };
1895                    let hoisted_access = context
1896                        .hoisted_context_declarations
1897                        .get(&decl_id)
1898                        .cloned()
1899                        .flatten();
1900                    let mut diagnostic = CompilerDiagnostic::new(
1901                        ErrorCategory::Immutability,
1902                        "Cannot access variable before it is declared",
1903                        Some(format!(
1904                            "{} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time",
1905                            variable.as_deref().unwrap_or("This variable")
1906                        )),
1907                    );
1908                    if let Some(ref access) = hoisted_access {
1909                        if access.loc != value.loc {
1910                            diagnostic.details.push(
1911                                react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
1912                                    loc: access.loc,
1913                                    message: Some(format!(
1914                                        "{} accessed before it is declared",
1915                                        variable.as_deref().unwrap_or("variable")
1916                                    )),
1917                                    identifier_name: None,
1918                                },
1919                            );
1920                        }
1921                    }
1922                    diagnostic.details.push(
1923                        react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
1924                            loc: value.loc,
1925                            message: Some(format!(
1926                                "{} is declared here",
1927                                variable.as_deref().unwrap_or("variable")
1928                            )),
1929                            identifier_name: None,
1930                        },
1931                    );
1932                    apply_effect(
1933                        context,
1934                        state,
1935                        AliasingEffect::MutateFrozen {
1936                            place: value.clone(),
1937                            error: diagnostic,
1938                        },
1939                        initialized,
1940                        effects,
1941                        env,
1942                        func,
1943                    )?;
1944                } else {
1945                    let reason_str = get_write_error_reason(&abstract_value);
1946                    let variable = match &ident.name {
1947                        Some(react_compiler_hir::IdentifierName::Named(n)) => format!("`{}`", n),
1948                        _ => "value".to_string(),
1949                    };
1950                    let mut diagnostic = CompilerDiagnostic::new(
1951                        ErrorCategory::Immutability,
1952                        "This value cannot be modified",
1953                        Some(reason_str),
1954                    );
1955                    diagnostic.details.push(
1956                        react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
1957                            loc: value.loc,
1958                            message: Some(format!("{} cannot be modified", variable)),
1959                            identifier_name: None,
1960                        },
1961                    );
1962
1963                    if let AliasingEffect::Mutate {
1964                        reason: Some(MutationReason::AssignCurrentProperty),
1965                        ..
1966                    } = &effect
1967                    {
1968                        diagnostic.details.push(react_compiler_diagnostics::CompilerDiagnosticDetail::Hint {
1969                            message: "Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in \"Ref\".".to_string(),
1970                        });
1971                    }
1972
1973                    let error_kind = if abstract_value.kind == ValueKind::Frozen {
1974                        AliasingEffect::MutateFrozen {
1975                            place: value.clone(),
1976                            error: diagnostic,
1977                        }
1978                    } else {
1979                        AliasingEffect::MutateGlobal {
1980                            place: value.clone(),
1981                            error: diagnostic,
1982                        }
1983                    };
1984                    apply_effect(context, state, error_kind, initialized, effects, env, func)?;
1985                }
1986            }
1987        }
1988        AliasingEffect::Impure { .. }
1989        | AliasingEffect::Render { .. }
1990        | AliasingEffect::MutateFrozen { .. }
1991        | AliasingEffect::MutateGlobal { .. } => {
1992            effects.push(effect.clone());
1993        }
1994    }
1995    Ok(())
1996}
1997
1998// =============================================================================
1999// computeSignatureForInstruction
2000// =============================================================================
2001
2002fn compute_signature_for_instruction(
2003    context: &mut Context,
2004    env: &Environment,
2005    instr: &react_compiler_hir::Instruction,
2006    _func: &HirFunction,
2007) -> InstructionSignature {
2008    let lvalue = &instr.lvalue;
2009    let value = &instr.value;
2010    let mut effects: Vec<AliasingEffect> = Vec::new();
2011
2012    match value {
2013        InstructionValue::ArrayExpression { elements, .. } => {
2014            effects.push(AliasingEffect::Create {
2015                into: lvalue.clone(),
2016                value: ValueKind::Mutable,
2017                reason: ValueReason::Other,
2018            });
2019            for element in elements {
2020                match element {
2021                    react_compiler_hir::ArrayElement::Place(p) => {
2022                        effects.push(AliasingEffect::Capture {
2023                            from: p.clone(),
2024                            into: lvalue.clone(),
2025                        });
2026                    }
2027                    react_compiler_hir::ArrayElement::Spread(s) => {
2028                        let ty = &env.types
2029                            [env.identifiers[s.place.identifier.0 as usize].type_.0 as usize];
2030                        if let Some(mutate_iter) = conditionally_mutate_iterator(&s.place, ty) {
2031                            effects.push(mutate_iter);
2032                        }
2033                        effects.push(AliasingEffect::Capture {
2034                            from: s.place.clone(),
2035                            into: lvalue.clone(),
2036                        });
2037                    }
2038                    react_compiler_hir::ArrayElement::Hole => {}
2039                }
2040            }
2041        }
2042        InstructionValue::ObjectExpression { properties, .. } => {
2043            effects.push(AliasingEffect::Create {
2044                into: lvalue.clone(),
2045                value: ValueKind::Mutable,
2046                reason: ValueReason::Other,
2047            });
2048            for property in properties {
2049                match property {
2050                    react_compiler_hir::ObjectPropertyOrSpread::Property(p) => {
2051                        effects.push(AliasingEffect::Capture {
2052                            from: p.place.clone(),
2053                            into: lvalue.clone(),
2054                        });
2055                    }
2056                    react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => {
2057                        effects.push(AliasingEffect::Capture {
2058                            from: s.place.clone(),
2059                            into: lvalue.clone(),
2060                        });
2061                    }
2062                }
2063            }
2064        }
2065        InstructionValue::Await {
2066            value: await_value, ..
2067        } => {
2068            effects.push(AliasingEffect::Create {
2069                into: lvalue.clone(),
2070                value: ValueKind::Mutable,
2071                reason: ValueReason::Other,
2072            });
2073            effects.push(AliasingEffect::MutateTransitiveConditionally {
2074                value: await_value.clone(),
2075            });
2076            effects.push(AliasingEffect::Capture {
2077                from: await_value.clone(),
2078                into: lvalue.clone(),
2079            });
2080        }
2081        InstructionValue::NewExpression { callee, args, loc } => {
2082            let sig = get_function_call_signature(env, callee.identifier)
2083                .ok()
2084                .flatten();
2085            effects.push(AliasingEffect::Apply {
2086                receiver: callee.clone(),
2087                function: callee.clone(),
2088                mutates_function: false,
2089                args: args.iter().map(place_or_spread_to_hole).collect(),
2090                into: lvalue.clone(),
2091                signature: sig,
2092                loc: *loc,
2093            });
2094        }
2095        InstructionValue::CallExpression { callee, args, loc } => {
2096            let sig = get_function_call_signature(env, callee.identifier)
2097                .ok()
2098                .flatten();
2099            effects.push(AliasingEffect::Apply {
2100                receiver: callee.clone(),
2101                function: callee.clone(),
2102                mutates_function: true,
2103                args: args.iter().map(place_or_spread_to_hole).collect(),
2104                into: lvalue.clone(),
2105                signature: sig,
2106                loc: *loc,
2107            });
2108        }
2109        InstructionValue::MethodCall {
2110            receiver,
2111            property,
2112            args,
2113            loc,
2114        } => {
2115            let sig = get_function_call_signature(env, property.identifier)
2116                .ok()
2117                .flatten();
2118            effects.push(AliasingEffect::Apply {
2119                receiver: receiver.clone(),
2120                function: property.clone(),
2121                mutates_function: false,
2122                args: args.iter().map(place_or_spread_to_hole).collect(),
2123                into: lvalue.clone(),
2124                signature: sig,
2125                loc: *loc,
2126            });
2127        }
2128        InstructionValue::PropertyDelete { object, .. }
2129        | InstructionValue::ComputedDelete { object, .. } => {
2130            effects.push(AliasingEffect::Create {
2131                into: lvalue.clone(),
2132                value: ValueKind::Primitive,
2133                reason: ValueReason::Other,
2134            });
2135            effects.push(AliasingEffect::Mutate {
2136                value: object.clone(),
2137                reason: None,
2138            });
2139        }
2140        InstructionValue::PropertyLoad { object, .. }
2141        | InstructionValue::ComputedLoad { object, .. } => {
2142            let ty = &env.types[env.identifiers[lvalue.identifier.0 as usize].type_.0 as usize];
2143            if react_compiler_hir::is_primitive_type(ty) {
2144                effects.push(AliasingEffect::Create {
2145                    into: lvalue.clone(),
2146                    value: ValueKind::Primitive,
2147                    reason: ValueReason::Other,
2148                });
2149            } else {
2150                effects.push(AliasingEffect::CreateFrom {
2151                    from: object.clone(),
2152                    into: lvalue.clone(),
2153                });
2154            }
2155        }
2156        InstructionValue::PropertyStore {
2157            object,
2158            property,
2159            value: store_value,
2160            ..
2161        } => {
2162            let mutation_reason: Option<MutationReason> = {
2163                let obj_ty =
2164                    &env.types[env.identifiers[object.identifier.0 as usize].type_.0 as usize];
2165                if let react_compiler_hir::PropertyLiteral::String(prop_name) = property {
2166                    if prop_name == "current" && matches!(obj_ty, Type::TypeVar { .. }) {
2167                        Some(MutationReason::AssignCurrentProperty)
2168                    } else {
2169                        None
2170                    }
2171                } else {
2172                    None
2173                }
2174            };
2175            effects.push(AliasingEffect::Mutate {
2176                value: object.clone(),
2177                reason: mutation_reason,
2178            });
2179            effects.push(AliasingEffect::Capture {
2180                from: store_value.clone(),
2181                into: object.clone(),
2182            });
2183            effects.push(AliasingEffect::Create {
2184                into: lvalue.clone(),
2185                value: ValueKind::Primitive,
2186                reason: ValueReason::Other,
2187            });
2188        }
2189        InstructionValue::ComputedStore {
2190            object,
2191            value: store_value,
2192            ..
2193        } => {
2194            effects.push(AliasingEffect::Mutate {
2195                value: object.clone(),
2196                reason: None,
2197            });
2198            effects.push(AliasingEffect::Capture {
2199                from: store_value.clone(),
2200                into: object.clone(),
2201            });
2202            effects.push(AliasingEffect::Create {
2203                into: lvalue.clone(),
2204                value: ValueKind::Primitive,
2205                reason: ValueReason::Other,
2206            });
2207        }
2208        InstructionValue::FunctionExpression { lowered_func, .. }
2209        | InstructionValue::ObjectMethod { lowered_func, .. } => {
2210            let inner_func = &env.functions[lowered_func.func.0 as usize];
2211            let captures: Vec<Place> = inner_func
2212                .context
2213                .iter()
2214                .filter(|operand| operand.effect == Effect::Capture)
2215                .cloned()
2216                .collect();
2217            effects.push(AliasingEffect::CreateFunction {
2218                into: lvalue.clone(),
2219                function_id: lowered_func.func,
2220                captures,
2221            });
2222        }
2223        InstructionValue::GetIterator { collection, .. } => {
2224            effects.push(AliasingEffect::Create {
2225                into: lvalue.clone(),
2226                value: ValueKind::Mutable,
2227                reason: ValueReason::Other,
2228            });
2229            let ty = &env.types[env.identifiers[collection.identifier.0 as usize].type_.0 as usize];
2230            if is_builtin_collection_type(ty) {
2231                effects.push(AliasingEffect::Capture {
2232                    from: collection.clone(),
2233                    into: lvalue.clone(),
2234                });
2235            } else {
2236                effects.push(AliasingEffect::Alias {
2237                    from: collection.clone(),
2238                    into: lvalue.clone(),
2239                });
2240                effects.push(AliasingEffect::MutateTransitiveConditionally {
2241                    value: collection.clone(),
2242                });
2243            }
2244        }
2245        InstructionValue::IteratorNext {
2246            iterator,
2247            collection,
2248            ..
2249        } => {
2250            effects.push(AliasingEffect::MutateConditionally {
2251                value: iterator.clone(),
2252            });
2253            effects.push(AliasingEffect::CreateFrom {
2254                from: collection.clone(),
2255                into: lvalue.clone(),
2256            });
2257        }
2258        InstructionValue::NextPropertyOf { .. } => {
2259            effects.push(AliasingEffect::Create {
2260                into: lvalue.clone(),
2261                value: ValueKind::Primitive,
2262                reason: ValueReason::Other,
2263            });
2264        }
2265        InstructionValue::JsxExpression {
2266            tag,
2267            props,
2268            children,
2269            ..
2270        } => {
2271            effects.push(AliasingEffect::Create {
2272                into: lvalue.clone(),
2273                value: ValueKind::Frozen,
2274                reason: ValueReason::JsxCaptured,
2275            });
2276            for operand in visitors::each_instruction_value_operand(value, env) {
2277                effects.push(AliasingEffect::Freeze {
2278                    value: operand.clone(),
2279                    reason: ValueReason::JsxCaptured,
2280                });
2281                effects.push(AliasingEffect::Capture {
2282                    from: operand.clone(),
2283                    into: lvalue.clone(),
2284                });
2285            }
2286            if let JsxTag::Place(tag_place) = tag {
2287                effects.push(AliasingEffect::Render {
2288                    place: tag_place.clone(),
2289                });
2290            }
2291            if let Some(ch) = children {
2292                for child in ch {
2293                    effects.push(AliasingEffect::Render {
2294                        place: child.clone(),
2295                    });
2296                }
2297            }
2298            for prop in props {
2299                if let react_compiler_hir::JsxAttribute::Attribute {
2300                    place: prop_place, ..
2301                } = prop
2302                {
2303                    let prop_ty = &env.types
2304                        [env.identifiers[prop_place.identifier.0 as usize].type_.0 as usize];
2305                    if let Type::Function { return_type, .. } = prop_ty {
2306                        if react_compiler_hir::is_jsx_type(return_type)
2307                            || is_phi_with_jsx(return_type)
2308                        {
2309                            effects.push(AliasingEffect::Render {
2310                                place: prop_place.clone(),
2311                            });
2312                        }
2313                    }
2314                }
2315            }
2316        }
2317        InstructionValue::JsxFragment { children: _, .. } => {
2318            effects.push(AliasingEffect::Create {
2319                into: lvalue.clone(),
2320                value: ValueKind::Frozen,
2321                reason: ValueReason::JsxCaptured,
2322            });
2323            for operand in visitors::each_instruction_value_operand(value, env) {
2324                effects.push(AliasingEffect::Freeze {
2325                    value: operand.clone(),
2326                    reason: ValueReason::JsxCaptured,
2327                });
2328                effects.push(AliasingEffect::Capture {
2329                    from: operand.clone(),
2330                    into: lvalue.clone(),
2331                });
2332            }
2333        }
2334        InstructionValue::DeclareLocal { lvalue: dl, .. } => {
2335            effects.push(AliasingEffect::Create {
2336                into: dl.place.clone(),
2337                value: ValueKind::Primitive,
2338                reason: ValueReason::Other,
2339            });
2340            effects.push(AliasingEffect::Create {
2341                into: lvalue.clone(),
2342                value: ValueKind::Primitive,
2343                reason: ValueReason::Other,
2344            });
2345        }
2346        InstructionValue::Destructure {
2347            lvalue: dl,
2348            value: dest_value,
2349            ..
2350        } => {
2351            for pat_item in each_pattern_items(&dl.pattern) {
2352                match pat_item {
2353                    PatternItem::Place(place) => {
2354                        let ty = &env.types
2355                            [env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2356                        if react_compiler_hir::is_primitive_type(ty) {
2357                            effects.push(AliasingEffect::Create {
2358                                into: place.clone(),
2359                                value: ValueKind::Primitive,
2360                                reason: ValueReason::Other,
2361                            });
2362                        } else {
2363                            effects.push(AliasingEffect::CreateFrom {
2364                                from: dest_value.clone(),
2365                                into: place.clone(),
2366                            });
2367                        }
2368                    }
2369                    PatternItem::Spread(place) => {
2370                        let value_kind = if context.non_mutating_spreads.contains(&place.identifier)
2371                        {
2372                            ValueKind::Frozen
2373                        } else {
2374                            ValueKind::Mutable
2375                        };
2376                        effects.push(AliasingEffect::Create {
2377                            into: place.clone(),
2378                            reason: ValueReason::Other,
2379                            value: value_kind,
2380                        });
2381                        effects.push(AliasingEffect::Capture {
2382                            from: dest_value.clone(),
2383                            into: place.clone(),
2384                        });
2385                    }
2386                }
2387            }
2388            effects.push(AliasingEffect::Assign {
2389                from: dest_value.clone(),
2390                into: lvalue.clone(),
2391            });
2392        }
2393        InstructionValue::LoadContext { place, .. } => {
2394            effects.push(AliasingEffect::CreateFrom {
2395                from: place.clone(),
2396                into: lvalue.clone(),
2397            });
2398        }
2399        InstructionValue::DeclareContext { lvalue: dcl, .. } => {
2400            let decl_id = env.identifiers[dcl.place.identifier.0 as usize].declaration_id;
2401            let kind = dcl.kind;
2402            if !context.hoisted_context_declarations.contains_key(&decl_id)
2403                || kind == InstructionKind::HoistedConst
2404                || kind == InstructionKind::HoistedFunction
2405                || kind == InstructionKind::HoistedLet
2406            {
2407                effects.push(AliasingEffect::Create {
2408                    into: dcl.place.clone(),
2409                    value: ValueKind::Mutable,
2410                    reason: ValueReason::Other,
2411                });
2412            } else {
2413                effects.push(AliasingEffect::Mutate {
2414                    value: dcl.place.clone(),
2415                    reason: None,
2416                });
2417            }
2418            effects.push(AliasingEffect::Create {
2419                into: lvalue.clone(),
2420                value: ValueKind::Primitive,
2421                reason: ValueReason::Other,
2422            });
2423        }
2424        InstructionValue::StoreContext {
2425            lvalue: scl,
2426            value: sc_value,
2427            ..
2428        } => {
2429            let decl_id = env.identifiers[scl.place.identifier.0 as usize].declaration_id;
2430            if scl.kind == InstructionKind::Reassign
2431                || context.hoisted_context_declarations.contains_key(&decl_id)
2432            {
2433                effects.push(AliasingEffect::Mutate {
2434                    value: scl.place.clone(),
2435                    reason: None,
2436                });
2437            } else {
2438                effects.push(AliasingEffect::Create {
2439                    into: scl.place.clone(),
2440                    value: ValueKind::Mutable,
2441                    reason: ValueReason::Other,
2442                });
2443            }
2444            effects.push(AliasingEffect::Capture {
2445                from: sc_value.clone(),
2446                into: scl.place.clone(),
2447            });
2448            effects.push(AliasingEffect::Assign {
2449                from: sc_value.clone(),
2450                into: lvalue.clone(),
2451            });
2452        }
2453        InstructionValue::LoadLocal { place, .. } => {
2454            effects.push(AliasingEffect::Assign {
2455                from: place.clone(),
2456                into: lvalue.clone(),
2457            });
2458        }
2459        InstructionValue::StoreLocal {
2460            lvalue: sl,
2461            value: sl_value,
2462            ..
2463        } => {
2464            effects.push(AliasingEffect::Assign {
2465                from: sl_value.clone(),
2466                into: sl.place.clone(),
2467            });
2468            effects.push(AliasingEffect::Assign {
2469                from: sl_value.clone(),
2470                into: lvalue.clone(),
2471            });
2472        }
2473        InstructionValue::PostfixUpdate {
2474            lvalue: pf_lvalue, ..
2475        }
2476        | InstructionValue::PrefixUpdate {
2477            lvalue: pf_lvalue, ..
2478        } => {
2479            effects.push(AliasingEffect::Create {
2480                into: lvalue.clone(),
2481                value: ValueKind::Primitive,
2482                reason: ValueReason::Other,
2483            });
2484            effects.push(AliasingEffect::Create {
2485                into: pf_lvalue.clone(),
2486                value: ValueKind::Primitive,
2487                reason: ValueReason::Other,
2488            });
2489        }
2490        InstructionValue::StoreGlobal {
2491            name,
2492            value: sg_value,
2493            loc: _,
2494            ..
2495        } => {
2496            let variable = format!("`{}`", name);
2497            let mut diagnostic = CompilerDiagnostic::new(
2498                ErrorCategory::Globals,
2499                "Cannot reassign variables declared outside of the component/hook",
2500                Some(format!(
2501                    "Variable {} is declared outside of the component/hook. Reassigning this value during render is a form of side effect, which can cause unpredictable behavior depending on when the component happens to re-render. If this variable is used in rendering, use useState instead. Otherwise, consider updating it in an effect. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)",
2502                    variable
2503                )),
2504            );
2505            diagnostic.details.push(
2506                react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
2507                    loc: instr.loc,
2508                    message: Some(format!("{} cannot be reassigned", variable)),
2509                    identifier_name: None,
2510                },
2511            );
2512            effects.push(AliasingEffect::MutateGlobal {
2513                place: sg_value.clone(),
2514                error: diagnostic,
2515            });
2516            effects.push(AliasingEffect::Assign {
2517                from: sg_value.clone(),
2518                into: lvalue.clone(),
2519            });
2520        }
2521        InstructionValue::TypeCastExpression {
2522            value: tc_value, ..
2523        } => {
2524            effects.push(AliasingEffect::Assign {
2525                from: tc_value.clone(),
2526                into: lvalue.clone(),
2527            });
2528        }
2529        InstructionValue::LoadGlobal { .. } => {
2530            effects.push(AliasingEffect::Create {
2531                into: lvalue.clone(),
2532                value: ValueKind::Global,
2533                reason: ValueReason::Global,
2534            });
2535        }
2536        InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => {
2537            if env.config.enable_preserve_existing_memoization_guarantees {
2538                for operand in visitors::each_instruction_value_operand(value, env) {
2539                    effects.push(AliasingEffect::Freeze {
2540                        value: operand.clone(),
2541                        reason: ValueReason::HookCaptured,
2542                    });
2543                }
2544            }
2545            effects.push(AliasingEffect::Create {
2546                into: lvalue.clone(),
2547                value: ValueKind::Primitive,
2548                reason: ValueReason::Other,
2549            });
2550        }
2551        // All primitive-creating instructions
2552        InstructionValue::TaggedTemplateExpression { .. }
2553        | InstructionValue::BinaryExpression { .. }
2554        | InstructionValue::Debugger { .. }
2555        | InstructionValue::JSXText { .. }
2556        | InstructionValue::MetaProperty { .. }
2557        | InstructionValue::Primitive { .. }
2558        | InstructionValue::RegExpLiteral { .. }
2559        | InstructionValue::TemplateLiteral { .. }
2560        | InstructionValue::UnaryExpression { .. }
2561        | InstructionValue::UnsupportedNode { .. } => {
2562            effects.push(AliasingEffect::Create {
2563                into: lvalue.clone(),
2564                value: ValueKind::Primitive,
2565                reason: ValueReason::Other,
2566            });
2567        }
2568    }
2569
2570    InstructionSignature { effects }
2571}
2572
2573// =============================================================================
2574// Legacy signature support
2575// =============================================================================
2576
2577fn compute_effects_for_legacy_signature(
2578    state: &InferenceState,
2579    signature: &FunctionSignature,
2580    lvalue: &Place,
2581    receiver: &Place,
2582    args: &[PlaceOrSpreadOrHole],
2583    _loc: Option<&SourceLocation>,
2584    env: &Environment,
2585    function_values: &HashMap<ValueId, FunctionId>,
2586    todo_errors: &mut Vec<react_compiler_diagnostics::CompilerErrorDetail>,
2587) -> Vec<AliasingEffect> {
2588    let return_value_reason = signature.return_value_reason.unwrap_or(ValueReason::Other);
2589    let mut effects: Vec<AliasingEffect> = Vec::new();
2590
2591    effects.push(AliasingEffect::Create {
2592        into: lvalue.clone(),
2593        value: signature.return_value_kind,
2594        reason: return_value_reason,
2595    });
2596
2597    if signature.impure && env.config.validate_no_impure_functions_in_render {
2598        let mut diagnostic = CompilerDiagnostic::new(
2599            ErrorCategory::Purity,
2600            "Cannot call impure function during render",
2601            Some(format!(
2602                "{}Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)",
2603                if let Some(ref name) = signature.canonical_name {
2604                    format!("`{}` is an impure function. ", name)
2605                } else {
2606                    String::new()
2607                }
2608            )),
2609        );
2610        diagnostic.details.push(
2611            react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
2612                loc: _loc.copied(),
2613                message: Some("Cannot call impure function".to_string()),
2614                identifier_name: None,
2615            },
2616        );
2617        effects.push(AliasingEffect::Impure {
2618            place: receiver.clone(),
2619            error: diagnostic,
2620        });
2621    }
2622
2623    // TODO: check signature.known_incompatible and throw (TS line 2351-2370)
2624    // This requires threading Result through apply_effect/apply_signature.
2625
2626    // If the function is mutable only if operands are mutable, and all
2627    // arguments are immutable/non-mutating, short-circuit with simple aliasing.
2628    if signature.mutable_only_if_operands_are_mutable
2629        && are_arguments_immutable_and_non_mutating(state, args, env, function_values)
2630    {
2631        effects.push(AliasingEffect::Alias {
2632            from: receiver.clone(),
2633            into: lvalue.clone(),
2634        });
2635        for arg in args {
2636            match arg {
2637                PlaceOrSpreadOrHole::Hole => continue,
2638                PlaceOrSpreadOrHole::Place(place)
2639                | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2640                    effects.push(AliasingEffect::ImmutableCapture {
2641                        from: place.clone(),
2642                        into: lvalue.clone(),
2643                    });
2644                }
2645            }
2646        }
2647        return effects;
2648    }
2649
2650    let mut stores: Vec<Place> = Vec::new();
2651    let mut captures: Vec<Place> = Vec::new();
2652
2653    let mut visit = |place: &Place, effect: Effect, effects: &mut Vec<AliasingEffect>| match effect
2654    {
2655        Effect::Store => {
2656            effects.push(AliasingEffect::Mutate {
2657                value: place.clone(),
2658                reason: None,
2659            });
2660            stores.push(place.clone());
2661        }
2662        Effect::Capture => {
2663            captures.push(place.clone());
2664        }
2665        Effect::ConditionallyMutate => {
2666            effects.push(AliasingEffect::MutateTransitiveConditionally {
2667                value: place.clone(),
2668            });
2669        }
2670        Effect::ConditionallyMutateIterator => {
2671            let ty = &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2672            if let Some(mutate_iter) = conditionally_mutate_iterator(place, ty) {
2673                effects.push(mutate_iter);
2674            }
2675            effects.push(AliasingEffect::Capture {
2676                from: place.clone(),
2677                into: lvalue.clone(),
2678            });
2679        }
2680        Effect::Freeze => {
2681            effects.push(AliasingEffect::Freeze {
2682                value: place.clone(),
2683                reason: return_value_reason,
2684            });
2685        }
2686        Effect::Mutate => {
2687            effects.push(AliasingEffect::MutateTransitive {
2688                value: place.clone(),
2689            });
2690        }
2691        Effect::Read => {
2692            effects.push(AliasingEffect::ImmutableCapture {
2693                from: place.clone(),
2694                into: lvalue.clone(),
2695            });
2696        }
2697        _ => {}
2698    };
2699
2700    if signature.callee_effect != Effect::Capture {
2701        effects.push(AliasingEffect::Alias {
2702            from: receiver.clone(),
2703            into: lvalue.clone(),
2704        });
2705    }
2706
2707    visit(receiver, signature.callee_effect, &mut effects);
2708    for (i, arg) in args.iter().enumerate() {
2709        match arg {
2710            PlaceOrSpreadOrHole::Hole => continue,
2711            PlaceOrSpreadOrHole::Place(place)
2712            | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2713                let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_));
2714                let sig_effect = if !is_spread && i < signature.positional_params.len() {
2715                    signature.positional_params[i]
2716                } else {
2717                    signature.rest_param.unwrap_or(Effect::ConditionallyMutate)
2718                };
2719                let (effect, err_detail) = get_argument_effect(sig_effect, is_spread, place.loc);
2720                if let Some(d) = err_detail {
2721                    todo_errors.push(d);
2722                }
2723                visit(place, effect, &mut effects);
2724            }
2725        }
2726    }
2727
2728    if !captures.is_empty() {
2729        if stores.is_empty() {
2730            for capture in &captures {
2731                effects.push(AliasingEffect::Alias {
2732                    from: capture.clone(),
2733                    into: lvalue.clone(),
2734                });
2735            }
2736        } else {
2737            for capture in &captures {
2738                for store in &stores {
2739                    effects.push(AliasingEffect::Capture {
2740                        from: capture.clone(),
2741                        into: store.clone(),
2742                    });
2743                }
2744            }
2745        }
2746    }
2747
2748    effects
2749}
2750
2751fn get_argument_effect(
2752    sig_effect: Effect,
2753    is_spread: bool,
2754    spread_loc: Option<SourceLocation>,
2755) -> (
2756    Effect,
2757    Option<react_compiler_diagnostics::CompilerErrorDetail>,
2758) {
2759    if !is_spread {
2760        (sig_effect, None)
2761    } else if sig_effect == Effect::Mutate || sig_effect == Effect::ConditionallyMutate {
2762        (sig_effect, None)
2763    } else {
2764        // Spread with Freeze effect is unsupported for hook arguments
2765        // (matches TS CompilerError.throwTodo)
2766        let detail = if sig_effect == Effect::Freeze {
2767            Some(react_compiler_diagnostics::CompilerErrorDetail {
2768                reason: "Support spread syntax for hook arguments".to_string(),
2769                description: None,
2770                category: ErrorCategory::Todo,
2771                loc: spread_loc,
2772                suggestions: None,
2773            })
2774        } else {
2775            None
2776        };
2777        (Effect::ConditionallyMutateIterator, detail)
2778    }
2779}
2780
2781/// Returns true if all of the arguments are both non-mutable (immutable or frozen)
2782/// _and_ are not functions which might mutate their arguments.
2783///
2784/// Corresponds to TS `areArgumentsImmutableAndNonMutating`.
2785fn are_arguments_immutable_and_non_mutating(
2786    state: &InferenceState,
2787    args: &[PlaceOrSpreadOrHole],
2788    env: &Environment,
2789    function_values: &HashMap<ValueId, FunctionId>,
2790) -> bool {
2791    for arg in args {
2792        match arg {
2793            PlaceOrSpreadOrHole::Hole => continue,
2794            PlaceOrSpreadOrHole::Place(place)
2795            | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2796                // Check if it's a function type with a known signature
2797                let is_place = matches!(arg, PlaceOrSpreadOrHole::Place(_));
2798                if is_place {
2799                    let ty =
2800                        &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2801                    if let Type::Function { .. } = ty {
2802                        let fn_shape = env.get_function_signature(ty).ok().flatten();
2803                        if let Some(fn_sig) = fn_shape {
2804                            let has_mutable_param = fn_sig
2805                                .positional_params
2806                                .iter()
2807                                .any(|e| is_known_mutable_effect(*e));
2808                            let has_mutable_rest = fn_sig
2809                                .rest_param
2810                                .map_or(false, |e| is_known_mutable_effect(e));
2811                            return !has_mutable_param && !has_mutable_rest;
2812                        }
2813                    }
2814                }
2815
2816                let kind = state.kind(place.identifier);
2817                match kind.kind {
2818                    ValueKind::Primitive | ValueKind::Frozen => {
2819                        // Immutable values are ok, continue checking
2820                    }
2821                    _ => {
2822                        return false;
2823                    }
2824                }
2825
2826                // Check if any value for this place is a function expression
2827                // that mutates its parameters (TS lines 2545-2557)
2828                let value_ids = state.values_for(place.identifier);
2829                for vid in &value_ids {
2830                    if let Some(&func_id) = function_values.get(vid) {
2831                        let inner_func = &env.functions[func_id.0 as usize];
2832                        let mutates_params = inner_func.params.iter().any(|param| {
2833                            let param_id = match param {
2834                                ParamPattern::Place(p) => p.identifier,
2835                                ParamPattern::Spread(s) => s.place.identifier,
2836                            };
2837                            let ident = &env.identifiers[param_id.0 as usize];
2838                            ident.mutable_range.end.0 > ident.mutable_range.start.0 + 1
2839                        });
2840                        if mutates_params {
2841                            return false;
2842                        }
2843                    }
2844                }
2845            }
2846        }
2847    }
2848    true
2849}
2850
2851fn is_known_mutable_effect(effect: Effect) -> bool {
2852    matches!(
2853        effect,
2854        Effect::Store
2855            | Effect::Mutate
2856            | Effect::ConditionallyMutate
2857            | Effect::ConditionallyMutateIterator
2858    )
2859}
2860
2861// =============================================================================
2862// Aliasing signature config support (new-style signatures)
2863// =============================================================================
2864
2865fn compute_effects_for_aliasing_signature_config(
2866    env: &mut Environment,
2867    config: &react_compiler_hir::type_config::AliasingSignatureConfig,
2868    lvalue: &Place,
2869    receiver: &Place,
2870    args: &[PlaceOrSpreadOrHole],
2871    context: &[Place],
2872    _loc: Option<&SourceLocation>,
2873    temp_cache: &mut HashMap<(IdentifierId, String), Place>,
2874) -> Result<Option<Vec<AliasingEffect>>, CompilerDiagnostic> {
2875    // Build substitutions from config strings to places
2876    let mut substitutions: HashMap<String, Vec<Place>> = HashMap::new();
2877    substitutions.insert(config.receiver.clone(), vec![receiver.clone()]);
2878    substitutions.insert(config.returns.clone(), vec![lvalue.clone()]);
2879
2880    let mut mutable_spreads: HashSet<IdentifierId> = HashSet::new();
2881
2882    for (i, arg) in args.iter().enumerate() {
2883        match arg {
2884            PlaceOrSpreadOrHole::Hole => continue,
2885            PlaceOrSpreadOrHole::Place(place)
2886            | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2887                if i < config.params.len() && !matches!(arg, PlaceOrSpreadOrHole::Spread(_)) {
2888                    substitutions.insert(config.params[i].clone(), vec![place.clone()]);
2889                } else if let Some(ref rest) = config.rest {
2890                    substitutions
2891                        .entry(rest.clone())
2892                        .or_default()
2893                        .push(place.clone());
2894                } else {
2895                    return Ok(None);
2896                }
2897
2898                if matches!(arg, PlaceOrSpreadOrHole::Spread(_)) {
2899                    let ty =
2900                        &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2901                    let mutate_iterator = conditionally_mutate_iterator(place, ty);
2902                    if mutate_iterator.is_some() {
2903                        mutable_spreads.insert(place.identifier);
2904                    }
2905                }
2906            }
2907        }
2908    }
2909
2910    for operand in context {
2911        let ident = &env.identifiers[operand.identifier.0 as usize];
2912        if let Some(ref name) = ident.name {
2913            substitutions.insert(format!("@{}", name.value()), vec![operand.clone()]);
2914        }
2915    }
2916
2917    // Create temporaries (cached by lvalue + temp_name to be stable across fixpoint iterations)
2918    for temp_name in &config.temporaries {
2919        let cache_key = (lvalue.identifier, temp_name.clone());
2920        let temp_place = temp_cache
2921            .entry(cache_key)
2922            .or_insert_with(|| create_temp_place(env, receiver.loc))
2923            .clone();
2924        substitutions.insert(temp_name.clone(), vec![temp_place]);
2925    }
2926
2927    let mut effects: Vec<AliasingEffect> = Vec::new();
2928
2929    for eff_config in &config.effects {
2930        match eff_config {
2931            react_compiler_hir::type_config::AliasingEffectConfig::Freeze { value, reason } => {
2932                let values = substitutions.get(value).cloned().unwrap_or_default();
2933                for v in values {
2934                    if mutable_spreads.contains(&v.identifier) {
2935                        return Err(CompilerDiagnostic::todo(
2936                            "Support spread syntax for hook arguments",
2937                            v.loc,
2938                        ));
2939                    }
2940                    effects.push(AliasingEffect::Freeze { value: v, reason: *reason });
2941                }
2942            }
2943            react_compiler_hir::type_config::AliasingEffectConfig::Create { into, value, reason } => {
2944                let intos = substitutions.get(into).cloned().unwrap_or_default();
2945                for v in intos {
2946                    effects.push(AliasingEffect::Create { into: v, value: *value, reason: *reason });
2947                }
2948            }
2949            react_compiler_hir::type_config::AliasingEffectConfig::CreateFrom { from, into } => {
2950                let froms = substitutions.get(from).cloned().unwrap_or_default();
2951                let intos = substitutions.get(into).cloned().unwrap_or_default();
2952                for f in &froms {
2953                    for t in &intos {
2954                        effects.push(AliasingEffect::CreateFrom { from: f.clone(), into: t.clone() });
2955                    }
2956                }
2957            }
2958            react_compiler_hir::type_config::AliasingEffectConfig::Assign { from, into } => {
2959                let froms = substitutions.get(from).cloned().unwrap_or_default();
2960                let intos = substitutions.get(into).cloned().unwrap_or_default();
2961                for f in &froms {
2962                    for t in &intos {
2963                        effects.push(AliasingEffect::Assign { from: f.clone(), into: t.clone() });
2964                    }
2965                }
2966            }
2967            react_compiler_hir::type_config::AliasingEffectConfig::Alias { from, into } => {
2968                let froms = substitutions.get(from).cloned().unwrap_or_default();
2969                let intos = substitutions.get(into).cloned().unwrap_or_default();
2970                for f in &froms {
2971                    for t in &intos {
2972                        effects.push(AliasingEffect::Alias { from: f.clone(), into: t.clone() });
2973                    }
2974                }
2975            }
2976            react_compiler_hir::type_config::AliasingEffectConfig::Capture { from, into } => {
2977                let froms = substitutions.get(from).cloned().unwrap_or_default();
2978                let intos = substitutions.get(into).cloned().unwrap_or_default();
2979                for f in &froms {
2980                    for t in &intos {
2981                        effects.push(AliasingEffect::Capture { from: f.clone(), into: t.clone() });
2982                    }
2983                }
2984            }
2985            react_compiler_hir::type_config::AliasingEffectConfig::ImmutableCapture { from, into } => {
2986                let froms = substitutions.get(from).cloned().unwrap_or_default();
2987                let intos = substitutions.get(into).cloned().unwrap_or_default();
2988                for f in &froms {
2989                    for t in &intos {
2990                        effects.push(AliasingEffect::ImmutableCapture { from: f.clone(), into: t.clone() });
2991                    }
2992                }
2993            }
2994            react_compiler_hir::type_config::AliasingEffectConfig::Impure { place } => {
2995                let values = substitutions.get(place).cloned().unwrap_or_default();
2996                for v in values {
2997                    effects.push(AliasingEffect::Impure {
2998                        place: v,
2999                        error: CompilerDiagnostic::new(ErrorCategory::Purity, "Impure function call", None),
3000                    });
3001                }
3002            }
3003            react_compiler_hir::type_config::AliasingEffectConfig::Mutate { value } => {
3004                let values = substitutions.get(value).cloned().unwrap_or_default();
3005                for v in values {
3006                    effects.push(AliasingEffect::Mutate { value: v, reason: None });
3007                }
3008            }
3009            react_compiler_hir::type_config::AliasingEffectConfig::MutateTransitiveConditionally { value } => {
3010                let values = substitutions.get(value).cloned().unwrap_or_default();
3011                for v in values {
3012                    effects.push(AliasingEffect::MutateTransitiveConditionally { value: v });
3013                }
3014            }
3015            react_compiler_hir::type_config::AliasingEffectConfig::Apply { receiver: r, function: f, mutates_function, args: a, into: i } => {
3016                let recv = substitutions.get(r).and_then(|v| v.first()).cloned();
3017                let func = substitutions.get(f).and_then(|v| v.first()).cloned();
3018                let into = substitutions.get(i).and_then(|v| v.first()).cloned();
3019                if let (Some(recv), Some(func), Some(into)) = (recv, func, into) {
3020                    let mut apply_args: Vec<PlaceOrSpreadOrHole> = Vec::new();
3021                    for arg in a {
3022                        match arg {
3023                            react_compiler_hir::type_config::ApplyArgConfig::Hole { .. } => {
3024                                apply_args.push(PlaceOrSpreadOrHole::Hole);
3025                            }
3026                            react_compiler_hir::type_config::ApplyArgConfig::Place(name) => {
3027                                if let Some(places) = substitutions.get(name) {
3028                                    if let Some(p) = places.first() {
3029                                        apply_args.push(PlaceOrSpreadOrHole::Place(p.clone()));
3030                                    }
3031                                }
3032                            }
3033                            react_compiler_hir::type_config::ApplyArgConfig::Spread { place: name, .. } => {
3034                                if let Some(places) = substitutions.get(name) {
3035                                    if let Some(p) = places.first() {
3036                                        apply_args.push(PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place: p.clone() }));
3037                                    }
3038                                }
3039                            }
3040                        }
3041                    }
3042                    effects.push(AliasingEffect::Apply {
3043                        receiver: recv,
3044                        function: func,
3045                        mutates_function: *mutates_function,
3046                        args: apply_args,
3047                        into,
3048                        signature: None,
3049                        loc: _loc.copied(),
3050                    });
3051                } else {
3052                    return Ok(None);
3053                }
3054            }
3055        }
3056    }
3057
3058    Ok(Some(effects))
3059}
3060
3061// =============================================================================
3062// Function expression signature building
3063// =============================================================================
3064
3065/// Build an AliasingSignature from a function expression's params/returns/aliasing effects.
3066/// Corresponds to TS `buildSignatureFromFunctionExpression`.
3067fn build_signature_from_function_expression(
3068    env: &mut Environment,
3069    func_id: FunctionId,
3070) -> AliasingSignature {
3071    let inner_func = &env.functions[func_id.0 as usize];
3072    let mut params: Vec<IdentifierId> = Vec::new();
3073    let mut rest: Option<IdentifierId> = None;
3074    for param in &inner_func.params {
3075        match param {
3076            ParamPattern::Place(p) => params.push(p.identifier),
3077            ParamPattern::Spread(s) => rest = Some(s.place.identifier),
3078        }
3079    }
3080    let returns = inner_func.returns.identifier;
3081    let aliasing_effects = inner_func.aliasing_effects.clone().unwrap_or_default();
3082    let loc = inner_func.loc;
3083
3084    if rest.is_none() {
3085        let temp = create_temp_place(env, loc);
3086        rest = Some(temp.identifier);
3087    }
3088
3089    AliasingSignature {
3090        receiver: IdentifierId(0),
3091        params,
3092        rest,
3093        returns,
3094        effects: aliasing_effects,
3095        temporaries: Vec::new(),
3096    }
3097}
3098
3099/// Compute effects by substituting an AliasingSignature (IdentifierId-based)
3100/// with actual arguments. Corresponds to TS `computeEffectsForSignature`.
3101fn compute_effects_for_aliasing_signature(
3102    env: &mut Environment,
3103    signature: &AliasingSignature,
3104    lvalue: &Place,
3105    receiver: &Place,
3106    args: &[PlaceOrSpreadOrHole],
3107    context: &[Place],
3108    _loc: Option<&SourceLocation>,
3109) -> Result<Option<Vec<AliasingEffect>>, CompilerDiagnostic> {
3110    if signature.params.len() > args.len()
3111        || (args.len() > signature.params.len() && signature.rest.is_none())
3112    {
3113        return Ok(None);
3114    }
3115
3116    let mut mutable_spreads: HashSet<IdentifierId> = HashSet::new();
3117    let mut substitutions: HashMap<IdentifierId, Vec<Place>> = HashMap::new();
3118    substitutions.insert(signature.receiver, vec![receiver.clone()]);
3119    substitutions.insert(signature.returns, vec![lvalue.clone()]);
3120
3121    for (i, arg) in args.iter().enumerate() {
3122        match arg {
3123            PlaceOrSpreadOrHole::Hole => continue,
3124            PlaceOrSpreadOrHole::Place(place)
3125            | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
3126                let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_));
3127                if !is_spread && i < signature.params.len() {
3128                    substitutions.insert(signature.params[i], vec![place.clone()]);
3129                } else if let Some(rest_id) = signature.rest {
3130                    substitutions
3131                        .entry(rest_id)
3132                        .or_default()
3133                        .push(place.clone());
3134                } else {
3135                    return Ok(None);
3136                }
3137
3138                if is_spread {
3139                    let ty =
3140                        &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
3141                    let mutate_iterator = conditionally_mutate_iterator(place, ty);
3142                    if mutate_iterator.is_some() {
3143                        mutable_spreads.insert(place.identifier);
3144                    }
3145                }
3146            }
3147        }
3148    }
3149
3150    // Add context variable substitutions (identity mapping)
3151    for operand in context {
3152        substitutions.insert(operand.identifier, vec![operand.clone()]);
3153    }
3154
3155    // Create temporaries
3156    for temp in &signature.temporaries {
3157        let temp_place = create_temp_place(env, receiver.loc);
3158        substitutions.insert(temp.identifier, vec![temp_place]);
3159    }
3160
3161    let mut effects: Vec<AliasingEffect> = Vec::new();
3162
3163    for eff in &signature.effects {
3164        match eff {
3165            AliasingEffect::MaybeAlias { from, into }
3166            | AliasingEffect::Assign { from, into }
3167            | AliasingEffect::ImmutableCapture { from, into }
3168            | AliasingEffect::Alias { from, into }
3169            | AliasingEffect::CreateFrom { from, into }
3170            | AliasingEffect::Capture { from, into } => {
3171                let from_places = substitutions
3172                    .get(&from.identifier)
3173                    .cloned()
3174                    .unwrap_or_default();
3175                let to_places = substitutions
3176                    .get(&into.identifier)
3177                    .cloned()
3178                    .unwrap_or_default();
3179                for f in &from_places {
3180                    for t in &to_places {
3181                        effects.push(match eff {
3182                            AliasingEffect::MaybeAlias { .. } => AliasingEffect::MaybeAlias {
3183                                from: f.clone(),
3184                                into: t.clone(),
3185                            },
3186                            AliasingEffect::Assign { .. } => AliasingEffect::Assign {
3187                                from: f.clone(),
3188                                into: t.clone(),
3189                            },
3190                            AliasingEffect::ImmutableCapture { .. } => {
3191                                AliasingEffect::ImmutableCapture {
3192                                    from: f.clone(),
3193                                    into: t.clone(),
3194                                }
3195                            }
3196                            AliasingEffect::Alias { .. } => AliasingEffect::Alias {
3197                                from: f.clone(),
3198                                into: t.clone(),
3199                            },
3200                            AliasingEffect::CreateFrom { .. } => AliasingEffect::CreateFrom {
3201                                from: f.clone(),
3202                                into: t.clone(),
3203                            },
3204                            AliasingEffect::Capture { .. } => AliasingEffect::Capture {
3205                                from: f.clone(),
3206                                into: t.clone(),
3207                            },
3208                            _ => unreachable!(),
3209                        });
3210                    }
3211                }
3212            }
3213            AliasingEffect::Impure { place, error } => {
3214                let values = substitutions
3215                    .get(&place.identifier)
3216                    .cloned()
3217                    .unwrap_or_default();
3218                for v in values {
3219                    effects.push(AliasingEffect::Impure {
3220                        place: v,
3221                        error: error.clone(),
3222                    });
3223                }
3224            }
3225            AliasingEffect::MutateFrozen { place, error } => {
3226                let values = substitutions
3227                    .get(&place.identifier)
3228                    .cloned()
3229                    .unwrap_or_default();
3230                for v in values {
3231                    effects.push(AliasingEffect::MutateFrozen {
3232                        place: v,
3233                        error: error.clone(),
3234                    });
3235                }
3236            }
3237            AliasingEffect::MutateGlobal { place, error } => {
3238                let values = substitutions
3239                    .get(&place.identifier)
3240                    .cloned()
3241                    .unwrap_or_default();
3242                for v in values {
3243                    effects.push(AliasingEffect::MutateGlobal {
3244                        place: v,
3245                        error: error.clone(),
3246                    });
3247                }
3248            }
3249            AliasingEffect::Render { place } => {
3250                let values = substitutions
3251                    .get(&place.identifier)
3252                    .cloned()
3253                    .unwrap_or_default();
3254                for v in values {
3255                    effects.push(AliasingEffect::Render { place: v });
3256                }
3257            }
3258            AliasingEffect::Mutate { value, reason } => {
3259                let values = substitutions
3260                    .get(&value.identifier)
3261                    .cloned()
3262                    .unwrap_or_default();
3263                for v in values {
3264                    effects.push(AliasingEffect::Mutate {
3265                        value: v,
3266                        reason: reason.clone(),
3267                    });
3268                }
3269            }
3270            AliasingEffect::MutateConditionally { value } => {
3271                let values = substitutions
3272                    .get(&value.identifier)
3273                    .cloned()
3274                    .unwrap_or_default();
3275                for v in values {
3276                    effects.push(AliasingEffect::MutateConditionally { value: v });
3277                }
3278            }
3279            AliasingEffect::MutateTransitive { value } => {
3280                let values = substitutions
3281                    .get(&value.identifier)
3282                    .cloned()
3283                    .unwrap_or_default();
3284                for v in values {
3285                    effects.push(AliasingEffect::MutateTransitive { value: v });
3286                }
3287            }
3288            AliasingEffect::MutateTransitiveConditionally { value } => {
3289                let values = substitutions
3290                    .get(&value.identifier)
3291                    .cloned()
3292                    .unwrap_or_default();
3293                for v in values {
3294                    effects.push(AliasingEffect::MutateTransitiveConditionally { value: v });
3295                }
3296            }
3297            AliasingEffect::Freeze { value, reason } => {
3298                let values = substitutions
3299                    .get(&value.identifier)
3300                    .cloned()
3301                    .unwrap_or_default();
3302                for v in values {
3303                    if mutable_spreads.contains(&v.identifier) {
3304                        return Err(CompilerDiagnostic::todo(
3305                            "Support spread syntax for hook arguments",
3306                            v.loc,
3307                        ));
3308                    }
3309                    effects.push(AliasingEffect::Freeze {
3310                        value: v,
3311                        reason: *reason,
3312                    });
3313                }
3314            }
3315            AliasingEffect::Create {
3316                into,
3317                value,
3318                reason,
3319            } => {
3320                let intos = substitutions
3321                    .get(&into.identifier)
3322                    .cloned()
3323                    .unwrap_or_default();
3324                for v in intos {
3325                    effects.push(AliasingEffect::Create {
3326                        into: v,
3327                        value: *value,
3328                        reason: *reason,
3329                    });
3330                }
3331            }
3332            AliasingEffect::Apply {
3333                receiver: r,
3334                function: f,
3335                mutates_function: mf,
3336                args: a,
3337                into: i,
3338                signature: s,
3339                loc: _l,
3340            } => {
3341                let recv = substitutions
3342                    .get(&r.identifier)
3343                    .and_then(|v| v.first())
3344                    .cloned();
3345                let func = substitutions
3346                    .get(&f.identifier)
3347                    .and_then(|v| v.first())
3348                    .cloned();
3349                let apply_into = substitutions
3350                    .get(&i.identifier)
3351                    .and_then(|v| v.first())
3352                    .cloned();
3353                if let (Some(recv), Some(func), Some(apply_into)) = (recv, func, apply_into) {
3354                    let mut apply_args: Vec<PlaceOrSpreadOrHole> = Vec::new();
3355                    for arg in a {
3356                        match arg {
3357                            PlaceOrSpreadOrHole::Hole => apply_args.push(PlaceOrSpreadOrHole::Hole),
3358                            PlaceOrSpreadOrHole::Place(p) => {
3359                                if let Some(places) = substitutions.get(&p.identifier) {
3360                                    if let Some(place) = places.first() {
3361                                        apply_args.push(PlaceOrSpreadOrHole::Place(place.clone()));
3362                                    }
3363                                }
3364                            }
3365                            PlaceOrSpreadOrHole::Spread(sp) => {
3366                                if let Some(places) = substitutions.get(&sp.place.identifier) {
3367                                    if let Some(place) = places.first() {
3368                                        apply_args.push(PlaceOrSpreadOrHole::Spread(
3369                                            react_compiler_hir::SpreadPattern {
3370                                                place: place.clone(),
3371                                            },
3372                                        ));
3373                                    }
3374                                }
3375                            }
3376                        }
3377                    }
3378                    effects.push(AliasingEffect::Apply {
3379                        receiver: recv,
3380                        function: func,
3381                        mutates_function: *mf,
3382                        args: apply_args,
3383                        into: apply_into,
3384                        signature: s.clone(),
3385                        loc: _loc.copied(),
3386                    });
3387                } else {
3388                    return Ok(None);
3389                }
3390            }
3391            AliasingEffect::CreateFunction { .. } => {
3392                // Not supported in signature substitution
3393                return Ok(None);
3394            }
3395        }
3396    }
3397
3398    Ok(Some(effects))
3399}
3400
3401// =============================================================================
3402// Helpers
3403// =============================================================================
3404
3405/// Select the primary (most specific) reason from a set of reasons.
3406/// TS uses `[...set][0]` which returns the first-inserted element;
3407/// since the primary reason is always inserted first, this effectively
3408/// picks the most specific non-Other reason. We replicate this by
3409/// preferring any non-Other reason over Other.
3410fn primary_reason(reasons: &IndexSet<ValueReason>) -> ValueReason {
3411    for &r in reasons {
3412        if r != ValueReason::Other {
3413            return r;
3414        }
3415    }
3416    ValueReason::Other
3417}
3418
3419fn get_write_error_reason(abstract_value: &AbstractValue) -> String {
3420    if abstract_value.reason.contains(&ValueReason::Global) {
3421        "Modifying a variable defined outside a component or hook is not allowed. Consider using an effect".to_string()
3422    } else if abstract_value.reason.contains(&ValueReason::JsxCaptured) {
3423        "Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX".to_string()
3424    } else if abstract_value.reason.contains(&ValueReason::Context) {
3425        "Modifying a value returned from 'useContext()' is not allowed.".to_string()
3426    } else if abstract_value
3427        .reason
3428        .contains(&ValueReason::KnownReturnSignature)
3429    {
3430        "Modifying a value returned from a function whose return value should not be mutated"
3431            .to_string()
3432    } else if abstract_value
3433        .reason
3434        .contains(&ValueReason::ReactiveFunctionArgument)
3435    {
3436        "Modifying component props or hook arguments is not allowed. Consider using a local variable instead".to_string()
3437    } else if abstract_value.reason.contains(&ValueReason::State) {
3438        "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead".to_string()
3439    } else if abstract_value.reason.contains(&ValueReason::ReducerState) {
3440        "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead".to_string()
3441    } else if abstract_value.reason.contains(&ValueReason::Effect) {
3442        "Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()".to_string()
3443    } else if abstract_value.reason.contains(&ValueReason::HookCaptured) {
3444        "Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook".to_string()
3445    } else if abstract_value.reason.contains(&ValueReason::HookReturn) {
3446        "Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed".to_string()
3447    } else {
3448        "This modifies a variable that React considers immutable".to_string()
3449    }
3450}
3451
3452fn conditionally_mutate_iterator(place: &Place, ty: &Type) -> Option<AliasingEffect> {
3453    if !is_builtin_collection_type(ty) {
3454        Some(AliasingEffect::MutateTransitiveConditionally {
3455            value: place.clone(),
3456        })
3457    } else {
3458        None
3459    }
3460}
3461
3462fn is_builtin_collection_type(ty: &Type) -> bool {
3463    matches!(ty, Type::Object { shape_id: Some(id) }
3464        if id == BUILT_IN_ARRAY_ID || id == BUILT_IN_SET_ID || id == BUILT_IN_MAP_ID
3465    )
3466}
3467
3468fn get_function_call_signature(
3469    env: &Environment,
3470    callee_id: IdentifierId,
3471) -> Result<Option<FunctionSignature>, CompilerDiagnostic> {
3472    let ty = &env.types[env.identifiers[callee_id.0 as usize].type_.0 as usize];
3473    Ok(env.get_function_signature(ty)?.cloned())
3474}
3475
3476fn is_ref_or_ref_value_for_id(env: &Environment, id: IdentifierId) -> bool {
3477    let ty = &env.types[env.identifiers[id.0 as usize].type_.0 as usize];
3478    react_compiler_hir::is_ref_or_ref_value(ty)
3479}
3480
3481fn get_hook_kind_for_type<'a>(
3482    env: &'a Environment,
3483    ty: &Type,
3484) -> Result<Option<&'a HookKind>, CompilerDiagnostic> {
3485    env.get_hook_kind_for_type(ty)
3486}
3487
3488/// Format a Type for printPlace-style output, matching TS's `printType()`.
3489fn format_type_for_print(ty: &Type) -> String {
3490    match ty {
3491        Type::Primitive => String::new(),
3492        Type::Function {
3493            shape_id,
3494            return_type,
3495            ..
3496        } => {
3497            if let Some(sid) = shape_id {
3498                let ret = format_type_for_print(return_type);
3499                if ret.is_empty() {
3500                    format!(":TFunction<{}>()", sid)
3501                } else {
3502                    format!(":TFunction<{}>():  {}", sid, ret)
3503                }
3504            } else {
3505                ":TFunction".to_string()
3506            }
3507        }
3508        Type::Object { shape_id } => {
3509            if let Some(sid) = shape_id {
3510                format!(":TObject<{}>", sid)
3511            } else {
3512                ":TObject".to_string()
3513            }
3514        }
3515        Type::Poly => ":TPoly".to_string(),
3516        Type::Phi { .. } => ":TPhi".to_string(),
3517        Type::Property { .. } => ":TProperty".to_string(),
3518        Type::TypeVar { .. } => String::new(),
3519        Type::ObjectMethod => ":TObjectMethod".to_string(),
3520    }
3521}
3522
3523fn is_phi_with_jsx(ty: &Type) -> bool {
3524    if let Type::Phi { operands } = ty {
3525        operands
3526            .iter()
3527            .any(|op| react_compiler_hir::is_jsx_type(op))
3528    } else {
3529        false
3530    }
3531}
3532
3533fn place_or_spread_to_hole(pos: &PlaceOrSpread) -> PlaceOrSpreadOrHole {
3534    match pos {
3535        PlaceOrSpread::Place(p) => PlaceOrSpreadOrHole::Place(p.clone()),
3536        PlaceOrSpread::Spread(s) => PlaceOrSpreadOrHole::Spread(s.clone()),
3537    }
3538}
3539
3540use react_compiler_hir::JsxTag;
3541
3542fn build_apply_operands(
3543    receiver: &Place,
3544    function: &Place,
3545    args: &[PlaceOrSpreadOrHole],
3546) -> Vec<(Place, bool, bool)> {
3547    let mut result = vec![
3548        (receiver.clone(), false, false),
3549        (function.clone(), true, false),
3550    ];
3551    for arg in args {
3552        match arg {
3553            PlaceOrSpreadOrHole::Hole => continue,
3554            PlaceOrSpreadOrHole::Place(p) => result.push((p.clone(), false, false)),
3555            PlaceOrSpreadOrHole::Spread(s) => result.push((s.place.clone(), false, true)),
3556        }
3557    }
3558    result
3559}
3560
3561fn create_temp_place(env: &mut Environment, loc: Option<SourceLocation>) -> Place {
3562    let id = env.next_identifier_id();
3563    env.identifiers[id.0 as usize].loc = loc;
3564    Place {
3565        identifier: id,
3566        effect: Effect::Unknown,
3567        reactive: false,
3568        loc,
3569    }
3570}
3571
3572// =============================================================================
3573// Terminal successor helper
3574// =============================================================================
3575
3576/// Returns the successor blocks used for traversal in mutation/aliasing inference.
3577///
3578/// Matches the TS `eachTerminalSuccessor` which yields standard control-flow
3579/// successors but NOT pseudo-successors (fallthroughs). Fallthroughs for
3580/// Logical/Ternary/Optional and Try/Scope/PrunedScope are reached naturally
3581/// via the block iteration order (blocks are stored in topological order).
3582fn terminal_successors(terminal: &react_compiler_hir::Terminal) -> Vec<BlockId> {
3583    use react_compiler_hir::Terminal;
3584    match terminal {
3585        Terminal::Goto { block, .. } => vec![*block],
3586        Terminal::If {
3587            consequent,
3588            alternate,
3589            ..
3590        } => vec![*consequent, *alternate],
3591        Terminal::Branch {
3592            consequent,
3593            alternate,
3594            ..
3595        } => vec![*consequent, *alternate],
3596        Terminal::Switch { cases, .. } => cases.iter().map(|c| c.block).collect(),
3597        Terminal::For { init, .. } => vec![*init],
3598        Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => vec![*init],
3599        Terminal::DoWhile { loop_block, .. } => vec![*loop_block],
3600        Terminal::While { test, .. } => vec![*test],
3601        Terminal::Return { .. }
3602        | Terminal::Throw { .. }
3603        | Terminal::Unreachable { .. }
3604        | Terminal::Unsupported { .. } => vec![],
3605        Terminal::Try { block, .. } => vec![*block],
3606        Terminal::MaybeThrow {
3607            continuation,
3608            handler,
3609            ..
3610        } => {
3611            let mut v = vec![*continuation];
3612            if let Some(h) = handler {
3613                v.push(*h);
3614            }
3615            v
3616        }
3617        Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => vec![*block],
3618        Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => vec![*test],
3619        Terminal::Optional { test, .. } => vec![*test],
3620        Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => vec![*block],
3621    }
3622}
3623
3624/// Pattern item helper for Destructure.
3625///
3626/// NOTE: This cannot use `visitors::each_pattern_operand` because callers need
3627/// to distinguish Place from Spread elements — Spread elements get different
3628/// aliasing effects (Create + Capture) vs Place elements (Create or CreateFrom).
3629enum PatternItem<'a> {
3630    Place(&'a Place),
3631    Spread(&'a Place),
3632}
3633
3634fn each_pattern_items(pattern: &react_compiler_hir::Pattern) -> Vec<PatternItem<'_>> {
3635    let mut items = Vec::new();
3636    match pattern {
3637        react_compiler_hir::Pattern::Array(arr) => {
3638            for el in &arr.items {
3639                match el {
3640                    react_compiler_hir::ArrayPatternElement::Place(p) => {
3641                        items.push(PatternItem::Place(p))
3642                    }
3643                    react_compiler_hir::ArrayPatternElement::Spread(s) => {
3644                        items.push(PatternItem::Spread(&s.place))
3645                    }
3646                    react_compiler_hir::ArrayPatternElement::Hole => {}
3647                }
3648            }
3649        }
3650        react_compiler_hir::Pattern::Object(obj) => {
3651            for prop in &obj.properties {
3652                match prop {
3653                    react_compiler_hir::ObjectPropertyOrSpread::Property(p) => {
3654                        items.push(PatternItem::Place(&p.place))
3655                    }
3656                    react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => {
3657                        items.push(PatternItem::Spread(&s.place))
3658                    }
3659                }
3660            }
3661        }
3662    }
3663    items
3664}