// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

//! Infers the mutation/aliasing effects for instructions and terminals.
//!
//! Ported from TypeScript `src/Inference/InferMutationAliasingEffects.ts`.
//!
//! This pass uses abstract interpretation to compute effects describing
//! creation, aliasing, mutation, freezing, and error conditions for each
//! instruction and terminal in the HIR.

use std::borrow::Cow;

use crate::react_compiler_utils::FxIndexMap;
use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;

use oxc_diagnostics::OxcDiagnostic;

use crate::diagnostics::ErrorCategory;
use crate::react_compiler_hir::AliasingEffect;
use crate::react_compiler_hir::AliasingSignature;
use crate::react_compiler_hir::ArrayElement;
use crate::react_compiler_hir::ArrayPatternElement;
use crate::react_compiler_hir::BlockId;
use crate::react_compiler_hir::DeclarationId;
use crate::react_compiler_hir::Effect;
use crate::react_compiler_hir::FunctionId;
use crate::react_compiler_hir::HirFunction;
use crate::react_compiler_hir::IdentifierId;
use crate::react_compiler_hir::IdentifierName;
use crate::react_compiler_hir::Instruction;
use crate::react_compiler_hir::InstructionKind;
use crate::react_compiler_hir::InstructionValue;
use crate::react_compiler_hir::JsxAttribute;
use crate::react_compiler_hir::MutationReason;
use crate::react_compiler_hir::ObjectPropertyOrSpread;
use crate::react_compiler_hir::ParamPattern;
use crate::react_compiler_hir::Pattern;
use crate::react_compiler_hir::Place;
use crate::react_compiler_hir::PlaceOrSpread;
use crate::react_compiler_hir::PlaceOrSpreadOrHole;
use crate::react_compiler_hir::PropertyLiteral;
use crate::react_compiler_hir::ReactFunctionType;
use crate::react_compiler_hir::SpreadPattern;
use crate::react_compiler_hir::Terminal;
use crate::react_compiler_hir::Type;
use crate::react_compiler_hir::environment::Environment;
use crate::react_compiler_hir::is_jsx_type;
use crate::react_compiler_hir::is_primitive_type;
use crate::react_compiler_hir::is_ref_or_ref_value;
use crate::react_compiler_hir::object_shape::BUILT_IN_ARRAY_ID;
use crate::react_compiler_hir::object_shape::BUILT_IN_MAP_ID;
use crate::react_compiler_hir::object_shape::BUILT_IN_SET_ID;
use crate::react_compiler_hir::object_shape::FunctionSignature;
use crate::react_compiler_hir::object_shape::HookKind;
use crate::react_compiler_hir::type_config::AliasingEffectConfig;
use crate::react_compiler_hir::type_config::AliasingSignatureConfig;
use crate::react_compiler_hir::type_config::ApplyArgConfig;
use crate::react_compiler_hir::type_config::ValueKind;
use crate::react_compiler_hir::type_config::ValueReason;
use crate::react_compiler_hir::visitors;
use oxc_span::Span;
use std::cell::Cell;
use std::rc::Rc;

// =============================================================================
// Public entry point
// =============================================================================

/// Infers mutation/aliasing effects for all instructions and terminals in `func`.
///
/// Corresponds to TS `inferMutationAliasingEffects(fn, {isFunctionExpression})`.
pub fn infer_mutation_aliasing_effects(
    func: &mut HirFunction,
    env: &mut Environment,
    is_function_expression: bool,
) -> Result<(), OxcDiagnostic> {
    let mut initial_state = InferenceState::empty(is_function_expression);

    // Map of blocks to the last (merged) incoming state that was processed
    let mut states_by_block: FxHashMap<BlockId, InferenceState> = FxHashMap::default();

    // Initialize context variables
    for ctx_place in &func.context {
        let value_id = ValueId::new();
        initial_state.initialize(
            value_id,
            AbstractValue {
                kind: ValueKind::Context,
                reason: ReasonSet::single(ValueReason::Other),
            },
        );
        initial_state.define(ctx_place.identifier, value_id);
    }

    let param_kind: AbstractValue = if is_function_expression {
        AbstractValue { kind: ValueKind::Mutable, reason: ReasonSet::single(ValueReason::Other) }
    } else {
        AbstractValue {
            kind: ValueKind::Frozen,
            reason: ReasonSet::single(ValueReason::ReactiveFunctionArgument),
        }
    };

    if func.fn_type == ReactFunctionType::Component {
        // Component: at most 2 params (props, ref)
        let params_len = func.params.len();
        if params_len > 0 {
            infer_param(&func.params[0], &mut initial_state, &param_kind);
        }
        if params_len > 1 {
            let ref_place = match &func.params[1] {
                ParamPattern::Place(p) => p,
                ParamPattern::Spread(s) => &s.place,
            };
            let value_id = ValueId::new();
            initial_state.initialize(
                value_id,
                AbstractValue {
                    kind: ValueKind::Mutable,
                    reason: ReasonSet::single(ValueReason::Other),
                },
            );
            initial_state.define(ref_place.identifier, value_id);
        }
    } else {
        for param in &func.params {
            infer_param(param, &mut initial_state, &param_kind);
        }
    }

    let mut queued_states: FxIndexMap<BlockId, InferenceState> = FxIndexMap::default();

    // Queue helper
    fn queue(
        queued_states: &mut FxIndexMap<BlockId, InferenceState>,
        states_by_block: &FxHashMap<BlockId, InferenceState>,
        block_id: BlockId,
        state: InferenceState,
    ) {
        if let Some(queued_state) = queued_states.get_mut(&block_id) {
            // Merge in place: same contents (and iteration order) as replacing
            // the entry with `merge`'s result, without cloning the whole state.
            queued_state.merge_from(&state);
        } else {
            let prev_state = states_by_block.get(&block_id);
            if let Some(prev) = prev_state {
                let next_state = prev.merge(&state);
                if let Some(next) = next_state {
                    queued_states.insert(block_id, next);
                }
            } else {
                queued_states.insert(block_id, state);
            }
        }
    }

    queue(&mut queued_states, &states_by_block, func.body.entry, initial_state);

    let hoisted_context_declarations = find_hoisted_context_declarations(func, env);
    let non_mutating_spreads = find_non_mutated_destructure_spreads(func, env);

    let mut context = Context {
        interned_effects: FxHashMap::default(),
        instruction_signature_cache: FxHashMap::default(),
        catch_handlers: FxHashMap::default(),
        is_function_expression,
        hoisted_context_declarations,
        non_mutating_spreads,
        effect_value_id_cache: FxHashMap::default(),
        function_values: FxHashMap::default(),
        function_signature_cache: FxHashMap::default(),
        aliasing_config_temp_cache: FxHashMap::default(),
    };

    // `states_by_block` is only ever read when a block is queued again after it
    // was already processed, which requires a CFG cycle. If every edge targets a
    // strictly later block in the map, the graph is acyclic and one pass over the
    // blocks suffices, so skip the per-block state snapshots. Reverse postorder
    // (the stored order) only makes this test precise for acyclic CFGs; soundness
    // needs no ordering assumption, and a successor missing from the map counts
    // as a back edge to stay conservative. The scan cannot go stale: `infer_block`
    // rewrites only terminal effects, never successor ids.
    let has_back_edge = func.body.blocks.iter().enumerate().any(|(index, (_, block))| {
        terminal_successors(&block.terminal).into_iter().any(|successor| {
            match func.body.blocks.get_index_of(&successor) {
                Some(successor_index) => successor_index <= index,
                None => true,
            }
        })
    });

    let mut iteration_count = 0;

    while !queued_states.is_empty() {
        iteration_count += 1;
        if iteration_count > 100 {
            return Err(ErrorCategory::Invariant.diagnostic(
                "[InferMutationAliasingEffects] Potential infinite loop: \
                 A value, temporary place, or effect was not cached properly",
            ));
        }

        // Collect block IDs to process in order
        let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();
        for block_id in block_ids {
            let incoming_state = match queued_states.swap_remove(&block_id) {
                Some(s) => s,
                None => continue,
            };

            if has_back_edge {
                states_by_block.insert(block_id, incoming_state.clone());
            }
            let mut state = incoming_state;

            infer_block(&mut context, &mut state, block_id, func, env)?;

            // Check for uninitialized identifier access (matches TS invariant:
            // "Expected value kind to be initialized")
            if let Some((uninitialized_id, usage_span)) = state.uninitialized_access.get() {
                let ident_info = env.identifiers.get(uninitialized_id);
                let name = ident_info
                    .and_then(|ident| ident.name.as_ref())
                    .map(|n| n.value().to_string())
                    .unwrap_or_default();
                // Use usage_span if available, otherwise fall back to identifier's own span
                let error_span = usage_span.or_else(|| ident_info.and_then(|i| i.span));
                // Match TS printPlace format: "<unknown> name$id:type"
                let type_str = ident_info
                    .map(|ident| {
                        let ty = &env.types[ident.type_];
                        format_type_for_print(ty)
                    })
                    .unwrap_or_default();
                let description =
                    format!("<unknown> {}${}{}", name, uninitialized_id.index(), type_str);
                let diag = ErrorCategory::Invariant
                    .diagnostic(
                        "[InferMutationAliasingEffects] Expected value kind to be initialized",
                    )
                    .with_help(description)
                    .with_labels(error_span.map(|s| s.label("this is uninitialized")));
                return Err(diag);
            }

            // Queue successors. Every successor receives the same post-block state;
            // the last one takes it by move instead of by clone.
            let successors = terminal_successors(&func.body.blocks[&block_id].terminal);
            let mut successors = successors.into_iter();
            if let Some(mut next_block_id) = successors.next() {
                for later_block_id in successors {
                    queue(&mut queued_states, &states_by_block, next_block_id, state.clone());
                    next_block_id = later_block_id;
                }
                queue(&mut queued_states, &states_by_block, next_block_id, state);
            }
        }
    }

    Ok(())
}

// =============================================================================
// ValueId: replaces InstructionValue identity as allocation-site key
// =============================================================================

/// Unique allocation-site identifier, replacing TS's object-identity on InstructionValue.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ValueId(u32);

use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering;
static NEXT_VALUE_ID: AtomicU32 = AtomicU32::new(1);

impl ValueId {
    fn new() -> Self {
        ValueId(NEXT_VALUE_ID.fetch_add(1, Ordering::Relaxed))
    }

    /// Deterministic value id derived from an identifier's current value. The high bit is
    /// set so these never collide with the counter-allocated ids from `new()`.
    fn from_identifier(id: IdentifierId) -> Self {
        ValueId(id.index() as u32 | 0x80000000)
    }
}

// =============================================================================
// AbstractValue
// =============================================================================

#[derive(Debug, Clone, Copy)]
struct AbstractValue {
    kind: ValueKind,
    reason: ReasonSet,
}

/// A set of [`ValueReason`]s stored as a bitmask.
///
/// `AbstractValue`s are copied constantly during the inference fixpoint — once per
/// entry on every `InferenceState` clone — so `reason` must be cheap to copy. There
/// are only 12 `ValueReason` variants, so the set fits in a `u16` bitmask: it is
/// `Copy` (no per-clone allocation or memcpy), and membership/union/superset are
/// single bitwise ops. [`Self::iter`] yields reasons in `ValueReason` declaration
/// order, which is the order [`primary_reason`] consumes.
#[derive(Debug, Clone, Copy, Default)]
struct ReasonSet(u16);

impl ReasonSet {
    /// All variants in declaration order; index matches the bit position.
    const ALL: [ValueReason; 12] = [
        ValueReason::KnownReturnSignature,
        ValueReason::State,
        ValueReason::ReducerState,
        ValueReason::Context,
        ValueReason::Effect,
        ValueReason::HookCaptured,
        ValueReason::HookReturn,
        ValueReason::Global,
        ValueReason::JsxCaptured,
        ValueReason::StoreLocal,
        ValueReason::ReactiveFunctionArgument,
        ValueReason::Other,
    ];

    fn bit(reason: ValueReason) -> u16 {
        1 << (reason as u16)
    }

    fn single(reason: ValueReason) -> Self {
        Self(Self::bit(reason))
    }

    fn contains(&self, reason: &ValueReason) -> bool {
        self.0 & Self::bit(*reason) != 0
    }

    fn iter(&self) -> impl Iterator<Item = ValueReason> + '_ {
        Self::ALL.into_iter().filter(move |&reason| self.0 & Self::bit(reason) != 0)
    }
}

// =============================================================================
// InferenceState
// =============================================================================

/// The abstract state tracked during inference.
/// The pass has exclusive access to each state (interior mutability only via
/// the `uninitialized_access` Cell); the one shared piece is the immutable
/// per-variable value sets (see `variables`).
#[derive(Debug, Clone)]
struct InferenceState {
    is_function_expression: bool,
    /// The kind of each value, based on its allocation site
    values: FxHashMap<ValueId, AbstractValue>,
    /// The set of values pointed to by each identifier. The sets are shared via
    /// `Rc`: every write to the map replaces the whole entry (sets are never
    /// mutated in place), so state clones and assignments bump a refcount
    /// instead of deep-copying each set. Shared sets iterate the very table a
    /// deep clone would have copied; freshly built union/phi/singleton sets are
    /// constructed by the same insert sequence as before. Either way iteration
    /// order is unchanged.
    variables: FxHashMap<IdentifierId, Rc<FxHashSet<ValueId>>>,
    /// Tracks uninitialized identifier access errors (matches TS invariant).
    /// Uses Cell so it can be set from `&self` methods like `kind()`.
    /// Stores (IdentifierId, usage_span) where usage_span is the source location
    /// of the Place that triggered the uninitialized access.
    uninitialized_access: Cell<Option<(IdentifierId, Option<Span>)>>,
}

impl InferenceState {
    fn empty(is_function_expression: bool) -> Self {
        InferenceState {
            is_function_expression,
            values: FxHashMap::default(),
            variables: FxHashMap::default(),
            uninitialized_access: Cell::new(None),
        }
    }

    /// Check the kind of a place, recording the usage location for error reporting.
    fn kind_with_span(&self, place_id: IdentifierId, usage_span: Option<Span>) -> AbstractValue {
        let values = match self.variables.get(&place_id) {
            Some(v) => v,
            None => {
                if self.uninitialized_access.get().is_none() {
                    self.uninitialized_access.set(Some((place_id, usage_span)));
                }
                return AbstractValue {
                    kind: ValueKind::Mutable,
                    reason: ReasonSet::single(ValueReason::Other),
                };
            }
        };
        let mut merged_kind: Option<AbstractValue> = None;
        for value_id in values.iter() {
            let kind = match self.values.get(value_id) {
                Some(k) => k,
                None => continue,
            };
            merged_kind = Some(match merged_kind {
                Some(prev) => merge_abstract_values(&prev, kind),
                None => *kind,
            });
        }
        merged_kind.unwrap_or_else(|| AbstractValue {
            kind: ValueKind::Mutable,
            reason: ReasonSet::single(ValueReason::Other),
        })
    }

    fn initialize(&mut self, value_id: ValueId, kind: AbstractValue) {
        self.values.insert(value_id, kind);
    }

    fn define(&mut self, place_id: IdentifierId, value_id: ValueId) {
        let mut set = FxHashSet::default();
        set.insert(value_id);
        self.variables.insert(place_id, Rc::new(set));
    }

    fn assign(&mut self, into: IdentifierId, from: IdentifierId) {
        let values = match self.variables.get(&from) {
            Some(v) => Rc::clone(v),
            None => {
                // Create a stable value for uninitialized identifiers
                // Use a deterministic ID based on the from identifier
                let vid = ValueId::from_identifier(from);
                let mut set = FxHashSet::default();
                set.insert(vid);
                self.values.entry(vid).or_insert_with(|| AbstractValue {
                    kind: ValueKind::Mutable,
                    reason: ReasonSet::single(ValueReason::Other),
                });
                Rc::new(set)
            }
        };
        self.variables.insert(into, values);
    }

    fn append_alias(&mut self, place: IdentifierId, value: IdentifierId) {
        let new_values = match self.variables.get(&value) {
            Some(v) => Rc::clone(v),
            None => return,
        };
        let prev_values = match self.variables.get(&place) {
            Some(v) => Rc::clone(v),
            None => return,
        };
        let merged: FxHashSet<ValueId> = prev_values.union(&new_values).copied().collect();
        self.variables.insert(place, Rc::new(merged));
    }

    fn is_defined(&self, place_id: IdentifierId) -> bool {
        self.variables.contains_key(&place_id)
    }

    fn values_for(&self, place_id: IdentifierId) -> Vec<ValueId> {
        match self.variables.get(&place_id) {
            Some(values) => values.iter().copied().collect(),
            None => Vec::new(),
        }
    }

    fn kind(&self, place_id: IdentifierId) -> AbstractValue {
        self.kind_with_span(place_id, None)
    }

    fn freeze(&mut self, place_id: IdentifierId, reason: ValueReason) -> bool {
        // Check if defined first to avoid recording uninitialized access error.
        // Freeze on undefined identifiers is a no-op — this matches the TS
        // behavior where freeze() is never called on undefined identifiers
        // (the invariant in kind() catches this before freeze is reached).
        if !self.variables.contains_key(&place_id) {
            return false;
        }
        let value = self.kind(place_id);
        match value.kind {
            ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => {
                let value_ids: Vec<ValueId> = self.values_for(place_id);
                for vid in value_ids {
                    self.freeze_value(vid, reason);
                }
                true
            }
            ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => false,
        }
    }

    fn freeze_value(&mut self, value_id: ValueId, reason: ValueReason) {
        self.values.insert(
            value_id,
            AbstractValue { kind: ValueKind::Frozen, reason: ReasonSet::single(reason) },
        );
        // Note: In TS, this also transitively freezes FunctionExpression captures
        // if enableTransitivelyFreezeFunctionExpressions is set. We skip that here
        // since we don't have access to the function arena from within state.
    }

    fn mutate_with_span(
        &self,
        variant: MutateVariant,
        place_id: IdentifierId,
        env: &Environment,
        usage_span: Option<Span>,
    ) -> MutationResult {
        let ty = &env.types[env.identifiers[place_id].type_];
        if is_ref_or_ref_value(ty) {
            return MutationResult::MutateRef;
        }
        let kind = self.kind_with_span(place_id, usage_span).kind;
        match variant {
            MutateVariant::MutateConditionally | MutateVariant::MutateTransitiveConditionally => {
                match kind {
                    ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate,
                    _ => MutationResult::None,
                }
            }
            MutateVariant::Mutate | MutateVariant::MutateTransitive => match kind {
                ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate,
                ValueKind::Primitive => MutationResult::None,
                ValueKind::Frozen | ValueKind::MaybeFrozen => MutationResult::MutateFrozen,
                ValueKind::Global => MutationResult::MutateGlobal,
            },
        }
    }

    fn merge(&self, other: &InferenceState) -> Option<InferenceState> {
        let mut next_values: Option<FxHashMap<ValueId, AbstractValue>> = None;
        let mut next_variables: Option<FxHashMap<IdentifierId, Rc<FxHashSet<ValueId>>>> = None;

        // Merge values present in both
        for (id, this_value) in &self.values {
            if let Some(other_value) = other.values.get(id) {
                let merged = merge_abstract_values(this_value, other_value);
                if merged.kind != this_value.kind
                    || !is_superset(&this_value.reason, &merged.reason)
                {
                    let nv = next_values.get_or_insert_with(|| self.values.clone());
                    nv.insert(*id, merged);
                }
            }
        }
        // Add values only in other
        for (id, other_value) in &other.values {
            if !self.values.contains_key(id) {
                let nv = next_values.get_or_insert_with(|| self.values.clone());
                nv.insert(*id, *other_value);
            }
        }

        // Merge variables present in both
        for (id, this_values) in &self.variables {
            if let Some(other_values) = other.variables.get(id) {
                let mut has_new = false;
                for ov in other_values.iter() {
                    if !this_values.contains(ov) {
                        has_new = true;
                        break;
                    }
                }
                if has_new {
                    let nvars = next_variables.get_or_insert_with(|| self.variables.clone());
                    let merged: FxHashSet<ValueId> =
                        this_values.union(other_values).copied().collect();
                    nvars.insert(*id, Rc::new(merged));
                }
            }
        }
        // Add variables only in other
        for (id, other_values) in &other.variables {
            if !self.variables.contains_key(id) {
                let nvars = next_variables.get_or_insert_with(|| self.variables.clone());
                nvars.insert(*id, Rc::clone(other_values));
            }
        }

        if next_variables.is_none() && next_values.is_none() {
            None
        } else {
            Some(InferenceState {
                is_function_expression: self.is_function_expression,
                values: next_values.unwrap_or_else(|| self.values.clone()),
                variables: next_variables.unwrap_or_else(|| self.variables.clone()),
                uninitialized_access: Cell::new(None),
            })
        }
    }

    /// In-place variant of [`InferenceState::merge`] for updating an
    /// already-queued state: applies the same per-entry updates directly to
    /// `self` instead of building a replacement state. Because `FxHashMap`'s
    /// `clone` preserves table layout, this leaves `self` with exactly the same
    /// contents *and iteration order* as `merge` + reinsert would. Like
    /// `merge`'s result, the access flag ends up cleared (queued states never
    /// carry a set flag: a set flag errors out before the state is queued).
    fn merge_from(&mut self, other: &InferenceState) {
        for (id, other_value) in &other.values {
            match self.values.get(id) {
                Some(this_value) => {
                    let merged = merge_abstract_values(this_value, other_value);
                    if merged.kind != this_value.kind
                        || !is_superset(&this_value.reason, &merged.reason)
                    {
                        self.values.insert(*id, merged);
                    }
                }
                None => {
                    self.values.insert(*id, *other_value);
                }
            }
        }

        for (id, other_values) in &other.variables {
            match self.variables.get(id) {
                Some(this_values) => {
                    let has_new = other_values.iter().any(|ov| !this_values.contains(ov));
                    if has_new {
                        // Build the union as a fresh set exactly like `merge`, so
                        // the resulting iteration order matches.
                        let merged: FxHashSet<ValueId> =
                            this_values.union(other_values).copied().collect();
                        self.variables.insert(*id, Rc::new(merged));
                    }
                }
                None => {
                    self.variables.insert(*id, Rc::clone(other_values));
                }
            }
        }

        // `merge` builds its result with a cleared access flag.
        debug_assert!(self.uninitialized_access.get().is_none());
        self.uninitialized_access.set(None);
    }

    fn infer_phi(&mut self, phi_place_id: IdentifierId, phi_operands: &FxIndexMap<BlockId, Place>) {
        let mut values: FxHashSet<ValueId> = FxHashSet::default();
        for (_, operand) in phi_operands {
            if let Some(operand_values) = self.variables.get(&operand.identifier) {
                for v in operand_values.iter() {
                    values.insert(*v);
                }
            }
            // If not found, it's a backedge that will be handled later by merge
        }
        if !values.is_empty() {
            self.variables.insert(phi_place_id, Rc::new(values));
        }
    }
}

fn is_superset(a: &ReasonSet, b: &ReasonSet) -> bool {
    a.0 & b.0 == b.0
}

#[derive(Debug, Clone, Copy)]
enum MutateVariant {
    Mutate,
    MutateConditionally,
    MutateTransitive,
    MutateTransitiveConditionally,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MutationResult {
    None,
    Mutate,
    MutateFrozen,
    MutateGlobal,
    MutateRef,
}

// =============================================================================
// Context
// =============================================================================

struct Context {
    interned_effects: FxHashMap<String, AliasingEffect>,
    instruction_signature_cache: FxHashMap<u32, InstructionSignature>,
    catch_handlers: FxHashMap<BlockId, Place>,
    is_function_expression: bool,
    hoisted_context_declarations: FxHashMap<DeclarationId, Option<Place>>,
    non_mutating_spreads: FxHashSet<IdentifierId>,
    /// Cache of ValueIds keyed by effect hash, ensuring stable allocation-site identity
    /// across fixpoint iterations. Mirrors TS `effectInstructionValueCache`.
    effect_value_id_cache: FxHashMap<String, ValueId>,
    /// Maps ValueId to FunctionId for function expressions, so we can look up
    /// locally-declared functions when processing Apply effects.
    function_values: FxHashMap<ValueId, FunctionId>,
    /// Cache of function expression signatures, keyed by FunctionId
    function_signature_cache: FxHashMap<FunctionId, AliasingSignature>,
    /// Cache of temporary places created for aliasing signature config temporaries.
    /// Keyed by (lvalue_identifier_id, temp_name) to ensure stable allocation
    /// across fixpoint iterations.
    aliasing_config_temp_cache: FxHashMap<(IdentifierId, String), Place>,
}

impl Context {
    fn intern_effect(&mut self, effect: AliasingEffect) -> AliasingEffect {
        let hash = hash_effect(&effect);
        self.interned_effects.entry(hash).or_insert(effect).clone()
    }

    /// Get or create a stable ValueId for a given effect, ensuring fixpoint convergence.
    fn get_or_create_value_id(&mut self, effect: &AliasingEffect) -> ValueId {
        let hash = hash_effect(effect);
        *self.effect_value_id_cache.entry(hash).or_insert_with(ValueId::new)
    }
}

struct InstructionSignature {
    effects: Vec<AliasingEffect>,
}

// =============================================================================
// Helper: hash_effect
// =============================================================================

fn hash_effect(effect: &AliasingEffect) -> String {
    match effect {
        AliasingEffect::Apply { receiver, function, mutates_function, args, into, .. } => {
            let args_str: Vec<Cow<'_, str>> = args
                .iter()
                .map(|a| match a {
                    PlaceOrSpreadOrHole::Hole => Cow::Borrowed(""),
                    PlaceOrSpreadOrHole::Place(p) => {
                        Cow::Owned(format!("{}", p.identifier.index()))
                    }
                    PlaceOrSpreadOrHole::Spread(s) => {
                        Cow::Owned(format!("...{}", s.place.identifier.index()))
                    }
                })
                .collect();
            format!(
                "Apply:{}:{}:{}:{}:{}",
                receiver.identifier.index(),
                function.identifier.index(),
                mutates_function,
                args_str.join(","),
                into.identifier.index()
            )
        }
        AliasingEffect::CreateFrom { from, into } => {
            format!("CreateFrom:{}:{}", from.identifier.index(), into.identifier.index())
        }
        AliasingEffect::ImmutableCapture { from, into } => {
            format!("ImmutableCapture:{}:{}", from.identifier.index(), into.identifier.index())
        }
        AliasingEffect::Assign { from, into } => {
            format!("Assign:{}:{}", from.identifier.index(), into.identifier.index())
        }
        AliasingEffect::Alias { from, into } => {
            format!("Alias:{}:{}", from.identifier.index(), into.identifier.index())
        }
        AliasingEffect::Capture { from, into } => {
            format!("Capture:{}:{}", from.identifier.index(), into.identifier.index())
        }
        AliasingEffect::MaybeAlias { from, into } => {
            format!("MaybeAlias:{}:{}", from.identifier.index(), into.identifier.index())
        }
        AliasingEffect::Create { into, value, reason } => {
            format!("Create:{}:{:?}:{:?}", into.identifier.index(), value, reason)
        }
        AliasingEffect::Freeze { value, reason } => {
            format!("Freeze:{}:{:?}", value.identifier.index(), reason)
        }
        AliasingEffect::Impure { place, .. } => format!("Impure:{}", place.identifier.index()),
        AliasingEffect::Render { place } => format!("Render:{}", place.identifier.index()),
        AliasingEffect::MutateFrozen { place, error } => {
            format!("MutateFrozen:{}:{}:{:?}", place.identifier.index(), error.message, error.help)
        }
        AliasingEffect::MutateGlobal { place, error } => {
            format!("MutateGlobal:{}:{}:{:?}", place.identifier.index(), error.message, error.help)
        }
        AliasingEffect::Mutate { value, .. } => format!("Mutate:{}", value.identifier.index()),
        AliasingEffect::MutateConditionally { value } => {
            format!("MutateConditionally:{}", value.identifier.index())
        }
        AliasingEffect::MutateTransitive { value } => {
            format!("MutateTransitive:{}", value.identifier.index())
        }
        AliasingEffect::MutateTransitiveConditionally { value } => {
            format!("MutateTransitiveConditionally:{}", value.identifier.index())
        }
        AliasingEffect::CreateFunction { into, function_id, captures } => {
            let cap_str: Vec<String> =
                captures.iter().map(|p| format!("{}", p.identifier.index())).collect();
            format!(
                "CreateFunction:{}:{}:{}",
                into.identifier.index(),
                function_id.index(),
                cap_str.join(",")
            )
        }
    }
}

// =============================================================================
// merge helpers
// =============================================================================

fn merge_abstract_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
    let kind = merge_value_kinds(a.kind, b.kind);
    if kind == a.kind && kind == b.kind && is_superset(&a.reason, &b.reason) {
        return *a;
    }
    let reason = ReasonSet(a.reason.0 | b.reason.0);
    AbstractValue { kind, reason }
}

fn merge_value_kinds(a: ValueKind, b: ValueKind) -> ValueKind {
    if a == b {
        return a;
    }
    if a == ValueKind::MaybeFrozen || b == ValueKind::MaybeFrozen {
        return ValueKind::MaybeFrozen;
    }
    if a == ValueKind::Mutable || b == ValueKind::Mutable {
        if a == ValueKind::Frozen || b == ValueKind::Frozen {
            return ValueKind::MaybeFrozen;
        } else if a == ValueKind::Context || b == ValueKind::Context {
            return ValueKind::Context;
        } else {
            return ValueKind::Mutable;
        }
    }
    if a == ValueKind::Context || b == ValueKind::Context {
        if a == ValueKind::Frozen || b == ValueKind::Frozen {
            return ValueKind::MaybeFrozen;
        } else {
            return ValueKind::Context;
        }
    }
    if a == ValueKind::Frozen || b == ValueKind::Frozen {
        return ValueKind::Frozen;
    }
    if a == ValueKind::Global || b == ValueKind::Global {
        return ValueKind::Global;
    }
    ValueKind::Primitive
}

// =============================================================================
// Pre-passes
// =============================================================================

fn find_hoisted_context_declarations(
    func: &HirFunction,
    env: &Environment,
) -> FxHashMap<DeclarationId, Option<Place>> {
    let mut hoisted: FxHashMap<DeclarationId, Option<Place>> = FxHashMap::default();

    fn visit(
        hoisted: &mut FxHashMap<DeclarationId, Option<Place>>,
        place: &Place,
        env: &Environment,
    ) {
        let decl_id = env.identifiers[place.identifier].declaration_id;
        if hoisted.contains_key(&decl_id) && hoisted.get(&decl_id).unwrap().is_none() {
            hoisted.insert(decl_id, Some(place.clone()));
        }
    }

    for (_block_id, block) in &func.body.blocks {
        for instr_id in &block.instructions {
            let instr = &func.instructions[instr_id.index()];
            match &instr.value {
                InstructionValue::DeclareContext { lvalue, .. } => {
                    let kind = lvalue.kind;
                    if kind == InstructionKind::HoistedConst
                        || kind == InstructionKind::HoistedFunction
                        || kind == InstructionKind::HoistedLet
                    {
                        let decl_id = env.identifiers[lvalue.place.identifier].declaration_id;
                        hoisted.insert(decl_id, None);
                    }
                }
                _ => {
                    for operand in visitors::each_instruction_value_operand(&instr.value, env) {
                        visit(&mut hoisted, &operand, env);
                    }
                }
            }
        }
        for operand in visitors::each_terminal_operand(&block.terminal) {
            visit(&mut hoisted, &operand, env);
        }
    }
    hoisted
}

fn find_non_mutated_destructure_spreads(
    func: &HirFunction,
    env: &Environment,
) -> FxHashSet<IdentifierId> {
    let mut known_frozen: FxHashSet<IdentifierId> = FxHashSet::default();
    if func.fn_type == ReactFunctionType::Component {
        if let Some(ParamPattern::Place(p)) = func.params.first() {
            known_frozen.insert(p.identifier);
        }
    } else {
        for param in &func.params {
            if let ParamPattern::Place(p) = param {
                known_frozen.insert(p.identifier);
            }
        }
    }

    let mut candidate_non_mutating_spreads: FxHashMap<IdentifierId, IdentifierId> =
        FxHashMap::default();
    for (_block_id, block) in &func.body.blocks {
        if !candidate_non_mutating_spreads.is_empty() {
            for phi in &block.phis {
                for (_, operand) in &phi.operands {
                    if let Some(spread) =
                        candidate_non_mutating_spreads.get(&operand.identifier).copied()
                    {
                        candidate_non_mutating_spreads.remove(&spread);
                    }
                }
            }
        }
        for instr_id in &block.instructions {
            let instr = &func.instructions[instr_id.index()];
            let lvalue_id = instr.lvalue.identifier;
            match &instr.value {
                InstructionValue::Destructure { lvalue, value, .. } => {
                    if !known_frozen.contains(&value.identifier) {
                        continue;
                    }
                    if !(lvalue.kind == InstructionKind::Let
                        || lvalue.kind == InstructionKind::Const)
                    {
                        continue;
                    }
                    match &lvalue.pattern {
                        Pattern::Object(obj_pat) => {
                            for prop in &obj_pat.properties {
                                if let ObjectPropertyOrSpread::Spread(s) = prop {
                                    candidate_non_mutating_spreads
                                        .insert(s.place.identifier, s.place.identifier);
                                }
                            }
                        }
                        _ => continue,
                    }
                }
                InstructionValue::LoadLocal { place, .. } => {
                    if let Some(spread) =
                        candidate_non_mutating_spreads.get(&place.identifier).copied()
                    {
                        candidate_non_mutating_spreads.insert(lvalue_id, spread);
                    }
                }
                InstructionValue::StoreLocal { lvalue: sl, value: sv, .. } => {
                    if let Some(spread) =
                        candidate_non_mutating_spreads.get(&sv.identifier).copied()
                    {
                        candidate_non_mutating_spreads.insert(lvalue_id, spread);
                        candidate_non_mutating_spreads.insert(sl.place.identifier, spread);
                    }
                }
                InstructionValue::JsxFragment { .. } | InstructionValue::JsxExpression { .. } => {
                    // Passing objects created with spread to jsx can't mutate them
                }
                InstructionValue::PropertyLoad { .. } => {
                    // Properties must be frozen since the original value was frozen
                }
                InstructionValue::CallExpression { callee, .. }
                | InstructionValue::MethodCall { property: callee, .. } => {
                    let callee_ty = &env.types[env.identifiers[callee.identifier].type_];
                    if get_hook_kind_for_type(env, callee_ty).ok().flatten().is_some() {
                        if !is_ref_or_ref_value_for_id(env, lvalue_id) {
                            known_frozen.insert(lvalue_id);
                        }
                    } else if !candidate_non_mutating_spreads.is_empty() {
                        for operand in visitors::each_instruction_value_operand(&instr.value, env) {
                            if let Some(spread) =
                                candidate_non_mutating_spreads.get(&operand.identifier).copied()
                            {
                                candidate_non_mutating_spreads.remove(&spread);
                            }
                        }
                    }
                }
                _ => {
                    if !candidate_non_mutating_spreads.is_empty() {
                        for operand in visitors::each_instruction_value_operand(&instr.value, env) {
                            if let Some(spread) =
                                candidate_non_mutating_spreads.get(&operand.identifier).copied()
                            {
                                candidate_non_mutating_spreads.remove(&spread);
                            }
                        }
                    }
                }
            }
        }
    }

    let mut non_mutating: FxHashSet<IdentifierId> = FxHashSet::default();
    for (key, value) in &candidate_non_mutating_spreads {
        if key == value {
            non_mutating.insert(*key);
        }
    }
    non_mutating
}

// =============================================================================
// inferParam
// =============================================================================

fn infer_param(param: &ParamPattern, state: &mut InferenceState, param_kind: &AbstractValue) {
    let place = match param {
        ParamPattern::Place(p) => p,
        ParamPattern::Spread(s) => &s.place,
    };
    let value_id = ValueId::new();
    state.initialize(value_id, *param_kind);
    state.define(place.identifier, value_id);
}

// =============================================================================
// inferBlock
// =============================================================================

fn infer_block(
    context: &mut Context,
    state: &mut InferenceState,
    block_id: BlockId,
    func: &mut HirFunction,
    env: &mut Environment,
) -> Result<(), OxcDiagnostic> {
    let block = &func.body.blocks[&block_id];

    // Process phis
    let phis: Vec<(IdentifierId, FxIndexMap<BlockId, Place>)> =
        block.phis.iter().map(|phi| (phi.place.identifier, phi.operands.clone())).collect();
    for (place_id, operands) in &phis {
        state.infer_phi(*place_id, operands);
    }

    // Process instructions
    let instr_ids: Vec<u32> = block.instructions.iter().map(|id| id.index() as u32).collect();
    for instr_idx in &instr_ids {
        let instr_index = *instr_idx as usize;

        // Compute signature if not cached
        if !context.instruction_signature_cache.contains_key(instr_idx) {
            let sig =
                compute_signature_for_instruction(context, env, &func.instructions[instr_index]);
            context.instruction_signature_cache.insert(*instr_idx, sig);
        }

        // Apply signature
        let effects =
            apply_signature(context, state, *instr_idx, &func.instructions[instr_index], env)?;
        func.instructions[instr_index].effects = effects;
    }

    // Process terminal
    // Determine what terminal action to take without holding borrows
    enum TerminalAction {
        Try { handler: BlockId, binding: Place },
        MaybeThrow { handler_id: BlockId },
        Return,
        None,
    }
    let action = {
        let block = &func.body.blocks[&block_id];
        match &block.terminal {
            Terminal::Try { handler, handler_binding: Some(binding), .. } => {
                TerminalAction::Try { handler: *handler, binding: binding.clone() }
            }
            Terminal::MaybeThrow { handler: Some(handler_id), .. } => {
                TerminalAction::MaybeThrow { handler_id: *handler_id }
            }
            Terminal::Return { .. } => TerminalAction::Return,
            _ => TerminalAction::None,
        }
    };

    match action {
        TerminalAction::Try { handler, binding } => {
            context.catch_handlers.insert(handler, binding);
        }
        TerminalAction::MaybeThrow { handler_id } => {
            if let Some(handler_param) = context.catch_handlers.get(&handler_id).cloned()
                && state.is_defined(handler_param.identifier)
            {
                let mut terminal_effects: Vec<AliasingEffect> = Vec::new();
                for instr_idx in &instr_ids {
                    let instr = &func.instructions[*instr_idx as usize];
                    match &instr.value {
                        InstructionValue::CallExpression { .. }
                        | InstructionValue::MethodCall { .. } => {
                            state.append_alias(handler_param.identifier, instr.lvalue.identifier);
                            let kind = state.kind(instr.lvalue.identifier).kind;
                            if kind == ValueKind::Mutable || kind == ValueKind::Context {
                                terminal_effects.push(context.intern_effect(
                                    AliasingEffect::Alias {
                                        from: instr.lvalue.clone(),
                                        into: handler_param.clone(),
                                    },
                                ));
                            }
                        }
                        _ => {}
                    }
                }
                let block_mut = func.body.blocks.get_mut(&block_id).unwrap();
                if let Terminal::MaybeThrow { effects: ref mut term_effects, .. } =
                    block_mut.terminal
                {
                    *term_effects =
                        if terminal_effects.is_empty() { None } else { Some(terminal_effects) };
                }
            }
        }
        TerminalAction::Return => {
            if !context.is_function_expression {
                let block_mut = func.body.blocks.get_mut(&block_id).unwrap();
                if let Terminal::Return { ref value, effects: ref mut term_effects, .. } =
                    block_mut.terminal
                {
                    *term_effects = Some(vec![context.intern_effect(AliasingEffect::Freeze {
                        value: value.clone(),
                        reason: ValueReason::JsxCaptured,
                    })]);
                }
            }
        }
        TerminalAction::None => {}
    }
    Ok(())
}

// =============================================================================
// applySignature
// =============================================================================

fn apply_signature(
    context: &mut Context,
    state: &mut InferenceState,
    instr_idx: u32,
    instr: &Instruction,
    env: &mut Environment,
) -> Result<Option<Vec<AliasingEffect>>, OxcDiagnostic> {
    let mut effects: Vec<AliasingEffect> = Vec::new();

    // For function instructions, validate frozen mutation
    match &instr.value {
        InstructionValue::FunctionExpression { lowered_func, .. }
        | InstructionValue::ObjectMethod { lowered_func, .. } => {
            let inner_func = &env.functions[lowered_func.func];
            if let Some(ref aliasing_effects) = inner_func.aliasing_effects {
                let context_ids: FxHashSet<IdentifierId> =
                    inner_func.context.iter().map(|p| p.identifier).collect();
                for effect in aliasing_effects {
                    let mutate_value = match effect {
                        AliasingEffect::Mutate { value, .. }
                        | AliasingEffect::MutateTransitive { value } => value,
                        _ => continue,
                    };
                    if !context_ids.contains(&mutate_value.identifier) {
                        continue;
                    }
                    if !state.is_defined(mutate_value.identifier) {
                        continue;
                    }
                    let value_abstract = state.kind(mutate_value.identifier);
                    if value_abstract.kind == ValueKind::Frozen {
                        let reason_str = get_write_error_reason(&value_abstract);
                        let ident = &env.identifiers[mutate_value.identifier];
                        let variable = match &ident.name {
                            Some(IdentifierName::Named(n)) => {
                                format!("`{}`", n)
                            }
                            _ => "value".to_string(),
                        };
                        let diagnostic = ErrorCategory::Immutability
                            .diagnostic("This value cannot be modified")
                            .with_help(reason_str)
                            .with_labels(
                                mutate_value
                                    .span
                                    .map(|s| s.label(format!("{} cannot be modified", variable))),
                            );
                        effects.push(AliasingEffect::MutateFrozen {
                            place: mutate_value.clone(),
                            error: diagnostic,
                        });
                    }
                }
            }
        }
        _ => {}
    }

    // Track which values we've already initialized
    let mut initialized: FxHashSet<IdentifierId> = FxHashSet::default();

    // Get the cached signature effects
    let sig = context.instruction_signature_cache.get(&instr_idx).unwrap();
    let sig_effects: Vec<AliasingEffect> = sig.effects.clone();

    for effect in &sig_effects {
        apply_effect(context, state, effect.clone(), &mut initialized, &mut effects, env)?;
    }

    // If lvalue is not yet defined, initialize it with a default value.
    // The TS version asserts this as an invariant, but the Rust port may have
    // edge cases where effects don't cover the lvalue (e.g. missing signature entries).
    if !state.is_defined(instr.lvalue.identifier) {
        let vid = ValueId::from_identifier(instr.lvalue.identifier);
        state.initialize(
            vid,
            AbstractValue {
                kind: ValueKind::Mutable,
                reason: ReasonSet::single(ValueReason::Other),
            },
        );
        state.define(instr.lvalue.identifier, vid);
    }

    Ok(if effects.is_empty() { None } else { Some(effects) })
}

// =============================================================================
// Transitive freeze helper
// =============================================================================

/// Recursively freeze through FunctionExpression captures. If `value_id`
/// corresponds to a FunctionExpression, freeze each of its context captures
/// and recurse into any that are themselves FunctionExpressions. This matches
/// the TS `freezeValue` → `freeze` → `freezeValue` recursion chain.
fn freeze_function_captures_transitive(
    state: &mut InferenceState,
    context: &Context,
    env: &Environment,
    value_id: ValueId,
    reason: ValueReason,
) {
    if let Some(&func_id) = context.function_values.get(&value_id) {
        let ctx_ids: Vec<IdentifierId> =
            env.functions[func_id].context.iter().map(|p| p.identifier).collect();
        for ctx_id in ctx_ids {
            // Replicate InferenceState::freeze() logic inline —
            // we need to recurse with context/env which freeze() doesn't have.
            if !state.variables.contains_key(&ctx_id) {
                continue;
            }
            let kind = state.kind(ctx_id).kind;
            match kind {
                ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => {
                    let vids: Vec<ValueId> = state.values_for(ctx_id);
                    for vid in vids {
                        state.freeze_value(vid, reason);
                        // Recurse into nested function captures
                        freeze_function_captures_transitive(state, context, env, vid, reason);
                    }
                }
                ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => {
                    // Already frozen or immutable — no-op
                }
            }
        }
    }
}

// =============================================================================
// applyEffect
// =============================================================================

fn apply_effect(
    context: &mut Context,
    state: &mut InferenceState,
    effect: AliasingEffect,
    initialized: &mut FxHashSet<IdentifierId>,
    effects: &mut Vec<AliasingEffect>,
    env: &mut Environment,
) -> Result<(), OxcDiagnostic> {
    let effect = context.intern_effect(effect);
    match effect {
        AliasingEffect::Freeze { ref value, reason } => {
            let did_freeze = state.freeze(value.identifier, reason);
            if did_freeze {
                effects.push(effect.clone());
                // Transitively freeze FunctionExpression captures if enabled
                // (matches TS freezeValue which recurses into func.context)
                let enable_transitive = env.config.enable_preserve_existing_memoization_guarantees
                    || env.config.enable_transitively_freeze_function_expressions;
                if enable_transitive {
                    // Recursively freeze through function captures. The TS
                    // freezeValue() calls freeze() on each capture, which
                    // calls freezeValue() again — creating a transitive
                    // closure through arbitrarily nested function captures.
                    let value_ids: Vec<ValueId> = state.values_for(value.identifier);
                    for vid in &value_ids {
                        freeze_function_captures_transitive(state, context, env, *vid, reason);
                    }
                }
            }
        }
        AliasingEffect::Create { ref into, value: kind, reason } => {
            assert!(
                !initialized.contains(&into.identifier),
                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
            );
            initialized.insert(into.identifier);
            let value_id = context.get_or_create_value_id(&effect);
            state.initialize(value_id, AbstractValue { kind, reason: ReasonSet::single(reason) });
            state.define(into.identifier, value_id);
            effects.push(effect.clone());
        }
        AliasingEffect::ImmutableCapture { ref from, .. } => {
            let kind = state.kind(from.identifier).kind;
            match kind {
                ValueKind::Global | ValueKind::Primitive => {
                    // no-op: don't track data flow for copy types
                }
                _ => {
                    effects.push(effect.clone());
                }
            }
        }
        AliasingEffect::CreateFrom { ref from, ref into } => {
            assert!(
                !initialized.contains(&into.identifier),
                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
            );
            initialized.insert(into.identifier);
            let from_value = state.kind(from.identifier);
            let value_id = context.get_or_create_value_id(&effect);
            state.initialize(
                value_id,
                AbstractValue { kind: from_value.kind, reason: from_value.reason },
            );
            state.define(into.identifier, value_id);
            match from_value.kind {
                ValueKind::Primitive | ValueKind::Global => {
                    let first_reason = primary_reason(&from_value.reason);
                    effects.push(AliasingEffect::Create {
                        value: from_value.kind,
                        into: into.clone(),
                        reason: first_reason,
                    });
                }
                ValueKind::Frozen => {
                    let first_reason = primary_reason(&from_value.reason);
                    effects.push(AliasingEffect::Create {
                        value: from_value.kind,
                        into: into.clone(),
                        reason: first_reason,
                    });
                    apply_effect(
                        context,
                        state,
                        AliasingEffect::ImmutableCapture { from: from.clone(), into: into.clone() },
                        initialized,
                        effects,
                        env,
                    )?;
                }
                _ => {
                    effects.push(effect.clone());
                }
            }
        }
        AliasingEffect::CreateFunction { ref captures, function_id, ref into } => {
            assert!(
                !initialized.contains(&into.identifier),
                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
            );
            initialized.insert(into.identifier);
            effects.push(effect.clone());

            // Check if function is mutable
            let has_captures = captures.iter().any(|capture| {
                if !state.is_defined(capture.identifier) {
                    return false;
                }
                let k = state.kind(capture.identifier).kind;
                k == ValueKind::Context || k == ValueKind::Mutable
            });

            let inner_func = &env.functions[function_id];
            let has_tracked_side_effects = inner_func
                .aliasing_effects
                .as_ref()
                .map(|effs| {
                    effs.iter().any(|e| {
                        matches!(
                            e,
                            AliasingEffect::MutateFrozen { .. }
                                | AliasingEffect::MutateGlobal { .. }
                                | AliasingEffect::Impure { .. }
                        )
                    })
                })
                .unwrap_or(false);

            let captures_ref = inner_func
                .context
                .iter()
                .any(|operand| is_ref_or_ref_value_for_id(env, operand.identifier));

            let is_mutable = has_captures || has_tracked_side_effects || captures_ref;

            // Update context variable effects
            let context_places: Vec<Place> = inner_func.context.clone();
            for operand in &context_places {
                if operand.effect != Effect::Capture {
                    continue;
                }
                if !state.is_defined(operand.identifier) {
                    continue;
                }
                let kind = state.kind(operand.identifier).kind;
                if kind == ValueKind::Primitive
                    || kind == ValueKind::Frozen
                    || kind == ValueKind::Global
                {
                    // Downgrade to Read - we need to mutate the inner function
                    let inner_func_mut = &mut env.functions[function_id];
                    for ctx in &mut inner_func_mut.context {
                        if ctx.identifier == operand.identifier && ctx.effect == Effect::Capture {
                            ctx.effect = Effect::Read;
                        }
                    }
                }
            }

            let value_id = context.get_or_create_value_id(&effect);
            // Track this value as a function expression so Apply can look it up
            context.function_values.insert(value_id, function_id);
            state.initialize(
                value_id,
                AbstractValue {
                    kind: if is_mutable { ValueKind::Mutable } else { ValueKind::Frozen },
                    reason: ReasonSet::default(),
                },
            );
            state.define(into.identifier, value_id);

            for capture in captures {
                apply_effect(
                    context,
                    state,
                    AliasingEffect::Capture { from: capture.clone(), into: into.clone() },
                    initialized,
                    effects,
                    env,
                )?;
            }
        }
        AliasingEffect::MaybeAlias { ref from, ref into }
        | AliasingEffect::Alias { ref from, ref into }
        | AliasingEffect::Capture { ref from, ref into } => {
            let is_capture = matches!(effect, AliasingEffect::Capture { .. });
            let is_maybe_alias = matches!(effect, AliasingEffect::MaybeAlias { .. });
            // For Alias, destination must already be initialized (Capture/MaybeAlias are exempt)
            assert!(
                is_capture || is_maybe_alias || initialized.contains(&into.identifier),
                "[InferMutationAliasingEffects] Expected destination to already be initialized within this instruction"
            );

            // Check destination kind
            let into_kind = state.kind_with_span(into.identifier, into.span).kind;
            let destination_type = match into_kind {
                ValueKind::Context => Some("context"),
                ValueKind::Mutable | ValueKind::MaybeFrozen => Some("mutable"),
                _ => None,
            };

            let from_kind = state.kind_with_span(from.identifier, from.span).kind;
            let source_type = match from_kind {
                ValueKind::Context => Some("context"),
                ValueKind::Global | ValueKind::Primitive => None,
                ValueKind::MaybeFrozen | ValueKind::Frozen => Some("frozen"),
                ValueKind::Mutable => Some("mutable"),
            };

            if source_type == Some("frozen") {
                apply_effect(
                    context,
                    state,
                    AliasingEffect::ImmutableCapture { from: from.clone(), into: into.clone() },
                    initialized,
                    effects,
                    env,
                )?;
            } else if (source_type == Some("mutable") && destination_type == Some("mutable"))
                || is_maybe_alias
            {
                effects.push(effect.clone());
            } else if (source_type == Some("context") && destination_type.is_some())
                || (source_type == Some("mutable") && destination_type == Some("context"))
            {
                apply_effect(
                    context,
                    state,
                    AliasingEffect::MaybeAlias { from: from.clone(), into: into.clone() },
                    initialized,
                    effects,
                    env,
                )?;
            }
        }
        AliasingEffect::Assign { ref from, ref into } => {
            assert!(
                !initialized.contains(&into.identifier),
                "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"
            );
            initialized.insert(into.identifier);
            let from_value = state.kind_with_span(from.identifier, from.span);
            match from_value.kind {
                ValueKind::Frozen => {
                    apply_effect(
                        context,
                        state,
                        AliasingEffect::ImmutableCapture { from: from.clone(), into: into.clone() },
                        initialized,
                        effects,
                        env,
                    )?;
                    let cache_key = format!(
                        "Assign_frozen:{}:{}",
                        from.identifier.index(),
                        into.identifier.index()
                    );
                    let value_id = *context
                        .effect_value_id_cache
                        .entry(cache_key)
                        .or_insert_with(ValueId::new);
                    state.initialize(
                        value_id,
                        AbstractValue { kind: from_value.kind, reason: from_value.reason },
                    );
                    state.define(into.identifier, value_id);
                }
                ValueKind::Global | ValueKind::Primitive => {
                    let cache_key = format!(
                        "Assign_copy:{}:{}",
                        from.identifier.index(),
                        into.identifier.index()
                    );
                    let value_id = *context
                        .effect_value_id_cache
                        .entry(cache_key)
                        .or_insert_with(ValueId::new);
                    state.initialize(
                        value_id,
                        AbstractValue { kind: from_value.kind, reason: from_value.reason },
                    );
                    state.define(into.identifier, value_id);
                }
                _ => {
                    state.assign(into.identifier, from.identifier);
                    effects.push(effect.clone());
                }
            }
        }
        AliasingEffect::Apply {
            ref receiver,
            ref function,
            mutates_function,
            ref args,
            ref into,
            ref signature,
            ref span,
        } => {
            // First, check if the callee is a locally-declared function expression
            // whose aliasing effects we already know (TS lines 1016-1068)
            if state.is_defined(function.identifier) {
                let function_values = state.values_for(function.identifier);
                if function_values.len() == 1 {
                    let value_id = function_values[0];
                    if let Some(func_id) = context.function_values.get(&value_id).copied() {
                        let inner_func = &env.functions[func_id];
                        if inner_func.aliasing_effects.is_some() {
                            // Build or retrieve the signature from the function expression
                            context.function_signature_cache.entry(func_id).or_insert_with(|| {
                                build_signature_from_function_expression(env, func_id)
                            });
                            let sig =
                                context.function_signature_cache.get(&func_id).unwrap().clone();
                            let inner_func = &env.functions[func_id];
                            let context_places: Vec<Place> = inner_func.context.clone();
                            let sig_effects = compute_effects_for_aliasing_signature(
                                env,
                                &sig,
                                into,
                                receiver,
                                args,
                                &context_places,
                                span.as_ref(),
                            )?;
                            if let Some(sig_effs) = sig_effects {
                                // Conditionally mutate the function itself first
                                apply_effect(
                                    context,
                                    state,
                                    AliasingEffect::MutateTransitiveConditionally {
                                        value: function.clone(),
                                    },
                                    initialized,
                                    effects,
                                    env,
                                )?;
                                for se in sig_effs {
                                    apply_effect(context, state, se, initialized, effects, env)?;
                                }
                                return Ok(());
                            }
                        }
                    }
                }
            }
            if let Some(sig) = signature {
                // Check known_incompatible (TS line 2351-2370)
                if let Some(ref incompatible_msg) = sig.known_incompatible
                    && env.enable_validations()
                {
                    let diagnostic = ErrorCategory::IncompatibleLibrary
                            .diagnostic("Use of incompatible library")
                            .with_help(
                                "This API returns functions which cannot be memoized without leading to stale UI. \
                                 To prevent this, by default React Compiler will skip memoizing this component/hook. \
                                 However, you may see issues if values from this API are passed to other components/hooks that are \
                                 memoized",
                            )
                            .with_labels(
                                receiver.span.map(|s| s.label(incompatible_msg.clone())),
                            );
                    // TS throws here, aborting compilation for this function
                    return Err(diagnostic);
                }

                if let Some(ref aliasing) = sig.aliasing {
                    let sig_effects = compute_effects_for_aliasing_signature_config(
                        env,
                        aliasing,
                        into,
                        receiver,
                        args,
                        &[],
                        span.as_ref(),
                        &mut context.aliasing_config_temp_cache,
                    )?;
                    if let Some(sig_effs) = sig_effects {
                        for se in sig_effs {
                            apply_effect(context, state, se, initialized, effects, env)?;
                        }
                        return Ok(());
                    }
                }

                // Legacy signature
                let mut todo_errors: Vec<OxcDiagnostic> = Vec::new();
                let legacy_effects = compute_effects_for_legacy_signature(
                    state,
                    sig,
                    into,
                    receiver,
                    args,
                    span.as_ref(),
                    env,
                    &context.function_values,
                    &mut todo_errors,
                );
                // Todo errors should short-circuit (TS throws throwTodo)
                if let Some(err_detail) = todo_errors.into_iter().next() {
                    return Err(err_detail);
                }
                for le in legacy_effects {
                    apply_effect(context, state, le, initialized, effects, env)?;
                }
            } else {
                // No signature: default behavior
                apply_effect(
                    context,
                    state,
                    AliasingEffect::Create {
                        into: into.clone(),
                        value: ValueKind::Mutable,
                        reason: ValueReason::Other,
                    },
                    initialized,
                    effects,
                    env,
                )?;

                let all_operands = build_apply_operands(receiver, function, args);
                for (operand, _is_function_operand, is_spread) in &all_operands {
                    // In TS, the check is `operand !== effect.function || effect.mutatesFunction`.
                    // This compares by reference identity, so for CallExpression/NewExpression
                    // where receiver === function, BOTH are skipped when !mutatesFunction.
                    if operand.identifier == function.identifier && !mutates_function {
                        // Don't mutate callee for non-mutating calls
                    } else {
                        apply_effect(
                            context,
                            state,
                            AliasingEffect::MutateTransitiveConditionally {
                                value: operand.clone(),
                            },
                            initialized,
                            effects,
                            env,
                        )?;
                    }

                    if *is_spread {
                        let ty = &env.types[env.identifiers[operand.identifier].type_];
                        if let Some(mutate_iter) = conditionally_mutate_iterator(operand, ty) {
                            apply_effect(context, state, mutate_iter, initialized, effects, env)?;
                        }
                    }

                    apply_effect(
                        context,
                        state,
                        AliasingEffect::MaybeAlias { from: operand.clone(), into: into.clone() },
                        initialized,
                        effects,
                        env,
                    )?;

                    // In TS, `other === arg` compares the Place extracted from
                    // `otherArg` with the original `arg` element. For Identifier
                    // args, the extracted Place IS the arg, so this is a reference
                    // identity check. For Spread args, the extracted Place is
                    // `.place` which is never `===` the Spread wrapper object,
                    // so NO pairs are skipped when the outer arg is a Spread
                    // (including self-pairs, producing self-captures).
                    for (other, _other_is_func, _other_is_spread) in &all_operands {
                        if !is_spread && other.identifier == operand.identifier {
                            continue;
                        }
                        apply_effect(
                            context,
                            state,
                            AliasingEffect::Capture { from: operand.clone(), into: other.clone() },
                            initialized,
                            effects,
                            env,
                        )?;
                    }
                }
            }
        }
        ref eff @ (AliasingEffect::Mutate { .. }
        | AliasingEffect::MutateConditionally { .. }
        | AliasingEffect::MutateTransitive { .. }
        | AliasingEffect::MutateTransitiveConditionally { .. }) => {
            let (mutate_place, variant) = match eff {
                AliasingEffect::Mutate { value, .. } => (value, MutateVariant::Mutate),
                AliasingEffect::MutateConditionally { value } => {
                    (value, MutateVariant::MutateConditionally)
                }
                AliasingEffect::MutateTransitive { value } => {
                    (value, MutateVariant::MutateTransitive)
                }
                AliasingEffect::MutateTransitiveConditionally { value } => {
                    (value, MutateVariant::MutateTransitiveConditionally)
                }
                _ => unreachable!(),
            };
            let value = mutate_place;
            let mutation_kind = state.mutate_with_span(variant, value.identifier, env, value.span);
            if mutation_kind == MutationResult::Mutate {
                effects.push(effect.clone());
            } else if mutation_kind == MutationResult::MutateRef {
                // no-op
            } else if mutation_kind != MutationResult::None
                && matches!(variant, MutateVariant::Mutate | MutateVariant::MutateTransitive)
            {
                let abstract_value = state.kind(value.identifier);

                let ident = &env.identifiers[value.identifier];
                let decl_id = ident.declaration_id;

                if mutation_kind == MutationResult::MutateFrozen
                    && context.hoisted_context_declarations.contains_key(&decl_id)
                {
                    let variable = match &ident.name {
                        Some(IdentifierName::Named(n)) => Some(format!("`{}`", n)),
                        _ => None,
                    };
                    let hoisted_access =
                        context.hoisted_context_declarations.get(&decl_id).cloned().flatten();
                    let mut diagnostic = ErrorCategory::Immutability
                        .diagnostic("Cannot access variable before it is declared")
                        .with_help(format!(
                            "{} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time",
                            variable.as_deref().unwrap_or("This variable")
                        ));
                    if let Some(ref access) = hoisted_access
                        && access.span != value.span
                    {
                        diagnostic.labels.extend(access.span.map(|s| {
                            s.label(format!(
                                "{} accessed before it is declared",
                                variable.as_deref().unwrap_or("variable")
                            ))
                        }));
                    }
                    diagnostic.labels.extend(value.span.map(|s| {
                        s.label(format!(
                            "{} is declared here",
                            variable.as_deref().unwrap_or("variable")
                        ))
                    }));
                    apply_effect(
                        context,
                        state,
                        AliasingEffect::MutateFrozen { place: value.clone(), error: diagnostic },
                        initialized,
                        effects,
                        env,
                    )?;
                } else {
                    let reason_str = get_write_error_reason(&abstract_value);
                    let variable = match &ident.name {
                        Some(IdentifierName::Named(n)) => {
                            format!("`{}`", n)
                        }
                        _ => "value".to_string(),
                    };
                    let diagnostic = ErrorCategory::Immutability
                        .diagnostic("This value cannot be modified")
                        .with_help(reason_str)
                        .with_labels(
                            value.span.map(|s| s.label(format!("{} cannot be modified", variable))),
                        );

                    let error_kind = if abstract_value.kind == ValueKind::Frozen {
                        AliasingEffect::MutateFrozen { place: value.clone(), error: diagnostic }
                    } else {
                        AliasingEffect::MutateGlobal { place: value.clone(), error: diagnostic }
                    };
                    apply_effect(context, state, error_kind, initialized, effects, env)?;
                }
            }
        }
        AliasingEffect::Impure { .. }
        | AliasingEffect::Render { .. }
        | AliasingEffect::MutateFrozen { .. }
        | AliasingEffect::MutateGlobal { .. } => {
            effects.push(effect.clone());
        }
    }
    Ok(())
}

// =============================================================================
// computeSignatureForInstruction
// =============================================================================

fn compute_signature_for_instruction(
    context: &mut Context,
    env: &Environment,
    instr: &Instruction,
) -> InstructionSignature {
    let lvalue = &instr.lvalue;
    let value = &instr.value;
    let mut effects: Vec<AliasingEffect> = Vec::new();

    match value {
        InstructionValue::ArrayExpression { elements, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Mutable,
                reason: ValueReason::Other,
            });
            for element in elements {
                match element {
                    ArrayElement::Place(p) => {
                        effects.push(AliasingEffect::Capture {
                            from: p.clone(),
                            into: lvalue.clone(),
                        });
                    }
                    ArrayElement::Spread(s) => {
                        let ty = &env.types[env.identifiers[s.place.identifier].type_];
                        if let Some(mutate_iter) = conditionally_mutate_iterator(&s.place, ty) {
                            effects.push(mutate_iter);
                        }
                        effects.push(AliasingEffect::Capture {
                            from: s.place.clone(),
                            into: lvalue.clone(),
                        });
                    }
                    ArrayElement::Hole => {}
                }
            }
        }
        InstructionValue::ObjectExpression { properties, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Mutable,
                reason: ValueReason::Other,
            });
            for property in properties {
                match property {
                    ObjectPropertyOrSpread::Property(p) => {
                        effects.push(AliasingEffect::Capture {
                            from: p.place.clone(),
                            into: lvalue.clone(),
                        });
                    }
                    ObjectPropertyOrSpread::Spread(s) => {
                        effects.push(AliasingEffect::Capture {
                            from: s.place.clone(),
                            into: lvalue.clone(),
                        });
                    }
                }
            }
        }
        InstructionValue::Await { value: await_value, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Mutable,
                reason: ValueReason::Other,
            });
            effects
                .push(AliasingEffect::MutateTransitiveConditionally { value: await_value.clone() });
            effects
                .push(AliasingEffect::Capture { from: await_value.clone(), into: lvalue.clone() });
        }
        InstructionValue::NewExpression { callee, args, span } => {
            let sig = get_function_call_signature(env, callee.identifier).ok().flatten();
            effects.push(AliasingEffect::Apply {
                receiver: callee.clone(),
                function: callee.clone(),
                mutates_function: false,
                args: args.iter().map(place_or_spread_to_hole).collect(),
                into: lvalue.clone(),
                signature: sig,
                span: *span,
            });
        }
        InstructionValue::CallExpression { callee, args, span } => {
            let sig = get_function_call_signature(env, callee.identifier).ok().flatten();
            effects.push(AliasingEffect::Apply {
                receiver: callee.clone(),
                function: callee.clone(),
                mutates_function: true,
                args: args.iter().map(place_or_spread_to_hole).collect(),
                into: lvalue.clone(),
                signature: sig,
                span: *span,
            });
        }
        InstructionValue::MethodCall { receiver, property, args, span } => {
            let sig = get_function_call_signature(env, property.identifier).ok().flatten();
            effects.push(AliasingEffect::Apply {
                receiver: receiver.clone(),
                function: property.clone(),
                mutates_function: false,
                args: args.iter().map(place_or_spread_to_hole).collect(),
                into: lvalue.clone(),
                signature: sig,
                span: *span,
            });
        }
        InstructionValue::PropertyDelete { object, .. }
        | InstructionValue::ComputedDelete { object, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
            effects.push(AliasingEffect::Mutate { value: object.clone(), reason: None });
        }
        InstructionValue::PropertyLoad { object, .. }
        | InstructionValue::ComputedLoad { object, .. } => {
            let ty = &env.types[env.identifiers[lvalue.identifier].type_];
            if is_primitive_type(ty) {
                effects.push(AliasingEffect::Create {
                    into: lvalue.clone(),
                    value: ValueKind::Primitive,
                    reason: ValueReason::Other,
                });
            } else {
                effects.push(AliasingEffect::CreateFrom {
                    from: object.clone(),
                    into: lvalue.clone(),
                });
            }
        }
        InstructionValue::PropertyStore { object, property, value: store_value, .. } => {
            let mutation_reason: Option<MutationReason> = {
                let obj_ty = &env.types[env.identifiers[object.identifier].type_];
                if let PropertyLiteral::String(prop_name) = property {
                    if prop_name == "current" && matches!(obj_ty, Type::Var { .. }) {
                        Some(MutationReason::AssignCurrentProperty)
                    } else {
                        None
                    }
                } else {
                    None
                }
            };
            effects.push(AliasingEffect::Mutate { value: object.clone(), reason: mutation_reason });
            effects
                .push(AliasingEffect::Capture { from: store_value.clone(), into: object.clone() });
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        InstructionValue::ComputedStore { object, value: store_value, .. } => {
            effects.push(AliasingEffect::Mutate { value: object.clone(), reason: None });
            effects
                .push(AliasingEffect::Capture { from: store_value.clone(), into: object.clone() });
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        InstructionValue::FunctionExpression { lowered_func, .. }
        | InstructionValue::ObjectMethod { lowered_func, .. } => {
            let inner_func = &env.functions[lowered_func.func];
            let captures: Vec<Place> = inner_func
                .context
                .iter()
                .filter(|operand| operand.effect == Effect::Capture)
                .cloned()
                .collect();
            effects.push(AliasingEffect::CreateFunction {
                into: lvalue.clone(),
                function_id: lowered_func.func,
                captures,
            });
        }
        InstructionValue::GetIterator { collection, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Mutable,
                reason: ValueReason::Other,
            });
            let ty = &env.types[env.identifiers[collection.identifier].type_];
            if is_builtin_collection_type(ty) {
                effects.push(AliasingEffect::Capture {
                    from: collection.clone(),
                    into: lvalue.clone(),
                });
            } else {
                effects
                    .push(AliasingEffect::Alias { from: collection.clone(), into: lvalue.clone() });
                effects.push(AliasingEffect::MutateTransitiveConditionally {
                    value: collection.clone(),
                });
            }
        }
        InstructionValue::IteratorNext { iterator, collection, .. } => {
            effects.push(AliasingEffect::MutateConditionally { value: iterator.clone() });
            effects.push(AliasingEffect::CreateFrom {
                from: collection.clone(),
                into: lvalue.clone(),
            });
        }
        InstructionValue::NextPropertyOf { .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        InstructionValue::JsxExpression { tag, props, children, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Frozen,
                reason: ValueReason::JsxCaptured,
            });
            for operand in visitors::each_instruction_value_operand(value, env) {
                effects.push(AliasingEffect::Freeze {
                    value: operand.clone(),
                    reason: ValueReason::JsxCaptured,
                });
                effects
                    .push(AliasingEffect::Capture { from: operand.clone(), into: lvalue.clone() });
            }
            if let JsxTag::Place(tag_place) = tag {
                effects.push(AliasingEffect::Render { place: tag_place.clone() });
            }
            if let Some(ch) = children {
                for child in ch {
                    effects.push(AliasingEffect::Render { place: child.clone() });
                }
            }
            for prop in props {
                if let JsxAttribute::Attribute { place: prop_place, .. } = prop {
                    let prop_ty = &env.types[env.identifiers[prop_place.identifier].type_];
                    if let Type::Function { return_type, .. } = prop_ty
                        && (is_jsx_type(return_type) || is_phi_with_jsx(return_type))
                    {
                        effects.push(AliasingEffect::Render { place: prop_place.clone() });
                    }
                }
            }
        }
        InstructionValue::JsxFragment { .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Frozen,
                reason: ValueReason::JsxCaptured,
            });
            for operand in visitors::each_instruction_value_operand(value, env) {
                effects.push(AliasingEffect::Freeze {
                    value: operand.clone(),
                    reason: ValueReason::JsxCaptured,
                });
                effects
                    .push(AliasingEffect::Capture { from: operand.clone(), into: lvalue.clone() });
            }
        }
        InstructionValue::DeclareLocal { lvalue: dl, .. } => {
            effects.push(AliasingEffect::Create {
                into: dl.place.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        InstructionValue::Destructure { lvalue: dl, value: dest_value, .. } => {
            for pat_item in each_pattern_items(&dl.pattern) {
                match pat_item {
                    PatternItem::Place(place) => {
                        let ty = &env.types[env.identifiers[place.identifier].type_];
                        if is_primitive_type(ty) {
                            effects.push(AliasingEffect::Create {
                                into: place.clone(),
                                value: ValueKind::Primitive,
                                reason: ValueReason::Other,
                            });
                        } else {
                            effects.push(AliasingEffect::CreateFrom {
                                from: dest_value.clone(),
                                into: place.clone(),
                            });
                        }
                    }
                    PatternItem::Spread(place) => {
                        let value_kind = if context.non_mutating_spreads.contains(&place.identifier)
                        {
                            ValueKind::Frozen
                        } else {
                            ValueKind::Mutable
                        };
                        effects.push(AliasingEffect::Create {
                            into: place.clone(),
                            reason: ValueReason::Other,
                            value: value_kind,
                        });
                        effects.push(AliasingEffect::Capture {
                            from: dest_value.clone(),
                            into: place.clone(),
                        });
                    }
                }
            }
            effects.push(AliasingEffect::Assign { from: dest_value.clone(), into: lvalue.clone() });
        }
        InstructionValue::LoadContext { place, .. } => {
            effects.push(AliasingEffect::CreateFrom { from: place.clone(), into: lvalue.clone() });
        }
        InstructionValue::DeclareContext { lvalue: dcl, .. } => {
            let decl_id = env.identifiers[dcl.place.identifier].declaration_id;
            let kind = dcl.kind;
            if !context.hoisted_context_declarations.contains_key(&decl_id)
                || kind == InstructionKind::HoistedConst
                || kind == InstructionKind::HoistedFunction
                || kind == InstructionKind::HoistedLet
            {
                effects.push(AliasingEffect::Create {
                    into: dcl.place.clone(),
                    value: ValueKind::Mutable,
                    reason: ValueReason::Other,
                });
            } else {
                effects.push(AliasingEffect::Mutate { value: dcl.place.clone(), reason: None });
            }
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        InstructionValue::StoreContext { lvalue: scl, value: sc_value, .. } => {
            let decl_id = env.identifiers[scl.place.identifier].declaration_id;
            if scl.kind == InstructionKind::Reassign
                || context.hoisted_context_declarations.contains_key(&decl_id)
            {
                effects.push(AliasingEffect::Mutate { value: scl.place.clone(), reason: None });
            } else {
                effects.push(AliasingEffect::Create {
                    into: scl.place.clone(),
                    value: ValueKind::Mutable,
                    reason: ValueReason::Other,
                });
            }
            effects
                .push(AliasingEffect::Capture { from: sc_value.clone(), into: scl.place.clone() });
            effects.push(AliasingEffect::Assign { from: sc_value.clone(), into: lvalue.clone() });
        }
        InstructionValue::LoadLocal { place, .. } => {
            effects.push(AliasingEffect::Assign { from: place.clone(), into: lvalue.clone() });
        }
        InstructionValue::StoreLocal { lvalue: sl, value: sl_value, .. } => {
            effects.push(AliasingEffect::Assign { from: sl_value.clone(), into: sl.place.clone() });
            effects.push(AliasingEffect::Assign { from: sl_value.clone(), into: lvalue.clone() });
        }
        InstructionValue::PostfixUpdate { lvalue: pf_lvalue, .. }
        | InstructionValue::PrefixUpdate { lvalue: pf_lvalue, .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
            effects.push(AliasingEffect::Create {
                into: pf_lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        InstructionValue::StoreGlobal { name, value: sg_value, .. } => {
            let variable = format!("`{}`", name);
            let diagnostic = ErrorCategory::Globals
                .diagnostic("Cannot reassign variables declared outside of the component/hook")
                .with_help(format!(
                    "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)",
                    variable
                ))
                .with_labels(
                    instr.span.map(|s| s.label(format!("{} cannot be reassigned", variable))),
                );
            effects
                .push(AliasingEffect::MutateGlobal { place: sg_value.clone(), error: diagnostic });
            effects.push(AliasingEffect::Assign { from: sg_value.clone(), into: lvalue.clone() });
        }
        InstructionValue::TypeCastExpression { value: tc_value, .. } => {
            effects.push(AliasingEffect::Assign { from: tc_value.clone(), into: lvalue.clone() });
        }
        InstructionValue::LoadGlobal { .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Global,
                reason: ValueReason::Global,
            });
        }
        InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => {
            if env.config.enable_preserve_existing_memoization_guarantees {
                for operand in visitors::each_instruction_value_operand(value, env) {
                    effects.push(AliasingEffect::Freeze {
                        value: operand.clone(),
                        reason: ValueReason::HookCaptured,
                    });
                }
            }
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
        // All primitive-creating instructions
        InstructionValue::TaggedTemplateExpression { .. }
        | InstructionValue::BinaryExpression { .. }
        | InstructionValue::Debugger { .. }
        | InstructionValue::JSXText { .. }
        | InstructionValue::MetaProperty { .. }
        | InstructionValue::Primitive { .. }
        | InstructionValue::RegExpLiteral { .. }
        | InstructionValue::TemplateLiteral { .. }
        | InstructionValue::UnaryExpression { .. } => {
            effects.push(AliasingEffect::Create {
                into: lvalue.clone(),
                value: ValueKind::Primitive,
                reason: ValueReason::Other,
            });
        }
    }

    InstructionSignature { effects }
}

// =============================================================================
// Legacy signature support
// =============================================================================

#[allow(clippy::too_many_arguments)]
fn compute_effects_for_legacy_signature(
    state: &InferenceState,
    signature: &FunctionSignature,
    lvalue: &Place,
    receiver: &Place,
    args: &[PlaceOrSpreadOrHole],
    span: Option<&Span>,
    env: &Environment,
    function_values: &FxHashMap<ValueId, FunctionId>,
    todo_errors: &mut Vec<OxcDiagnostic>,
) -> Vec<AliasingEffect> {
    let return_value_reason = signature.return_value_reason.unwrap_or(ValueReason::Other);
    let mut effects: Vec<AliasingEffect> = Vec::new();

    effects.push(AliasingEffect::Create {
        into: lvalue.clone(),
        value: signature.return_value_kind,
        reason: return_value_reason,
    });

    if signature.impure && env.config.validate_no_impure_functions_in_render {
        let diagnostic = ErrorCategory::Purity
            .diagnostic("Cannot call impure function during render")
            .with_help(format!(
                "{}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)",
                if let Some(ref name) = signature.canonical_name {
                    format!("`{}` is an impure function. ", name)
                } else {
                    String::new()
                }
            ))
            .with_labels(span.copied().map(|s| s.label("Cannot call impure function")));
        effects.push(AliasingEffect::Impure { place: receiver.clone(), error: diagnostic });
    }

    // TODO: check signature.known_incompatible and throw (TS line 2351-2370)
    // This requires threading Result through apply_effect/apply_signature.

    // If the function is mutable only if operands are mutable, and all
    // arguments are immutable/non-mutating, short-circuit with simple aliasing.
    if signature.mutable_only_if_operands_are_mutable
        && are_arguments_immutable_and_non_mutating(state, args, env, function_values)
    {
        effects.push(AliasingEffect::Alias { from: receiver.clone(), into: lvalue.clone() });
        for arg in args {
            match arg {
                PlaceOrSpreadOrHole::Hole => continue,
                PlaceOrSpreadOrHole::Place(place)
                | PlaceOrSpreadOrHole::Spread(SpreadPattern { place }) => {
                    effects.push(AliasingEffect::ImmutableCapture {
                        from: place.clone(),
                        into: lvalue.clone(),
                    });
                }
            }
        }
        return effects;
    }

    let mut stores: Vec<Place> = Vec::new();
    let mut captures: Vec<Place> = Vec::new();

    let mut visit = |place: &Place, effect: Effect, effects: &mut Vec<AliasingEffect>| match effect
    {
        Effect::Store => {
            effects.push(AliasingEffect::Mutate { value: place.clone(), reason: None });
            stores.push(place.clone());
        }
        Effect::Capture => {
            captures.push(place.clone());
        }
        Effect::ConditionallyMutate => {
            effects.push(AliasingEffect::MutateTransitiveConditionally { value: place.clone() });
        }
        Effect::ConditionallyMutateIterator => {
            let ty = &env.types[env.identifiers[place.identifier].type_];
            if let Some(mutate_iter) = conditionally_mutate_iterator(place, ty) {
                effects.push(mutate_iter);
            }
            effects.push(AliasingEffect::Capture { from: place.clone(), into: lvalue.clone() });
        }
        Effect::Freeze => {
            effects
                .push(AliasingEffect::Freeze { value: place.clone(), reason: return_value_reason });
        }
        Effect::Mutate => {
            effects.push(AliasingEffect::MutateTransitive { value: place.clone() });
        }
        Effect::Read => {
            effects.push(AliasingEffect::ImmutableCapture {
                from: place.clone(),
                into: lvalue.clone(),
            });
        }
        _ => {}
    };

    if signature.callee_effect != Effect::Capture {
        effects.push(AliasingEffect::Alias { from: receiver.clone(), into: lvalue.clone() });
    }

    visit(receiver, signature.callee_effect, &mut effects);
    for (i, arg) in args.iter().enumerate() {
        match arg {
            PlaceOrSpreadOrHole::Hole => continue,
            PlaceOrSpreadOrHole::Place(place)
            | PlaceOrSpreadOrHole::Spread(SpreadPattern { place }) => {
                let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_));
                let sig_effect = if !is_spread && i < signature.positional_params.len() {
                    signature.positional_params[i]
                } else {
                    signature.rest_param.unwrap_or(Effect::ConditionallyMutate)
                };
                let (effect, err_detail) = get_argument_effect(sig_effect, is_spread, place.span);
                if let Some(d) = err_detail {
                    todo_errors.push(d);
                }
                visit(place, effect, &mut effects);
            }
        }
    }

    if !captures.is_empty() {
        if stores.is_empty() {
            for capture in &captures {
                effects.push(AliasingEffect::Alias { from: capture.clone(), into: lvalue.clone() });
            }
        } else {
            for capture in &captures {
                for store in &stores {
                    effects.push(AliasingEffect::Capture {
                        from: capture.clone(),
                        into: store.clone(),
                    });
                }
            }
        }
    }

    effects
}

fn get_argument_effect(
    sig_effect: Effect,
    is_spread: bool,
    spread_span: Option<Span>,
) -> (Effect, Option<OxcDiagnostic>) {
    if !is_spread || sig_effect == Effect::Mutate || sig_effect == Effect::ConditionallyMutate {
        (sig_effect, None)
    } else {
        // Spread with Freeze effect is unsupported for hook arguments
        // (matches TS CompilerError.throwTodo)
        let detail = if sig_effect == Effect::Freeze {
            Some(
                ErrorCategory::Todo
                    .diagnostic("Support spread syntax for hook arguments")
                    .with_labels(spread_span),
            )
        } else {
            None
        };
        (Effect::ConditionallyMutateIterator, detail)
    }
}

/// Returns true if all of the arguments are both non-mutable (immutable or frozen)
/// _and_ are not functions which might mutate their arguments.
///
/// Corresponds to TS `areArgumentsImmutableAndNonMutating`.
fn are_arguments_immutable_and_non_mutating(
    state: &InferenceState,
    args: &[PlaceOrSpreadOrHole],
    env: &Environment,
    function_values: &FxHashMap<ValueId, FunctionId>,
) -> bool {
    for arg in args {
        match arg {
            PlaceOrSpreadOrHole::Hole => continue,
            PlaceOrSpreadOrHole::Place(place)
            | PlaceOrSpreadOrHole::Spread(SpreadPattern { place }) => {
                // Check if it's a function type with a known signature
                let is_place = matches!(arg, PlaceOrSpreadOrHole::Place(_));
                if is_place {
                    let ty = &env.types[env.identifiers[place.identifier].type_];
                    if let Type::Function { .. } = ty {
                        let fn_shape = env.get_function_signature(ty).ok().flatten();
                        if let Some(fn_sig) = fn_shape {
                            let has_mutable_param = fn_sig
                                .positional_params
                                .iter()
                                .any(|e| is_known_mutable_effect(*e));
                            let has_mutable_rest =
                                fn_sig.rest_param.is_some_and(is_known_mutable_effect);
                            return !has_mutable_param && !has_mutable_rest;
                        }
                    }
                }

                let kind = state.kind(place.identifier);
                match kind.kind {
                    ValueKind::Primitive | ValueKind::Frozen => {
                        // Immutable values are ok, continue checking
                    }
                    _ => {
                        return false;
                    }
                }

                // Check if any value for this place is a function expression
                // that mutates its parameters (TS lines 2545-2557)
                let value_ids = state.values_for(place.identifier);
                for vid in &value_ids {
                    if let Some(&func_id) = function_values.get(vid) {
                        let inner_func = &env.functions[func_id];
                        let mutates_params = inner_func.params.iter().any(|param| {
                            let param_id = match param {
                                ParamPattern::Place(p) => p.identifier,
                                ParamPattern::Spread(s) => s.place.identifier,
                            };
                            let ident = &env.identifiers[param_id];
                            ident.mutable_range.end > ident.mutable_range.start + 1
                        });
                        if mutates_params {
                            return false;
                        }
                    }
                }
            }
        }
    }
    true
}

fn is_known_mutable_effect(effect: Effect) -> bool {
    matches!(
        effect,
        Effect::Store
            | Effect::Mutate
            | Effect::ConditionallyMutate
            | Effect::ConditionallyMutateIterator
    )
}

// =============================================================================
// Aliasing signature config support (new-style signatures)
// =============================================================================

#[allow(clippy::too_many_arguments)]
fn compute_effects_for_aliasing_signature_config(
    env: &mut Environment,
    config: &AliasingSignatureConfig,
    lvalue: &Place,
    receiver: &Place,
    args: &[PlaceOrSpreadOrHole],
    context: &[Place],
    span: Option<&Span>,
    temp_cache: &mut FxHashMap<(IdentifierId, String), Place>,
) -> Result<Option<Vec<AliasingEffect>>, OxcDiagnostic> {
    // Build substitutions from config strings to places
    let mut substitutions: FxHashMap<String, Vec<Place>> = FxHashMap::default();
    substitutions.insert(config.receiver.to_string(), vec![receiver.clone()]);
    substitutions.insert(config.returns.to_string(), vec![lvalue.clone()]);

    let mut mutable_spreads: FxHashSet<IdentifierId> = FxHashSet::default();

    for (i, arg) in args.iter().enumerate() {
        match arg {
            PlaceOrSpreadOrHole::Hole => continue,
            PlaceOrSpreadOrHole::Place(place)
            | PlaceOrSpreadOrHole::Spread(SpreadPattern { place }) => {
                if i < config.params.len() && !matches!(arg, PlaceOrSpreadOrHole::Spread(_)) {
                    substitutions.insert(config.params[i].to_string(), vec![place.clone()]);
                } else if let Some(rest) = config.rest {
                    substitutions.entry(rest.to_string()).or_default().push(place.clone());
                } else {
                    return Ok(None);
                }

                if matches!(arg, PlaceOrSpreadOrHole::Spread(_)) {
                    let ty = &env.types[env.identifiers[place.identifier].type_];
                    let mutate_iterator = conditionally_mutate_iterator(place, ty);
                    if mutate_iterator.is_some() {
                        mutable_spreads.insert(place.identifier);
                    }
                }
            }
        }
    }

    for operand in context {
        let ident = &env.identifiers[operand.identifier];
        if let Some(ref name) = ident.name {
            substitutions.insert(format!("@{}", name.value()), vec![operand.clone()]);
        }
    }

    // Create temporaries (cached by lvalue + temp_name to be stable across fixpoint iterations)
    for temp_name in config.temporaries {
        let cache_key = (lvalue.identifier, temp_name.to_string());
        let temp_place = temp_cache
            .entry(cache_key)
            .or_insert_with(|| create_temp_place(env, receiver.span))
            .clone();
        substitutions.insert(temp_name.to_string(), vec![temp_place]);
    }

    let mut effects: Vec<AliasingEffect> = Vec::new();

    for eff_config in config.effects {
        match eff_config {
            AliasingEffectConfig::Freeze { value, reason } => {
                let values = substitutions.get(*value).cloned().unwrap_or_default();
                for v in values {
                    if mutable_spreads.contains(&v.identifier) {
                        return Err(ErrorCategory::Todo
                            .diagnostic("Support spread syntax for hook arguments")
                            .with_labels(v.span));
                    }
                    effects.push(AliasingEffect::Freeze { value: v, reason: *reason });
                }
            }
            AliasingEffectConfig::Create { into, value, reason } => {
                let intos = substitutions.get(*into).cloned().unwrap_or_default();
                for v in intos {
                    effects.push(AliasingEffect::Create {
                        into: v,
                        value: *value,
                        reason: *reason,
                    });
                }
            }
            AliasingEffectConfig::CreateFrom { from, into } => {
                let froms = substitutions.get(*from).cloned().unwrap_or_default();
                let intos = substitutions.get(*into).cloned().unwrap_or_default();
                for f in &froms {
                    for t in &intos {
                        effects
                            .push(AliasingEffect::CreateFrom { from: f.clone(), into: t.clone() });
                    }
                }
            }
            AliasingEffectConfig::Assign { from, into } => {
                let froms = substitutions.get(*from).cloned().unwrap_or_default();
                let intos = substitutions.get(*into).cloned().unwrap_or_default();
                for f in &froms {
                    for t in &intos {
                        effects.push(AliasingEffect::Assign { from: f.clone(), into: t.clone() });
                    }
                }
            }
            AliasingEffectConfig::Alias { from, into } => {
                let froms = substitutions.get(*from).cloned().unwrap_or_default();
                let intos = substitutions.get(*into).cloned().unwrap_or_default();
                for f in &froms {
                    for t in &intos {
                        effects.push(AliasingEffect::Alias { from: f.clone(), into: t.clone() });
                    }
                }
            }
            AliasingEffectConfig::Capture { from, into } => {
                let froms = substitutions.get(*from).cloned().unwrap_or_default();
                let intos = substitutions.get(*into).cloned().unwrap_or_default();
                for f in &froms {
                    for t in &intos {
                        effects.push(AliasingEffect::Capture { from: f.clone(), into: t.clone() });
                    }
                }
            }
            AliasingEffectConfig::ImmutableCapture { from, into } => {
                let froms = substitutions.get(*from).cloned().unwrap_or_default();
                let intos = substitutions.get(*into).cloned().unwrap_or_default();
                for f in &froms {
                    for t in &intos {
                        effects.push(AliasingEffect::ImmutableCapture {
                            from: f.clone(),
                            into: t.clone(),
                        });
                    }
                }
            }
            AliasingEffectConfig::Impure { place } => {
                let values = substitutions.get(*place).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::Impure {
                        place: v,
                        error: ErrorCategory::Purity.diagnostic("Impure function call"),
                    });
                }
            }
            AliasingEffectConfig::Mutate { value } => {
                let values = substitutions.get(*value).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::Mutate { value: v, reason: None });
                }
            }
            AliasingEffectConfig::MutateTransitiveConditionally { value } => {
                let values = substitutions.get(*value).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::MutateTransitiveConditionally { value: v });
                }
            }
            AliasingEffectConfig::Apply {
                receiver: r,
                function: f,
                mutates_function,
                args: a,
                into: i,
            } => {
                let recv = substitutions.get(*r).and_then(|v| v.first()).cloned();
                let func = substitutions.get(*f).and_then(|v| v.first()).cloned();
                let into = substitutions.get(*i).and_then(|v| v.first()).cloned();
                if let (Some(recv), Some(func), Some(into)) = (recv, func, into) {
                    let mut apply_args: Vec<PlaceOrSpreadOrHole> = Vec::new();
                    for arg in *a {
                        match arg {
                            ApplyArgConfig::Hole { .. } => {
                                apply_args.push(PlaceOrSpreadOrHole::Hole);
                            }
                            ApplyArgConfig::Place(name) => {
                                if let Some(places) = substitutions.get(*name)
                                    && let Some(p) = places.first()
                                {
                                    apply_args.push(PlaceOrSpreadOrHole::Place(p.clone()));
                                }
                            }
                            ApplyArgConfig::Spread { place: name, .. } => {
                                if let Some(places) = substitutions.get(*name)
                                    && let Some(p) = places.first()
                                {
                                    apply_args.push(PlaceOrSpreadOrHole::Spread(SpreadPattern {
                                        place: p.clone(),
                                    }));
                                }
                            }
                        }
                    }
                    effects.push(AliasingEffect::Apply {
                        receiver: recv,
                        function: func,
                        mutates_function: *mutates_function,
                        args: apply_args,
                        into,
                        signature: None,
                        span: span.copied(),
                    });
                } else {
                    return Ok(None);
                }
            }
        }
    }

    Ok(Some(effects))
}

// =============================================================================
// Function expression signature building
// =============================================================================

/// Build an AliasingSignature from a function expression's params/returns/aliasing effects.
/// Corresponds to TS `buildSignatureFromFunctionExpression`.
fn build_signature_from_function_expression(
    env: &mut Environment,
    func_id: FunctionId,
) -> AliasingSignature {
    let inner_func = &env.functions[func_id];
    let mut params: Vec<IdentifierId> = Vec::new();
    let mut rest: Option<IdentifierId> = None;
    for param in &inner_func.params {
        match param {
            ParamPattern::Place(p) => params.push(p.identifier),
            ParamPattern::Spread(s) => rest = Some(s.place.identifier),
        }
    }
    let returns = inner_func.returns.identifier;
    let aliasing_effects = inner_func.aliasing_effects.clone().unwrap_or_default();
    let span = inner_func.span;

    if rest.is_none() {
        let temp = create_temp_place(env, span);
        rest = Some(temp.identifier);
    }

    AliasingSignature {
        receiver: IdentifierId::from_usize(0),
        params,
        rest,
        returns,
        effects: aliasing_effects,
        temporaries: Vec::new(),
    }
}

/// Compute effects by substituting an AliasingSignature (IdentifierId-based)
/// with actual arguments. Corresponds to TS `computeEffectsForSignature`.
fn compute_effects_for_aliasing_signature(
    env: &mut Environment,
    signature: &AliasingSignature,
    lvalue: &Place,
    receiver: &Place,
    args: &[PlaceOrSpreadOrHole],
    context: &[Place],
    span: Option<&Span>,
) -> Result<Option<Vec<AliasingEffect>>, OxcDiagnostic> {
    if signature.params.len() > args.len()
        || (args.len() > signature.params.len() && signature.rest.is_none())
    {
        return Ok(None);
    }

    let mut mutable_spreads: FxHashSet<IdentifierId> = FxHashSet::default();
    let mut substitutions: FxHashMap<IdentifierId, Vec<Place>> = FxHashMap::default();
    substitutions.insert(signature.receiver, vec![receiver.clone()]);
    substitutions.insert(signature.returns, vec![lvalue.clone()]);

    for (i, arg) in args.iter().enumerate() {
        match arg {
            PlaceOrSpreadOrHole::Hole => continue,
            PlaceOrSpreadOrHole::Place(place)
            | PlaceOrSpreadOrHole::Spread(SpreadPattern { place }) => {
                let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_));
                if !is_spread && i < signature.params.len() {
                    substitutions.insert(signature.params[i], vec![place.clone()]);
                } else if let Some(rest_id) = signature.rest {
                    substitutions.entry(rest_id).or_default().push(place.clone());
                } else {
                    return Ok(None);
                }

                if is_spread {
                    let ty = &env.types[env.identifiers[place.identifier].type_];
                    let mutate_iterator = conditionally_mutate_iterator(place, ty);
                    if mutate_iterator.is_some() {
                        mutable_spreads.insert(place.identifier);
                    }
                }
            }
        }
    }

    // Add context variable substitutions (identity mapping)
    for operand in context {
        substitutions.insert(operand.identifier, vec![operand.clone()]);
    }

    // Create temporaries
    for temp in &signature.temporaries {
        let temp_place = create_temp_place(env, receiver.span);
        substitutions.insert(temp.identifier, vec![temp_place]);
    }

    let mut effects: Vec<AliasingEffect> = Vec::new();

    for eff in &signature.effects {
        match eff {
            AliasingEffect::MaybeAlias { from, into }
            | AliasingEffect::Assign { from, into }
            | AliasingEffect::ImmutableCapture { from, into }
            | AliasingEffect::Alias { from, into }
            | AliasingEffect::CreateFrom { from, into }
            | AliasingEffect::Capture { from, into } => {
                let from_places = substitutions.get(&from.identifier).cloned().unwrap_or_default();
                let to_places = substitutions.get(&into.identifier).cloned().unwrap_or_default();
                for f in &from_places {
                    for t in &to_places {
                        effects.push(match eff {
                            AliasingEffect::MaybeAlias { .. } => {
                                AliasingEffect::MaybeAlias { from: f.clone(), into: t.clone() }
                            }
                            AliasingEffect::Assign { .. } => {
                                AliasingEffect::Assign { from: f.clone(), into: t.clone() }
                            }
                            AliasingEffect::ImmutableCapture { .. } => {
                                AliasingEffect::ImmutableCapture {
                                    from: f.clone(),
                                    into: t.clone(),
                                }
                            }
                            AliasingEffect::Alias { .. } => {
                                AliasingEffect::Alias { from: f.clone(), into: t.clone() }
                            }
                            AliasingEffect::CreateFrom { .. } => {
                                AliasingEffect::CreateFrom { from: f.clone(), into: t.clone() }
                            }
                            AliasingEffect::Capture { .. } => {
                                AliasingEffect::Capture { from: f.clone(), into: t.clone() }
                            }
                            _ => unreachable!(),
                        });
                    }
                }
            }
            AliasingEffect::Impure { place, error } => {
                let values = substitutions.get(&place.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::Impure { place: v, error: error.clone() });
                }
            }
            AliasingEffect::MutateFrozen { place, error } => {
                let values = substitutions.get(&place.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::MutateFrozen { place: v, error: error.clone() });
                }
            }
            AliasingEffect::MutateGlobal { place, error } => {
                let values = substitutions.get(&place.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::MutateGlobal { place: v, error: error.clone() });
                }
            }
            AliasingEffect::Render { place } => {
                let values = substitutions.get(&place.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::Render { place: v });
                }
            }
            AliasingEffect::Mutate { value, reason } => {
                let values = substitutions.get(&value.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::Mutate { value: v, reason: reason.clone() });
                }
            }
            AliasingEffect::MutateConditionally { value } => {
                let values = substitutions.get(&value.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::MutateConditionally { value: v });
                }
            }
            AliasingEffect::MutateTransitive { value } => {
                let values = substitutions.get(&value.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::MutateTransitive { value: v });
                }
            }
            AliasingEffect::MutateTransitiveConditionally { value } => {
                let values = substitutions.get(&value.identifier).cloned().unwrap_or_default();
                for v in values {
                    effects.push(AliasingEffect::MutateTransitiveConditionally { value: v });
                }
            }
            AliasingEffect::Freeze { value, reason } => {
                let values = substitutions.get(&value.identifier).cloned().unwrap_or_default();
                for v in values {
                    if mutable_spreads.contains(&v.identifier) {
                        return Err(ErrorCategory::Todo
                            .diagnostic("Support spread syntax for hook arguments")
                            .with_labels(v.span));
                    }
                    effects.push(AliasingEffect::Freeze { value: v, reason: *reason });
                }
            }
            AliasingEffect::Create { into, value, reason } => {
                let intos = substitutions.get(&into.identifier).cloned().unwrap_or_default();
                for v in intos {
                    effects.push(AliasingEffect::Create {
                        into: v,
                        value: *value,
                        reason: *reason,
                    });
                }
            }
            AliasingEffect::Apply {
                receiver: r,
                function: f,
                mutates_function: mf,
                args: a,
                into: i,
                signature: s,
                span: _l,
            } => {
                let recv = substitutions.get(&r.identifier).and_then(|v| v.first()).cloned();
                let func = substitutions.get(&f.identifier).and_then(|v| v.first()).cloned();
                let apply_into = substitutions.get(&i.identifier).and_then(|v| v.first()).cloned();
                if let (Some(recv), Some(func), Some(apply_into)) = (recv, func, apply_into) {
                    let mut apply_args: Vec<PlaceOrSpreadOrHole> = Vec::new();
                    for arg in a {
                        match arg {
                            PlaceOrSpreadOrHole::Hole => apply_args.push(PlaceOrSpreadOrHole::Hole),
                            PlaceOrSpreadOrHole::Place(p) => {
                                if let Some(places) = substitutions.get(&p.identifier)
                                    && let Some(place) = places.first()
                                {
                                    apply_args.push(PlaceOrSpreadOrHole::Place(place.clone()));
                                }
                            }
                            PlaceOrSpreadOrHole::Spread(sp) => {
                                if let Some(places) = substitutions.get(&sp.place.identifier)
                                    && let Some(place) = places.first()
                                {
                                    apply_args.push(PlaceOrSpreadOrHole::Spread(SpreadPattern {
                                        place: place.clone(),
                                    }));
                                }
                            }
                        }
                    }
                    effects.push(AliasingEffect::Apply {
                        receiver: recv,
                        function: func,
                        mutates_function: *mf,
                        args: apply_args,
                        into: apply_into,
                        signature: s.clone(),
                        span: span.copied(),
                    });
                } else {
                    return Ok(None);
                }
            }
            AliasingEffect::CreateFunction { .. } => {
                // Not supported in signature substitution
                return Ok(None);
            }
        }
    }

    Ok(Some(effects))
}

// =============================================================================
// Helpers
// =============================================================================

/// Select the primary (most specific) reason from a set of reasons.
/// TS uses `[...set][0]` which returns the first-inserted element;
/// since the primary reason is always inserted first, this effectively
/// picks the most specific non-Other reason. We replicate this by
/// preferring any non-Other reason over Other.
fn primary_reason(reasons: &ReasonSet) -> ValueReason {
    for r in reasons.iter() {
        if r != ValueReason::Other {
            return r;
        }
    }
    ValueReason::Other
}

fn get_write_error_reason(abstract_value: &AbstractValue) -> String {
    if abstract_value.reason.contains(&ValueReason::Global) {
        "Modifying a variable defined outside a component or hook is not allowed. Consider using an effect".to_string()
    } else if abstract_value.reason.contains(&ValueReason::JsxCaptured) {
        "Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX".to_string()
    } else if abstract_value.reason.contains(&ValueReason::Context) {
        "Modifying a value returned from 'useContext()' is not allowed.".to_string()
    } else if abstract_value.reason.contains(&ValueReason::KnownReturnSignature) {
        "Modifying a value returned from a function whose return value should not be mutated"
            .to_string()
    } else if abstract_value.reason.contains(&ValueReason::ReactiveFunctionArgument) {
        "Modifying component props or hook arguments is not allowed. Consider using a local variable instead".to_string()
    } else if abstract_value.reason.contains(&ValueReason::State) {
        "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead".to_string()
    } else if abstract_value.reason.contains(&ValueReason::ReducerState) {
        "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead".to_string()
    } else if abstract_value.reason.contains(&ValueReason::Effect) {
        "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()
    } else if abstract_value.reason.contains(&ValueReason::HookCaptured) {
        "Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook".to_string()
    } else if abstract_value.reason.contains(&ValueReason::HookReturn) {
        "Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed".to_string()
    } else {
        "This modifies a variable that React considers immutable".to_string()
    }
}

fn conditionally_mutate_iterator(place: &Place, ty: &Type) -> Option<AliasingEffect> {
    if !is_builtin_collection_type(ty) {
        Some(AliasingEffect::MutateTransitiveConditionally { value: place.clone() })
    } else {
        None
    }
}

fn is_builtin_collection_type(ty: &Type) -> bool {
    matches!(ty, Type::Object { shape_id: Some(id) }
        if *id == BUILT_IN_ARRAY_ID || *id == BUILT_IN_SET_ID || *id == BUILT_IN_MAP_ID
    )
}

fn get_function_call_signature(
    env: &Environment,
    callee_id: IdentifierId,
) -> Result<Option<FunctionSignature>, OxcDiagnostic> {
    let ty = &env.types[env.identifiers[callee_id].type_];
    Ok(env.get_function_signature(ty)?.cloned())
}

fn is_ref_or_ref_value_for_id(env: &Environment, id: IdentifierId) -> bool {
    let ty = &env.types[env.identifiers[id].type_];
    is_ref_or_ref_value(ty)
}

fn get_hook_kind_for_type<'a>(
    env: &'a Environment,
    ty: &Type,
) -> Result<Option<&'a HookKind>, OxcDiagnostic> {
    env.get_hook_kind_for_type(ty)
}

/// Format a Type for printPlace-style output, matching TS's `printType()`.
fn format_type_for_print<'t>(ty: &'t Type) -> Cow<'t, str> {
    match ty {
        Type::Primitive => Cow::Borrowed(""),
        Type::Function { shape_id, return_type, .. } => {
            if let Some(sid) = shape_id {
                let ret = format_type_for_print(return_type);
                if ret.is_empty() {
                    Cow::Owned(format!(":TFunction<{}>()", sid))
                } else {
                    Cow::Owned(format!(":TFunction<{}>():  {}", sid, ret))
                }
            } else {
                Cow::Borrowed(":TFunction")
            }
        }
        Type::Object { shape_id } => {
            if let Some(sid) = shape_id {
                Cow::Owned(format!(":TObject<{}>", sid))
            } else {
                Cow::Borrowed(":TObject")
            }
        }
        Type::Poly => Cow::Borrowed(":TPoly"),
        Type::Phi { .. } => Cow::Borrowed(":TPhi"),
        Type::Property { .. } => Cow::Borrowed(":TProperty"),
        Type::Var { .. } => Cow::Borrowed(""),
        Type::ObjectMethod => Cow::Borrowed(":TObjectMethod"),
    }
}

fn is_phi_with_jsx(ty: &Type) -> bool {
    if let Type::Phi { operands } = ty { operands.iter().any(is_jsx_type) } else { false }
}

fn place_or_spread_to_hole(pos: &PlaceOrSpread) -> PlaceOrSpreadOrHole {
    match pos {
        PlaceOrSpread::Place(p) => PlaceOrSpreadOrHole::Place(p.clone()),
        PlaceOrSpread::Spread(s) => PlaceOrSpreadOrHole::Spread(s.clone()),
    }
}

use crate::react_compiler_hir::JsxTag;

fn build_apply_operands(
    receiver: &Place,
    function: &Place,
    args: &[PlaceOrSpreadOrHole],
) -> Vec<(Place, bool, bool)> {
    let mut result = vec![(receiver.clone(), false, false), (function.clone(), true, false)];
    for arg in args {
        match arg {
            PlaceOrSpreadOrHole::Hole => continue,
            PlaceOrSpreadOrHole::Place(p) => result.push((p.clone(), false, false)),
            PlaceOrSpreadOrHole::Spread(s) => result.push((s.place.clone(), false, true)),
        }
    }
    result
}

fn create_temp_place(env: &mut Environment, span: Option<Span>) -> Place {
    let id = env.next_identifier_id();
    env.identifiers[id].span = span;
    Place { identifier: id, effect: Effect::Unknown, reactive: false, span }
}

// =============================================================================
// Terminal successor helper
// =============================================================================

/// Returns the successor blocks used for traversal in mutation/aliasing inference.
///
/// Matches the TS `eachTerminalSuccessor` which yields standard control-flow
/// successors but NOT pseudo-successors (fallthroughs). Fallthroughs for
/// Logical/Ternary/Optional and Try/Scope/PrunedScope are reached naturally
/// via the block iteration order (blocks are stored in topological order).
fn terminal_successors(terminal: &Terminal) -> Vec<BlockId> {
    match terminal {
        Terminal::Goto { block, .. } => vec![*block],
        Terminal::If { consequent, alternate, .. } => vec![*consequent, *alternate],
        Terminal::Branch { consequent, alternate, .. } => vec![*consequent, *alternate],
        Terminal::Switch { cases, .. } => cases.iter().map(|c| c.block).collect(),
        Terminal::For { init, .. } => vec![*init],
        Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => vec![*init],
        Terminal::DoWhile { loop_block, .. } => vec![*loop_block],
        Terminal::While { test, .. } => vec![*test],
        Terminal::Return { .. } | Terminal::Throw { .. } | Terminal::Unreachable { .. } => vec![],
        Terminal::Try { block, .. } => vec![*block],
        Terminal::MaybeThrow { continuation, handler, .. } => {
            let mut v = vec![*continuation];
            if let Some(h) = handler {
                v.push(*h);
            }
            v
        }
        Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => vec![*block],
        Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => vec![*test],
        Terminal::Optional { test, .. } => vec![*test],
        Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => vec![*block],
    }
}

/// Pattern item helper for Destructure.
///
/// NOTE: This cannot use `visitors::each_pattern_operand` because callers need
/// to distinguish Place from Spread elements — Spread elements get different
/// aliasing effects (Create + Capture) vs Place elements (Create or CreateFrom).
enum PatternItem<'a> {
    Place(&'a Place),
    Spread(&'a Place),
}

fn each_pattern_items<'p>(pattern: &'p Pattern) -> Vec<PatternItem<'p>> {
    let mut items = Vec::new();
    match pattern {
        Pattern::Array(arr) => {
            for el in &arr.items {
                match el {
                    ArrayPatternElement::Place(p) => items.push(PatternItem::Place(p)),
                    ArrayPatternElement::Spread(s) => items.push(PatternItem::Spread(&s.place)),
                    ArrayPatternElement::Hole => {}
                }
            }
        }
        Pattern::Object(obj) => {
            for prop in &obj.properties {
                match prop {
                    ObjectPropertyOrSpread::Property(p) => items.push(PatternItem::Place(&p.place)),
                    ObjectPropertyOrSpread::Spread(s) => items.push(PatternItem::Spread(&s.place)),
                }
            }
        }
    }
    items
}
