1// Copyright (c) Meta Platforms, Inc. and affiliates.2//3// This source code is licensed under the MIT license found in the4// LICENSE file in the root directory of this source tree.56//! Infers the mutation/aliasing effects for instructions and terminals.7//!8//! Ported from TypeScript `src/Inference/InferMutationAliasingEffects.ts`.9//!10//! This pass uses abstract interpretation to compute effects describing11//! creation, aliasing, mutation, freezing, and error conditions for each12//! instruction and terminal in the HIR.1314use indexmap::IndexMap;15use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};1617use react_compiler_diagnostics::CompilerDiagnostic;18use react_compiler_diagnostics::CompilerDiagnosticDetail;19use react_compiler_diagnostics::ErrorCategory;20use react_compiler_hir::AliasingEffect;21use react_compiler_hir::AliasingSignature;22use react_compiler_hir::BlockId;23use react_compiler_hir::DeclarationId;24use react_compiler_hir::Effect;25use react_compiler_hir::FunctionId;26use react_compiler_hir::HirFunction;27use react_compiler_hir::IdentifierId;28use react_compiler_hir::InstructionKind;29use react_compiler_hir::InstructionValue;30use react_compiler_hir::MutationReason;31use react_compiler_hir::ParamPattern;32use react_compiler_hir::Place;33use react_compiler_hir::PlaceOrSpread;34use react_compiler_hir::PlaceOrSpreadOrHole;35use react_compiler_hir::ReactFunctionType;36use react_compiler_hir::SourceLocation;37use react_compiler_hir::Type;38use react_compiler_hir::environment::Environment;39use react_compiler_hir::object_shape::BUILT_IN_ARRAY_ID;40use react_compiler_hir::object_shape::BUILT_IN_MAP_ID;41use react_compiler_hir::object_shape::BUILT_IN_SET_ID;42use react_compiler_hir::object_shape::FunctionSignature;43use react_compiler_hir::object_shape::HookKind;44use react_compiler_hir::type_config::ValueKind;45use react_compiler_hir::type_config::ValueReason;46use react_compiler_hir::visitors;4748// =============================================================================49// Public entry point50// =============================================================================5152/// Infers mutation/aliasing effects for all instructions and terminals in `func`.53///54/// Corresponds to TS `inferMutationAliasingEffects(fn, {isFunctionExpression})`.55pub fn infer_mutation_aliasing_effects(56 func: &mut HirFunction,57 env: &mut Environment,58 is_function_expression: bool,59) -> Result<(), CompilerDiagnostic> {60 let mut initial_state = InferenceState::empty(env, is_function_expression);6162 // Map of blocks to the last (merged) incoming state that was processed63 let mut states_by_block: FxHashMap<BlockId, InferenceState> = FxHashMap::default();6465 // Initialize context variables66 for ctx_place in &func.context {67 let value_id = ValueId::new();68 initial_state.initialize(69 value_id,70 AbstractValue {71 kind: ValueKind::Context,72 reason: ValueReasonSet::single(ValueReason::Other),73 },74 );75 initial_state.define(ctx_place.identifier, value_id);76 }7778 let param_kind: AbstractValue = if is_function_expression {79 AbstractValue {80 kind: ValueKind::Mutable,81 reason: ValueReasonSet::single(ValueReason::Other),82 }83 } else {84 AbstractValue {85 kind: ValueKind::Frozen,86 reason: ValueReasonSet::single(ValueReason::ReactiveFunctionArgument),87 }88 };8990 if func.fn_type == ReactFunctionType::Component {91 // Component: at most 2 params (props, ref)92 let params_len = func.params.len();93 if params_len > 0 {94 infer_param(&func.params[0], &mut initial_state, ¶m_kind);95 }96 if params_len > 1 {97 let ref_place = match &func.params[1] {98 ParamPattern::Place(p) => p,99 ParamPattern::Spread(s) => &s.place,100 };101 let value_id = ValueId::new();102 initial_state.initialize(103 value_id,104 AbstractValue {105 kind: ValueKind::Mutable,106 reason: ValueReasonSet::single(ValueReason::Other),107 },108 );109 initial_state.define(ref_place.identifier, value_id);110 }111 } else {112 for param in &func.params {113 infer_param(param, &mut initial_state, ¶m_kind);114 }115 }116117 let mut queued_states: IndexMap<BlockId, InferenceState, FxBuildHasher> = IndexMap::default();118119 // Queue helper120 fn queue(121 queued_states: &mut IndexMap<BlockId, InferenceState, FxBuildHasher>,122 states_by_block: &FxHashMap<BlockId, InferenceState>,123 block_id: BlockId,124 state: InferenceState,125 ) {126 if let Some(queued_state) = queued_states.get(&block_id) {127 let merged = queued_state.merge(&state);128 let new_state = merged.unwrap_or_else(|| queued_state.clone());129 queued_states.insert(block_id, new_state);130 } else {131 let prev_state = states_by_block.get(&block_id);132 if let Some(prev) = prev_state {133 let next_state = prev.merge(&state);134 if let Some(next) = next_state {135 queued_states.insert(block_id, next);136 }137 } else {138 queued_states.insert(block_id, state);139 }140 }141 }142143 queue(144 &mut queued_states,145 &states_by_block,146 func.body.entry,147 initial_state,148 );149150 let hoisted_context_declarations = find_hoisted_context_declarations(func, env);151 let non_mutating_spreads = find_non_mutated_destructure_spreads(func, env);152153 let mut context = Context {154 interned_effects: FxHashMap::default(),155 instruction_signature_cache: FxHashMap::default(),156 catch_handlers: FxHashMap::default(),157 is_function_expression,158 hoisted_context_declarations,159 non_mutating_spreads,160 effect_value_id_cache: FxHashMap::default(),161 function_values: FxHashMap::default(),162 function_signature_cache: FxHashMap::default(),163 aliasing_config_temp_cache: FxHashMap::default(),164 };165166 let mut iteration_count = 0;167168 while !queued_states.is_empty() {169 iteration_count += 1;170 if iteration_count > 100 {171 return Err(CompilerDiagnostic::new(172 ErrorCategory::Invariant,173 "[InferMutationAliasingEffects] Potential infinite loop: \174 A value, temporary place, or effect was not cached properly",175 None,176 ));177 }178179 // Collect block IDs to process in order180 let block_ids: Vec<BlockId> = func.body.blocks.keys().copied().collect();181 for block_id in block_ids {182 let incoming_state = match queued_states.swap_remove(&block_id) {183 Some(s) => s,184 None => continue,185 };186187 states_by_block.insert(block_id, incoming_state.clone());188 let mut state = incoming_state;189190 infer_block(&mut context, &mut state, block_id, func, env)?;191192 // Check for uninitialized identifier access (matches TS invariant:193 // "Expected value kind to be initialized")194 if let Some((uninitialized_id, usage_loc)) = state.uninitialized_access.get() {195 let ident_info = env.identifiers.get(uninitialized_id.0 as usize);196 let name = ident_info197 .and_then(|ident| ident.name.as_ref())198 .map(|n| n.value().to_string())199 .unwrap_or_else(|| "".to_string());200 // Use usage_loc if available, otherwise fall back to identifier's own loc201 let error_loc = usage_loc.or_else(|| ident_info.and_then(|i| i.loc));202 // Match TS printPlace format: "<unknown> name$id:type"203 let type_str = ident_info204 .map(|ident| {205 let ty = &env.types[ident.type_.0 as usize];206 format_type_for_print(ty)207 })208 .unwrap_or_default();209 let description = format!("<unknown> {}${}{}", name, uninitialized_id.0, type_str);210 let diag = CompilerDiagnostic::new(211 ErrorCategory::Invariant,212 "[InferMutationAliasingEffects] Expected value kind to be initialized",213 Some(description),214 )215 .with_detail(CompilerDiagnosticDetail::Error {216 loc: error_loc,217 message: Some("this is uninitialized".to_string()),218 identifier_name: None,219 });220 return Err(diag);221 }222223 // Queue successors224 let successors = terminal_successors(&func.body.blocks[&block_id].terminal);225 for next_block_id in successors {226 queue(227 &mut queued_states,228 &states_by_block,229 next_block_id,230 state.clone(),231 );232 }233 }234 }235236 Ok(())237}238239// =============================================================================240// ValueId: replaces InstructionValue identity as allocation-site key241// =============================================================================242243/// Unique allocation-site identifier, replacing TS's object-identity on InstructionValue.244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]245struct ValueId(u32);246247use std::sync::atomic::AtomicU32;248use std::sync::atomic::Ordering;249static NEXT_VALUE_ID: AtomicU32 = AtomicU32::new(1);250251impl ValueId {252 fn new() -> Self {253 ValueId(NEXT_VALUE_ID.fetch_add(1, Ordering::Relaxed))254 }255}256257// =============================================================================258// AbstractValue259// =============================================================================260261#[derive(Debug, Clone, Copy)]262struct AbstractValue {263 kind: ValueKind,264 reason: ValueReasonSet,265}266267/// Capacity of [`ValueReasonSet`]. A set holds at most one of each `ValueReason`268/// variant, of which there are currently 12; the extra slots are headroom so269/// that adding variants upstream cannot overflow the set.270const VALUE_REASON_CAPACITY: usize = 16;271272/// An insertion-ordered set of [`ValueReason`]s, stored inline.273///274/// This is a deliberate replacement for `IndexSet`, enabling insertion-order275/// memory while avoiding any heap allocation. At `AbstractValue`'s scale, this276/// has a dramatic impact on heap memory and wall time.277/// This takes advantage of the format of the data it's actually storing. A set278/// can hold at most one of each variant, so the members fit into a fixed inline279/// array. `ValueReason` is implemented as a single byte, so this struct is280/// ~18 bytes on the stack.281///282/// Insertion order is preserved deliberately: [`primary_reason`] returns the283/// first non-`Other` member, matching the iteration order of the `Set` used by284/// the TypeScript implementation this is ported from.285#[derive(Debug, Clone, Copy)]286struct ValueReasonSet {287 /// Members in insertion order. Only the first `len` entries are meaningful.288 members: [ValueReason; VALUE_REASON_CAPACITY],289 len: u8,290}291292impl Default for ValueReasonSet {293 fn default() -> Self {294 ValueReasonSet {295 members: [ValueReason::Other; VALUE_REASON_CAPACITY],296 len: 0,297 }298 }299}300301impl ValueReasonSet {302 fn single(reason: ValueReason) -> Self {303 let mut set = Self::default();304 set.insert(reason);305 set306 }307308 fn contains(&self, reason: ValueReason) -> bool {309 self.members[..self.len as usize].contains(&reason)310 }311312 fn iter(&self) -> impl Iterator<Item = ValueReason> + '_ {313 self.members[..self.len as usize].iter().copied()314 }315316 /// Appends `reason` if not already present, preserving insertion order.317 fn insert(&mut self, reason: ValueReason) {318 if self.contains(reason) {319 return;320 }321 debug_assert!(322 (self.len as usize) < VALUE_REASON_CAPACITY,323 "ValueReasonSet capacity must cover every ValueReason variant"324 );325 if (self.len as usize) < VALUE_REASON_CAPACITY {326 self.members[self.len as usize] = reason;327 self.len += 1;328 }329 }330331 /// True when every member of `other` is also a member of `self`.332 fn is_superset_of(&self, other: &ValueReasonSet) -> bool {333 other.iter().all(|reason| self.contains(reason))334 }335336 /// Adds every member of `other`, keeping `self`'s existing order and337 /// appending newcomers in `other`'s order — matching `IndexSet::insert`.338 fn union_with(&mut self, other: &ValueReasonSet) {339 for reason in other.iter() {340 self.insert(reason);341 }342 }343}344345// =============================================================================346// InferenceState347// =============================================================================348349/// The abstract state tracked during inference.350/// Uses interior mutability via a struct with direct fields (no Rc needed since351/// we always have exclusive access in the pass).352#[derive(Debug, Clone)]353struct InferenceState {354 is_function_expression: bool,355 /// The kind of each value, based on its allocation site356 values: FxHashMap<ValueId, AbstractValue>,357 /// The set of values pointed to by each identifier358 variables: FxHashMap<IdentifierId, FxHashSet<ValueId>>,359 /// Tracks uninitialized identifier access errors (matches TS invariant).360 /// Uses Cell so it can be set from `&self` methods like `kind()`.361 /// Stores (IdentifierId, usage_loc) where usage_loc is the source location362 /// of the Place that triggered the uninitialized access.363 uninitialized_access: std::cell::Cell<Option<(IdentifierId, Option<SourceLocation>)>>,364}365366impl InferenceState {367 fn empty(_env: &Environment, is_function_expression: bool) -> Self {368 InferenceState {369 is_function_expression,370 values: FxHashMap::default(),371 variables: FxHashMap::default(),372 uninitialized_access: std::cell::Cell::new(None),373 }374 }375376 /// Check the kind of a place, recording the usage location for error reporting.377 fn kind_with_loc(378 &self,379 place_id: IdentifierId,380 usage_loc: Option<SourceLocation>,381 ) -> AbstractValue {382 let values = match self.variables.get(&place_id) {383 Some(v) => v,384 None => {385 if self.uninitialized_access.get().is_none() {386 self.uninitialized_access.set(Some((place_id, usage_loc)));387 }388 return AbstractValue {389 kind: ValueKind::Mutable,390 reason: ValueReasonSet::single(ValueReason::Other),391 };392 }393 };394 let mut merged_kind: Option<AbstractValue> = None;395 for value_id in values {396 let kind = match self.values.get(value_id) {397 Some(k) => k,398 None => continue,399 };400 merged_kind = Some(match merged_kind {401 Some(prev) => merge_abstract_values(&prev, kind),402 None => kind.clone(),403 });404 }405 merged_kind.unwrap_or_else(|| AbstractValue {406 kind: ValueKind::Mutable,407 reason: ValueReasonSet::single(ValueReason::Other),408 })409 }410411 fn initialize(&mut self, value_id: ValueId, kind: AbstractValue) {412 self.values.insert(value_id, kind);413 }414415 fn define(&mut self, place_id: IdentifierId, value_id: ValueId) {416 let mut set = FxHashSet::default();417 set.insert(value_id);418 self.variables.insert(place_id, set);419 }420421 fn assign(&mut self, into: IdentifierId, from: IdentifierId) {422 let values = match self.variables.get(&from) {423 Some(v) => v.clone(),424 None => {425 // Create a stable value for uninitialized identifiers426 // Use a deterministic ID based on the from identifier427 let vid = ValueId(from.0 | 0x80000000);428 let mut set = FxHashSet::default();429 set.insert(vid);430 if !self.values.contains_key(&vid) {431 self.values.insert(432 vid,433 AbstractValue {434 kind: ValueKind::Mutable,435 reason: ValueReasonSet::single(ValueReason::Other),436 },437 );438 }439 set440 }441 };442 self.variables.insert(into, values);443 }444445 fn append_alias(&mut self, place: IdentifierId, value: IdentifierId) {446 let new_values = match self.variables.get(&value) {447 Some(v) => v.clone(),448 None => return,449 };450 let prev_values = match self.variables.get(&place) {451 Some(v) => v.clone(),452 None => return,453 };454 let merged: FxHashSet<ValueId> = prev_values.union(&new_values).copied().collect();455 self.variables.insert(place, merged);456 }457458 fn is_defined(&self, place_id: IdentifierId) -> bool {459 self.variables.contains_key(&place_id)460 }461462 fn values_for(&self, place_id: IdentifierId) -> Vec<ValueId> {463 match self.variables.get(&place_id) {464 Some(values) => values.iter().copied().collect(),465 None => Vec::new(),466 }467 }468469 #[allow(dead_code)]470 fn kind_opt(&self, place_id: IdentifierId) -> Option<AbstractValue> {471 let values = self.variables.get(&place_id)?;472 let mut merged_kind: Option<AbstractValue> = None;473 for value_id in values {474 let kind = self.values.get(value_id)?;475 merged_kind = Some(match merged_kind {476 Some(prev) => merge_abstract_values(&prev, kind),477 None => kind.clone(),478 });479 }480 merged_kind481 }482483 fn kind(&self, place_id: IdentifierId) -> AbstractValue {484 self.kind_with_loc(place_id, None)485 }486487 fn freeze(&mut self, place_id: IdentifierId, reason: ValueReason) -> bool {488 // Check if defined first to avoid recording uninitialized access error.489 // Freeze on undefined identifiers is a no-op — this matches the TS490 // behavior where freeze() is never called on undefined identifiers491 // (the invariant in kind() catches this before freeze is reached).492 if !self.variables.contains_key(&place_id) {493 return false;494 }495 let value = self.kind(place_id);496 match value.kind {497 ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => {498 let value_ids: Vec<ValueId> = self.values_for(place_id);499 for vid in value_ids {500 self.freeze_value(vid, reason);501 }502 true503 }504 ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => false,505 }506 }507508 fn freeze_value(&mut self, value_id: ValueId, reason: ValueReason) {509 self.values.insert(510 value_id,511 AbstractValue {512 kind: ValueKind::Frozen,513 reason: ValueReasonSet::single(reason),514 },515 );516 // Note: In TS, this also transitively freezes FunctionExpression captures517 // if enableTransitivelyFreezeFunctionExpressions is set. We skip that here518 // since we don't have access to the function arena from within state.519 }520521 #[allow(dead_code)]522 fn mutate(523 &self,524 variant: MutateVariant,525 place_id: IdentifierId,526 env: &Environment,527 ) -> MutationResult {528 self.mutate_with_loc(variant, place_id, env, None)529 }530531 fn mutate_with_loc(532 &self,533 variant: MutateVariant,534 place_id: IdentifierId,535 env: &Environment,536 usage_loc: Option<SourceLocation>,537 ) -> MutationResult {538 let ty = &env.types[env.identifiers[place_id.0 as usize].type_.0 as usize];539 if react_compiler_hir::is_ref_or_ref_value(ty) {540 return MutationResult::MutateRef;541 }542 let kind = self.kind_with_loc(place_id, usage_loc).kind;543 match variant {544 MutateVariant::MutateConditionally | MutateVariant::MutateTransitiveConditionally => {545 match kind {546 ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate,547 _ => MutationResult::None,548 }549 }550 MutateVariant::Mutate | MutateVariant::MutateTransitive => match kind {551 ValueKind::Mutable | ValueKind::Context => MutationResult::Mutate,552 ValueKind::Primitive => MutationResult::None,553 ValueKind::Frozen | ValueKind::MaybeFrozen => MutationResult::MutateFrozen,554 ValueKind::Global => MutationResult::MutateGlobal,555 },556 }557 }558559 fn merge(&self, other: &InferenceState) -> Option<InferenceState> {560 let mut next_values: Option<FxHashMap<ValueId, AbstractValue>> = None;561 let mut next_variables: Option<FxHashMap<IdentifierId, FxHashSet<ValueId>>> = None;562563 // Merge values present in both564 for (id, this_value) in &self.values {565 if let Some(other_value) = other.values.get(id) {566 let merged = merge_abstract_values(this_value, other_value);567 if merged.kind != this_value.kind568 || !this_value.reason.is_superset_of(&merged.reason)569 {570 let nv = next_values.get_or_insert_with(|| self.values.clone());571 nv.insert(*id, merged);572 }573 }574 }575 // Add values only in other576 for (id, other_value) in &other.values {577 if !self.values.contains_key(id) {578 let nv = next_values.get_or_insert_with(|| self.values.clone());579 nv.insert(*id, other_value.clone());580 }581 }582583 // Merge variables present in both584 for (id, this_values) in &self.variables {585 if let Some(other_values) = other.variables.get(id) {586 let mut has_new = false;587 for ov in other_values {588 if !this_values.contains(ov) {589 has_new = true;590 break;591 }592 }593 if has_new {594 let nvars = next_variables.get_or_insert_with(|| self.variables.clone());595 let merged: FxHashSet<ValueId> =596 this_values.union(other_values).copied().collect();597 nvars.insert(*id, merged);598 }599 }600 }601 // Add variables only in other602 for (id, other_values) in &other.variables {603 if !self.variables.contains_key(id) {604 let nvars = next_variables.get_or_insert_with(|| self.variables.clone());605 nvars.insert(*id, other_values.clone());606 }607 }608609 if next_variables.is_none() && next_values.is_none() {610 None611 } else {612 Some(InferenceState {613 is_function_expression: self.is_function_expression,614 values: next_values.unwrap_or_else(|| self.values.clone()),615 variables: next_variables.unwrap_or_else(|| self.variables.clone()),616 uninitialized_access: std::cell::Cell::new(None),617 })618 }619 }620621 fn infer_phi(622 &mut self,623 phi_place_id: IdentifierId,624 phi_operands: &IndexMap<BlockId, Place, FxBuildHasher>,625 ) {626 let mut values: FxHashSet<ValueId> = FxHashSet::default();627 for (_, operand) in phi_operands {628 if let Some(operand_values) = self.variables.get(&operand.identifier) {629 for v in operand_values {630 values.insert(*v);631 }632 }633 // If not found, it's a backedge that will be handled later by merge634 }635 if !values.is_empty() {636 self.variables.insert(phi_place_id, values);637 }638 }639}640641#[derive(Debug, Clone, Copy)]642enum MutateVariant {643 Mutate,644 MutateConditionally,645 MutateTransitive,646 MutateTransitiveConditionally,647}648649#[derive(Debug, Clone, Copy, PartialEq, Eq)]650enum MutationResult {651 None,652 Mutate,653 MutateFrozen,654 MutateGlobal,655 MutateRef,656}657658// =============================================================================659// Context660// =============================================================================661662struct Context {663 interned_effects: FxHashMap<String, AliasingEffect>,664 instruction_signature_cache: FxHashMap<u32, InstructionSignature>,665 catch_handlers: FxHashMap<BlockId, Place>,666 is_function_expression: bool,667 hoisted_context_declarations: FxHashMap<DeclarationId, Option<Place>>,668 non_mutating_spreads: FxHashSet<IdentifierId>,669 /// Cache of ValueIds keyed by effect hash, ensuring stable allocation-site identity670 /// across fixpoint iterations. Mirrors TS `effectInstructionValueCache`.671 effect_value_id_cache: FxHashMap<String, ValueId>,672 /// Maps ValueId to FunctionId for function expressions, so we can look up673 /// locally-declared functions when processing Apply effects.674 function_values: FxHashMap<ValueId, FunctionId>,675 /// Cache of function expression signatures, keyed by FunctionId676 function_signature_cache: FxHashMap<FunctionId, AliasingSignature>,677 /// Cache of temporary places created for aliasing signature config temporaries.678 /// Keyed by (lvalue_identifier_id, temp_name) to ensure stable allocation679 /// across fixpoint iterations.680 aliasing_config_temp_cache: FxHashMap<(IdentifierId, String), Place>,681}682683impl Context {684 fn intern_effect(&mut self, effect: AliasingEffect) -> AliasingEffect {685 let hash = hash_effect(&effect);686 self.interned_effects.entry(hash).or_insert(effect).clone()687 }688689 /// Get or create a stable ValueId for a given effect, ensuring fixpoint convergence.690 fn get_or_create_value_id(&mut self, effect: &AliasingEffect) -> ValueId {691 let hash = hash_effect(effect);692 *self693 .effect_value_id_cache694 .entry(hash)695 .or_insert_with(ValueId::new)696 }697}698699struct InstructionSignature {700 effects: Vec<AliasingEffect>,701}702703// =============================================================================704// Helper: hash_effect705// =============================================================================706707fn hash_effect(effect: &AliasingEffect) -> String {708 match effect {709 AliasingEffect::Apply {710 receiver,711 function,712 mutates_function,713 args,714 into,715 ..716 } => {717 let args_str: Vec<String> = args718 .iter()719 .map(|a| match a {720 PlaceOrSpreadOrHole::Hole => String::new(),721 PlaceOrSpreadOrHole::Place(p) => format!("{}", p.identifier.0),722 PlaceOrSpreadOrHole::Spread(s) => format!("...{}", s.place.identifier.0),723 })724 .collect();725 format!(726 "Apply:{}:{}:{}:{}:{}",727 receiver.identifier.0,728 function.identifier.0,729 mutates_function,730 args_str.join(","),731 into.identifier.0732 )733 }734 AliasingEffect::CreateFrom { from, into } => {735 format!("CreateFrom:{}:{}", from.identifier.0, into.identifier.0)736 }737 AliasingEffect::ImmutableCapture { from, into } => format!(738 "ImmutableCapture:{}:{}",739 from.identifier.0, into.identifier.0740 ),741 AliasingEffect::Assign { from, into } => {742 format!("Assign:{}:{}", from.identifier.0, into.identifier.0)743 }744 AliasingEffect::Alias { from, into } => {745 format!("Alias:{}:{}", from.identifier.0, into.identifier.0)746 }747 AliasingEffect::Capture { from, into } => {748 format!("Capture:{}:{}", from.identifier.0, into.identifier.0)749 }750 AliasingEffect::MaybeAlias { from, into } => {751 format!("MaybeAlias:{}:{}", from.identifier.0, into.identifier.0)752 }753 AliasingEffect::Create {754 into,755 value,756 reason,757 } => format!("Create:{}:{:?}:{:?}", into.identifier.0, value, reason),758 AliasingEffect::Freeze { value, reason } => {759 format!("Freeze:{}:{:?}", value.identifier.0, reason)760 }761 AliasingEffect::Impure { place, .. } => format!("Impure:{}", place.identifier.0),762 AliasingEffect::Render { place } => format!("Render:{}", place.identifier.0),763 AliasingEffect::MutateFrozen { place, error } => format!(764 "MutateFrozen:{}:{}:{:?}",765 place.identifier.0, error.reason, error.description766 ),767 AliasingEffect::MutateGlobal { place, error } => format!(768 "MutateGlobal:{}:{}:{:?}",769 place.identifier.0, error.reason, error.description770 ),771 AliasingEffect::Mutate { value, .. } => format!("Mutate:{}", value.identifier.0),772 AliasingEffect::MutateConditionally { value } => {773 format!("MutateConditionally:{}", value.identifier.0)774 }775 AliasingEffect::MutateTransitive { value } => {776 format!("MutateTransitive:{}", value.identifier.0)777 }778 AliasingEffect::MutateTransitiveConditionally { value } => {779 format!("MutateTransitiveConditionally:{}", value.identifier.0)780 }781 AliasingEffect::CreateFunction {782 into,783 function_id,784 captures,785 } => {786 let cap_str: Vec<String> = captures787 .iter()788 .map(|p| format!("{}", p.identifier.0))789 .collect();790 format!(791 "CreateFunction:{}:{}:{}",792 into.identifier.0,793 function_id.0,794 cap_str.join(",")795 )796 }797 }798}799800// =============================================================================801// merge helpers802// =============================================================================803804fn merge_abstract_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {805 let kind = merge_value_kinds(a.kind, b.kind);806 if kind == a.kind && kind == b.kind && a.reason.is_superset_of(&b.reason) {807 return a.clone();808 }809 let mut reason = a.reason;810 reason.union_with(&b.reason);811 AbstractValue { kind, reason }812}813814fn merge_value_kinds(a: ValueKind, b: ValueKind) -> ValueKind {815 if a == b {816 return a;817 }818 if a == ValueKind::MaybeFrozen || b == ValueKind::MaybeFrozen {819 return ValueKind::MaybeFrozen;820 }821 if a == ValueKind::Mutable || b == ValueKind::Mutable {822 if a == ValueKind::Frozen || b == ValueKind::Frozen {823 return ValueKind::MaybeFrozen;824 } else if a == ValueKind::Context || b == ValueKind::Context {825 return ValueKind::Context;826 } else {827 return ValueKind::Mutable;828 }829 }830 if a == ValueKind::Context || b == ValueKind::Context {831 if a == ValueKind::Frozen || b == ValueKind::Frozen {832 return ValueKind::MaybeFrozen;833 } else {834 return ValueKind::Context;835 }836 }837 if a == ValueKind::Frozen || b == ValueKind::Frozen {838 return ValueKind::Frozen;839 }840 if a == ValueKind::Global || b == ValueKind::Global {841 return ValueKind::Global;842 }843 ValueKind::Primitive844}845846// =============================================================================847// Pre-passes848// =============================================================================849850fn find_hoisted_context_declarations(851 func: &HirFunction,852 env: &Environment,853) -> FxHashMap<DeclarationId, Option<Place>> {854 let mut hoisted: FxHashMap<DeclarationId, Option<Place>> = FxHashMap::default();855856 fn visit(857 hoisted: &mut FxHashMap<DeclarationId, Option<Place>>,858 place: &Place,859 env: &Environment,860 ) {861 let decl_id = env.identifiers[place.identifier.0 as usize].declaration_id;862 if hoisted.contains_key(&decl_id) && hoisted.get(&decl_id).unwrap().is_none() {863 hoisted.insert(decl_id, Some(place.clone()));864 }865 }866867 for (_block_id, block) in &func.body.blocks {868 for instr_id in &block.instructions {869 let instr = &func.instructions[instr_id.0 as usize];870 match &instr.value {871 InstructionValue::DeclareContext { lvalue, .. } => {872 let kind = lvalue.kind;873 if kind == InstructionKind::HoistedConst874 || kind == InstructionKind::HoistedFunction875 || kind == InstructionKind::HoistedLet876 {877 let decl_id =878 env.identifiers[lvalue.place.identifier.0 as usize].declaration_id;879 hoisted.insert(decl_id, None);880 }881 }882 _ => {883 for operand in visitors::each_instruction_value_operand(&instr.value, env) {884 visit(&mut hoisted, &operand, env);885 }886 }887 }888 }889 for operand in visitors::each_terminal_operand(&block.terminal) {890 visit(&mut hoisted, &operand, env);891 }892 }893 hoisted894}895896fn find_non_mutated_destructure_spreads(897 func: &HirFunction,898 env: &Environment,899) -> FxHashSet<IdentifierId> {900 let mut known_frozen: FxHashSet<IdentifierId> = FxHashSet::default();901 if func.fn_type == ReactFunctionType::Component {902 if let Some(param) = func.params.first() {903 if let ParamPattern::Place(p) = param {904 known_frozen.insert(p.identifier);905 }906 }907 } else {908 for param in &func.params {909 if let ParamPattern::Place(p) = param {910 known_frozen.insert(p.identifier);911 }912 }913 }914915 let mut candidate_non_mutating_spreads: FxHashMap<IdentifierId, IdentifierId> =916 FxHashMap::default();917 for (_block_id, block) in &func.body.blocks {918 if !candidate_non_mutating_spreads.is_empty() {919 for phi in &block.phis {920 for (_, operand) in &phi.operands {921 if let Some(spread) = candidate_non_mutating_spreads922 .get(&operand.identifier)923 .copied()924 {925 candidate_non_mutating_spreads.remove(&spread);926 }927 }928 }929 }930 for instr_id in &block.instructions {931 let instr = &func.instructions[instr_id.0 as usize];932 let lvalue_id = instr.lvalue.identifier;933 match &instr.value {934 InstructionValue::Destructure { lvalue, value, .. } => {935 if !known_frozen.contains(&value.identifier) {936 continue;937 }938 if !(lvalue.kind == InstructionKind::Let939 || lvalue.kind == InstructionKind::Const)940 {941 continue;942 }943 match &lvalue.pattern {944 react_compiler_hir::Pattern::Object(obj_pat) => {945 for prop in &obj_pat.properties {946 if let react_compiler_hir::ObjectPropertyOrSpread::Spread(s) = prop947 {948 candidate_non_mutating_spreads949 .insert(s.place.identifier, s.place.identifier);950 }951 }952 }953 _ => continue,954 }955 }956 InstructionValue::LoadLocal { place, .. } => {957 if let Some(spread) = candidate_non_mutating_spreads958 .get(&place.identifier)959 .copied()960 {961 candidate_non_mutating_spreads.insert(lvalue_id, spread);962 }963 }964 InstructionValue::StoreLocal {965 lvalue: sl,966 value: sv,967 ..968 } => {969 if let Some(spread) =970 candidate_non_mutating_spreads.get(&sv.identifier).copied()971 {972 candidate_non_mutating_spreads.insert(lvalue_id, spread);973 candidate_non_mutating_spreads.insert(sl.place.identifier, spread);974 }975 }976 InstructionValue::JsxFragment { .. } | InstructionValue::JsxExpression { .. } => {977 // Passing objects created with spread to jsx can't mutate them978 }979 InstructionValue::PropertyLoad { .. } => {980 // Properties must be frozen since the original value was frozen981 }982 InstructionValue::CallExpression { callee, .. }983 | InstructionValue::MethodCall {984 property: callee, ..985 } => {986 let callee_ty =987 &env.types[env.identifiers[callee.identifier.0 as usize].type_.0 as usize];988 if get_hook_kind_for_type(env, callee_ty)989 .ok()990 .flatten()991 .is_some()992 {993 if !is_ref_or_ref_value_for_id(env, lvalue_id) {994 known_frozen.insert(lvalue_id);995 }996 } else if !candidate_non_mutating_spreads.is_empty() {997 for operand in visitors::each_instruction_value_operand(&instr.value, env) {998 if let Some(spread) = candidate_non_mutating_spreads999 .get(&operand.identifier)1000 .copied()1001 {1002 candidate_non_mutating_spreads.remove(&spread);1003 }1004 }1005 }1006 }1007 _ => {1008 if !candidate_non_mutating_spreads.is_empty() {1009 for operand in visitors::each_instruction_value_operand(&instr.value, env) {1010 if let Some(spread) = candidate_non_mutating_spreads1011 .get(&operand.identifier)1012 .copied()1013 {1014 candidate_non_mutating_spreads.remove(&spread);1015 }1016 }1017 }1018 }1019 }1020 }1021 }10221023 let mut non_mutating: FxHashSet<IdentifierId> = FxHashSet::default();1024 for (key, value) in &candidate_non_mutating_spreads {1025 if key == value {1026 non_mutating.insert(*key);1027 }1028 }1029 non_mutating1030}10311032// =============================================================================1033// inferParam1034// =============================================================================10351036fn infer_param(param: &ParamPattern, state: &mut InferenceState, param_kind: &AbstractValue) {1037 let place = match param {1038 ParamPattern::Place(p) => p,1039 ParamPattern::Spread(s) => &s.place,1040 };1041 let value_id = ValueId::new();1042 state.initialize(value_id, param_kind.clone());1043 state.define(place.identifier, value_id);1044}10451046// =============================================================================1047// inferBlock1048// =============================================================================10491050fn infer_block(1051 context: &mut Context,1052 state: &mut InferenceState,1053 block_id: BlockId,1054 func: &mut HirFunction,1055 env: &mut Environment,1056) -> Result<(), CompilerDiagnostic> {1057 let block = &func.body.blocks[&block_id];10581059 // Process phis1060 let phis: Vec<(IdentifierId, IndexMap<BlockId, Place, FxBuildHasher>)> = block1061 .phis1062 .iter()1063 .map(|phi| (phi.place.identifier, phi.operands.clone()))1064 .collect();1065 for (place_id, operands) in &phis {1066 state.infer_phi(*place_id, operands);1067 }10681069 // Process instructions1070 let instr_ids: Vec<u32> = block.instructions.iter().map(|id| id.0).collect();1071 for instr_idx in &instr_ids {1072 let instr_index = *instr_idx as usize;10731074 // Compute signature if not cached1075 if !context.instruction_signature_cache.contains_key(instr_idx) {1076 let sig = compute_signature_for_instruction(1077 context,1078 env,1079 &func.instructions[instr_index],1080 func,1081 );1082 context.instruction_signature_cache.insert(*instr_idx, sig);1083 }10841085 // Apply signature1086 let effects = apply_signature(1087 context,1088 state,1089 *instr_idx,1090 &func.instructions[instr_index],1091 env,1092 func,1093 )?;1094 func.instructions[instr_index].effects = effects;1095 }10961097 // Process terminal1098 // Determine what terminal action to take without holding borrows1099 enum TerminalAction {1100 Try { handler: BlockId, binding: Place },1101 MaybeThrow { handler_id: BlockId },1102 Return,1103 None,1104 }1105 let action = {1106 let block = &func.body.blocks[&block_id];1107 match &block.terminal {1108 react_compiler_hir::Terminal::Try {1109 handler,1110 handler_binding: Some(binding),1111 ..1112 } => TerminalAction::Try {1113 handler: *handler,1114 binding: binding.clone(),1115 },1116 react_compiler_hir::Terminal::MaybeThrow {1117 handler: Some(handler_id),1118 ..1119 } => TerminalAction::MaybeThrow {1120 handler_id: *handler_id,1121 },1122 react_compiler_hir::Terminal::Return { .. } => TerminalAction::Return,1123 _ => TerminalAction::None,1124 }1125 };11261127 match action {1128 TerminalAction::Try { handler, binding } => {1129 context.catch_handlers.insert(handler, binding);1130 }1131 TerminalAction::MaybeThrow { handler_id } => {1132 if let Some(handler_param) = context.catch_handlers.get(&handler_id).cloned() {1133 if state.is_defined(handler_param.identifier) {1134 let mut terminal_effects: Vec<AliasingEffect> = Vec::new();1135 for instr_idx in &instr_ids {1136 let instr = &func.instructions[*instr_idx as usize];1137 match &instr.value {1138 InstructionValue::CallExpression { .. }1139 | InstructionValue::MethodCall { .. } => {1140 state.append_alias(1141 handler_param.identifier,1142 instr.lvalue.identifier,1143 );1144 let kind = state.kind(instr.lvalue.identifier).kind;1145 if kind == ValueKind::Mutable || kind == ValueKind::Context {1146 terminal_effects.push(context.intern_effect(1147 AliasingEffect::Alias {1148 from: instr.lvalue.clone(),1149 into: handler_param.clone(),1150 },1151 ));1152 }1153 }1154 _ => {}1155 }1156 }1157 let block_mut = func.body.blocks.get_mut(&block_id).unwrap();1158 if let react_compiler_hir::Terminal::MaybeThrow {1159 effects: ref mut term_effects,1160 ..1161 } = block_mut.terminal1162 {1163 *term_effects = if terminal_effects.is_empty() {1164 None1165 } else {1166 Some(terminal_effects)1167 };1168 }1169 }1170 }1171 }1172 TerminalAction::Return => {1173 if !context.is_function_expression {1174 let block_mut = func.body.blocks.get_mut(&block_id).unwrap();1175 if let react_compiler_hir::Terminal::Return {1176 ref value,1177 effects: ref mut term_effects,1178 ..1179 } = block_mut.terminal1180 {1181 *term_effects = Some(vec![context.intern_effect(AliasingEffect::Freeze {1182 value: value.clone(),1183 reason: ValueReason::JsxCaptured,1184 })]);1185 }1186 }1187 }1188 TerminalAction::None => {}1189 }1190 Ok(())1191}11921193// =============================================================================1194// applySignature1195// =============================================================================11961197fn apply_signature(1198 context: &mut Context,1199 state: &mut InferenceState,1200 instr_idx: u32,1201 instr: &react_compiler_hir::Instruction,1202 env: &mut Environment,1203 func: &HirFunction,1204) -> Result<Option<Vec<AliasingEffect>>, CompilerDiagnostic> {1205 let mut effects: Vec<AliasingEffect> = Vec::new();12061207 // For function instructions, validate frozen mutation1208 match &instr.value {1209 InstructionValue::FunctionExpression { lowered_func, .. }1210 | InstructionValue::ObjectMethod { lowered_func, .. } => {1211 let inner_func = &env.functions[lowered_func.func.0 as usize];1212 if let Some(ref aliasing_effects) = inner_func.aliasing_effects {1213 let context_ids: FxHashSet<IdentifierId> =1214 inner_func.context.iter().map(|p| p.identifier).collect();1215 for effect in aliasing_effects {1216 let (mutate_value, is_mutate) = match effect {1217 AliasingEffect::Mutate { value, .. } => (value, true),1218 AliasingEffect::MutateTransitive { value } => (value, false),1219 _ => continue,1220 };1221 if !context_ids.contains(&mutate_value.identifier) {1222 continue;1223 }1224 if !state.is_defined(mutate_value.identifier) {1225 continue;1226 }1227 let value_abstract = state.kind(mutate_value.identifier);1228 if value_abstract.kind == ValueKind::Frozen {1229 let reason_str = get_write_error_reason(&value_abstract);1230 let ident = &env.identifiers[mutate_value.identifier.0 as usize];1231 let variable = match &ident.name {1232 Some(react_compiler_hir::IdentifierName::Named(n)) => {1233 format!("`{}`", n)1234 }1235 _ => "value".to_string(),1236 };1237 let mut diagnostic = CompilerDiagnostic::new(1238 ErrorCategory::Immutability,1239 "This value cannot be modified",1240 Some(reason_str),1241 );1242 diagnostic.details.push(1243 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {1244 loc: mutate_value.loc,1245 message: Some(format!("{} cannot be modified", variable)),1246 identifier_name: None,1247 },1248 );1249 if is_mutate {1250 if let AliasingEffect::Mutate {1251 reason: Some(MutationReason::AssignCurrentProperty),1252 ..1253 } = effect1254 {1255 diagnostic.details.push(react_compiler_diagnostics::CompilerDiagnosticDetail::Hint {1256 message: "Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in \"Ref\".".to_string()1257 });1258 }1259 }1260 effects.push(AliasingEffect::MutateFrozen {1261 place: mutate_value.clone(),1262 error: diagnostic,1263 });1264 }1265 }1266 }1267 }1268 _ => {}1269 }12701271 // Track which values we've already initialized1272 let mut initialized: FxHashSet<IdentifierId> = FxHashSet::default();12731274 // Get the cached signature effects1275 let sig = context.instruction_signature_cache.get(&instr_idx).unwrap();1276 let sig_effects: Vec<AliasingEffect> = sig.effects.clone();12771278 for effect in &sig_effects {1279 apply_effect(1280 context,1281 state,1282 effect.clone(),1283 &mut initialized,1284 &mut effects,1285 env,1286 func,1287 )?;1288 }12891290 // If lvalue is not yet defined, initialize it with a default value.1291 // The TS version asserts this as an invariant, but the Rust port may have1292 // edge cases where effects don't cover the lvalue (e.g. missing signature entries).1293 if !state.is_defined(instr.lvalue.identifier) {1294 let vid = ValueId(instr.lvalue.identifier.0 | 0x80000000);1295 state.initialize(1296 vid,1297 AbstractValue {1298 kind: ValueKind::Mutable,1299 reason: ValueReasonSet::single(ValueReason::Other),1300 },1301 );1302 state.define(instr.lvalue.identifier, vid);1303 }13041305 Ok(if effects.is_empty() {1306 None1307 } else {1308 Some(effects)1309 })1310}13111312// =============================================================================1313// Transitive freeze helper1314// =============================================================================13151316/// Recursively freeze through FunctionExpression captures. If `value_id`1317/// corresponds to a FunctionExpression, freeze each of its context captures1318/// and recurse into any that are themselves FunctionExpressions. This matches1319/// the TS `freezeValue` → `freeze` → `freezeValue` recursion chain.1320fn freeze_function_captures_transitive(1321 state: &mut InferenceState,1322 context: &Context,1323 env: &Environment,1324 value_id: ValueId,1325 reason: ValueReason,1326) {1327 if let Some(&func_id) = context.function_values.get(&value_id) {1328 let ctx_ids: Vec<IdentifierId> = env.functions[func_id.0 as usize]1329 .context1330 .iter()1331 .map(|p| p.identifier)1332 .collect();1333 for ctx_id in ctx_ids {1334 // Replicate InferenceState::freeze() logic inline —1335 // we need to recurse with context/env which freeze() doesn't have.1336 if !state.variables.contains_key(&ctx_id) {1337 continue;1338 }1339 let kind = state.kind(ctx_id).kind;1340 match kind {1341 ValueKind::Context | ValueKind::Mutable | ValueKind::MaybeFrozen => {1342 let vids: Vec<ValueId> = state.values_for(ctx_id);1343 for vid in vids {1344 state.freeze_value(vid, reason);1345 // Recurse into nested function captures1346 freeze_function_captures_transitive(state, context, env, vid, reason);1347 }1348 }1349 ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => {1350 // Already frozen or immutable — no-op1351 }1352 }1353 }1354 }1355}13561357// =============================================================================1358// applyEffect1359// =============================================================================13601361fn apply_effect(1362 context: &mut Context,1363 state: &mut InferenceState,1364 effect: AliasingEffect,1365 initialized: &mut FxHashSet<IdentifierId>,1366 effects: &mut Vec<AliasingEffect>,1367 env: &mut Environment,1368 func: &HirFunction,1369) -> Result<(), CompilerDiagnostic> {1370 let effect = context.intern_effect(effect);1371 match effect {1372 AliasingEffect::Freeze { ref value, reason } => {1373 let did_freeze = state.freeze(value.identifier, reason);1374 if did_freeze {1375 effects.push(effect.clone());1376 // Transitively freeze FunctionExpression captures if enabled1377 // (matches TS freezeValue which recurses into func.context)1378 let enable_transitive = env.config.enable_preserve_existing_memoization_guarantees1379 || env.config.enable_transitively_freeze_function_expressions;1380 if enable_transitive {1381 // Recursively freeze through function captures. The TS1382 // freezeValue() calls freeze() on each capture, which1383 // calls freezeValue() again — creating a transitive1384 // closure through arbitrarily nested function captures.1385 let value_ids: Vec<ValueId> = state.values_for(value.identifier);1386 for vid in &value_ids {1387 freeze_function_captures_transitive(state, context, env, *vid, reason);1388 }1389 }1390 }1391 }1392 AliasingEffect::Create {1393 ref into,1394 value: kind,1395 reason,1396 } => {1397 assert!(1398 !initialized.contains(&into.identifier),1399 "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"1400 );1401 initialized.insert(into.identifier);1402 let value_id = context.get_or_create_value_id(&effect);1403 state.initialize(1404 value_id,1405 AbstractValue {1406 kind,1407 reason: ValueReasonSet::single(reason),1408 },1409 );1410 state.define(into.identifier, value_id);1411 effects.push(effect.clone());1412 }1413 AliasingEffect::ImmutableCapture { ref from, .. } => {1414 let kind = state.kind(from.identifier).kind;1415 match kind {1416 ValueKind::Global | ValueKind::Primitive => {1417 // no-op: don't track data flow for copy types1418 }1419 _ => {1420 effects.push(effect.clone());1421 }1422 }1423 }1424 AliasingEffect::CreateFrom { ref from, ref into } => {1425 assert!(1426 !initialized.contains(&into.identifier),1427 "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"1428 );1429 initialized.insert(into.identifier);1430 let from_value = state.kind(from.identifier);1431 let value_id = context.get_or_create_value_id(&effect);1432 state.initialize(1433 value_id,1434 AbstractValue {1435 kind: from_value.kind,1436 reason: from_value.reason,1437 },1438 );1439 state.define(into.identifier, value_id);1440 match from_value.kind {1441 ValueKind::Primitive | ValueKind::Global => {1442 let first_reason = primary_reason(&from_value.reason);1443 effects.push(AliasingEffect::Create {1444 value: from_value.kind,1445 into: into.clone(),1446 reason: first_reason,1447 });1448 }1449 ValueKind::Frozen => {1450 let first_reason = primary_reason(&from_value.reason);1451 effects.push(AliasingEffect::Create {1452 value: from_value.kind,1453 into: into.clone(),1454 reason: first_reason,1455 });1456 apply_effect(1457 context,1458 state,1459 AliasingEffect::ImmutableCapture {1460 from: from.clone(),1461 into: into.clone(),1462 },1463 initialized,1464 effects,1465 env,1466 func,1467 )?;1468 }1469 _ => {1470 effects.push(effect.clone());1471 }1472 }1473 }1474 AliasingEffect::CreateFunction {1475 ref captures,1476 function_id,1477 ref into,1478 } => {1479 assert!(1480 !initialized.contains(&into.identifier),1481 "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"1482 );1483 initialized.insert(into.identifier);1484 effects.push(effect.clone());14851486 // Check if function is mutable1487 let has_captures = captures.iter().any(|capture| {1488 if !state.is_defined(capture.identifier) {1489 return false;1490 }1491 let k = state.kind(capture.identifier).kind;1492 k == ValueKind::Context || k == ValueKind::Mutable1493 });14941495 let inner_func = &env.functions[function_id.0 as usize];1496 let has_tracked_side_effects = inner_func1497 .aliasing_effects1498 .as_ref()1499 .map(|effs| {1500 effs.iter().any(|e| {1501 matches!(1502 e,1503 AliasingEffect::MutateFrozen { .. }1504 | AliasingEffect::MutateGlobal { .. }1505 | AliasingEffect::Impure { .. }1506 )1507 })1508 })1509 .unwrap_or(false);15101511 let captures_ref = inner_func1512 .context1513 .iter()1514 .any(|operand| is_ref_or_ref_value_for_id(env, operand.identifier));15151516 let is_mutable = has_captures || has_tracked_side_effects || captures_ref;15171518 // Update context variable effects1519 let context_places: Vec<Place> = inner_func.context.clone();1520 for operand in &context_places {1521 if operand.effect != Effect::Capture {1522 continue;1523 }1524 if !state.is_defined(operand.identifier) {1525 continue;1526 }1527 let kind = state.kind(operand.identifier).kind;1528 if kind == ValueKind::Primitive1529 || kind == ValueKind::Frozen1530 || kind == ValueKind::Global1531 {1532 // Downgrade to Read - we need to mutate the inner function1533 let inner_func_mut = &mut env.functions[function_id.0 as usize];1534 for ctx in &mut inner_func_mut.context {1535 if ctx.identifier == operand.identifier && ctx.effect == Effect::Capture {1536 ctx.effect = Effect::Read;1537 }1538 }1539 }1540 }15411542 let value_id = context.get_or_create_value_id(&effect);1543 // Track this value as a function expression so Apply can look it up1544 context.function_values.insert(value_id, function_id);1545 state.initialize(1546 value_id,1547 AbstractValue {1548 kind: if is_mutable {1549 ValueKind::Mutable1550 } else {1551 ValueKind::Frozen1552 },1553 reason: ValueReasonSet::default(),1554 },1555 );1556 state.define(into.identifier, value_id);15571558 for capture in captures {1559 apply_effect(1560 context,1561 state,1562 AliasingEffect::Capture {1563 from: capture.clone(),1564 into: into.clone(),1565 },1566 initialized,1567 effects,1568 env,1569 func,1570 )?;1571 }1572 }1573 AliasingEffect::MaybeAlias { ref from, ref into }1574 | AliasingEffect::Alias { ref from, ref into }1575 | AliasingEffect::Capture { ref from, ref into } => {1576 let is_capture = matches!(effect, AliasingEffect::Capture { .. });1577 let is_maybe_alias = matches!(effect, AliasingEffect::MaybeAlias { .. });1578 // For Alias, destination must already be initialized (Capture/MaybeAlias are exempt)1579 assert!(1580 is_capture || is_maybe_alias || initialized.contains(&into.identifier),1581 "[InferMutationAliasingEffects] Expected destination to already be initialized within this instruction"1582 );15831584 // Check destination kind1585 let into_kind = state.kind_with_loc(into.identifier, into.loc).kind;1586 let destination_type = match into_kind {1587 ValueKind::Context => Some("context"),1588 ValueKind::Mutable | ValueKind::MaybeFrozen => Some("mutable"),1589 _ => None,1590 };15911592 let from_kind = state.kind_with_loc(from.identifier, from.loc).kind;1593 let source_type = match from_kind {1594 ValueKind::Context => Some("context"),1595 ValueKind::Global | ValueKind::Primitive => None,1596 ValueKind::MaybeFrozen | ValueKind::Frozen => Some("frozen"),1597 ValueKind::Mutable => Some("mutable"),1598 };15991600 if source_type == Some("frozen") {1601 apply_effect(1602 context,1603 state,1604 AliasingEffect::ImmutableCapture {1605 from: from.clone(),1606 into: into.clone(),1607 },1608 initialized,1609 effects,1610 env,1611 func,1612 )?;1613 } else if (source_type == Some("mutable") && destination_type == Some("mutable"))1614 || is_maybe_alias1615 {1616 effects.push(effect.clone());1617 } else if (source_type == Some("context") && destination_type.is_some())1618 || (source_type == Some("mutable") && destination_type == Some("context"))1619 {1620 apply_effect(1621 context,1622 state,1623 AliasingEffect::MaybeAlias {1624 from: from.clone(),1625 into: into.clone(),1626 },1627 initialized,1628 effects,1629 env,1630 func,1631 )?;1632 }1633 }1634 AliasingEffect::Assign { ref from, ref into } => {1635 assert!(1636 !initialized.contains(&into.identifier),1637 "[InferMutationAliasingEffects] Cannot re-initialize variable within an instruction"1638 );1639 initialized.insert(into.identifier);1640 let from_value = state.kind_with_loc(from.identifier, from.loc);1641 match from_value.kind {1642 ValueKind::Frozen => {1643 apply_effect(1644 context,1645 state,1646 AliasingEffect::ImmutableCapture {1647 from: from.clone(),1648 into: into.clone(),1649 },1650 initialized,1651 effects,1652 env,1653 func,1654 )?;1655 let cache_key =1656 format!("Assign_frozen:{}:{}", from.identifier.0, into.identifier.0);1657 let value_id = *context1658 .effect_value_id_cache1659 .entry(cache_key)1660 .or_insert_with(ValueId::new);1661 state.initialize(1662 value_id,1663 AbstractValue {1664 kind: from_value.kind,1665 reason: from_value.reason,1666 },1667 );1668 state.define(into.identifier, value_id);1669 }1670 ValueKind::Global | ValueKind::Primitive => {1671 let cache_key =1672 format!("Assign_copy:{}:{}", from.identifier.0, into.identifier.0);1673 let value_id = *context1674 .effect_value_id_cache1675 .entry(cache_key)1676 .or_insert_with(ValueId::new);1677 state.initialize(1678 value_id,1679 AbstractValue {1680 kind: from_value.kind,1681 reason: from_value.reason,1682 },1683 );1684 state.define(into.identifier, value_id);1685 }1686 _ => {1687 state.assign(into.identifier, from.identifier);1688 effects.push(effect.clone());1689 }1690 }1691 }1692 AliasingEffect::Apply {1693 ref receiver,1694 ref function,1695 mutates_function,1696 ref args,1697 ref into,1698 ref signature,1699 ref loc,1700 } => {1701 // First, check if the callee is a locally-declared function expression1702 // whose aliasing effects we already know (TS lines 1016-1068)1703 if state.is_defined(function.identifier) {1704 let function_values = state.values_for(function.identifier);1705 if function_values.len() == 1 {1706 let value_id = function_values[0];1707 if let Some(func_id) = context.function_values.get(&value_id).copied() {1708 let inner_func = &env.functions[func_id.0 as usize];1709 if inner_func.aliasing_effects.is_some() {1710 // Build or retrieve the signature from the function expression1711 if !context.function_signature_cache.contains_key(&func_id) {1712 let sig = build_signature_from_function_expression(env, func_id);1713 context.function_signature_cache.insert(func_id, sig);1714 }1715 let sig = context1716 .function_signature_cache1717 .get(&func_id)1718 .unwrap()1719 .clone();1720 let inner_func = &env.functions[func_id.0 as usize];1721 let context_places: Vec<Place> = inner_func.context.clone();1722 let sig_effects = compute_effects_for_aliasing_signature(1723 env,1724 &sig,1725 into,1726 receiver,1727 args,1728 &context_places,1729 loc.as_ref(),1730 )?;1731 if let Some(sig_effs) = sig_effects {1732 // Conditionally mutate the function itself first1733 apply_effect(1734 context,1735 state,1736 AliasingEffect::MutateTransitiveConditionally {1737 value: function.clone(),1738 },1739 initialized,1740 effects,1741 env,1742 func,1743 )?;1744 for se in sig_effs {1745 apply_effect(1746 context,1747 state,1748 se,1749 initialized,1750 effects,1751 env,1752 func,1753 )?;1754 }1755 return Ok(());1756 }1757 }1758 }1759 }1760 }1761 if let Some(sig) = signature {1762 // Check known_incompatible (TS line 2351-2370)1763 if let Some(ref incompatible_msg) = sig.known_incompatible {1764 if env.enable_validations() {1765 let mut diagnostic = CompilerDiagnostic::new(1766 ErrorCategory::IncompatibleLibrary,1767 "Use of incompatible library",1768 Some(1769 "This API returns functions which cannot be memoized without leading to stale UI. \1770 To prevent this, by default React Compiler will skip memoizing this component/hook. \1771 However, you may see issues if values from this API are passed to other components/hooks that are \1772 memoized".to_string(),1773 ),1774 );1775 diagnostic.details.push(CompilerDiagnosticDetail::Error {1776 loc: receiver.loc,1777 message: Some(incompatible_msg.clone()),1778 identifier_name: None,1779 });1780 // TS throws here, aborting compilation for this function1781 return Err(diagnostic);1782 }1783 }17841785 if let Some(ref aliasing) = sig.aliasing {1786 let sig_effects = compute_effects_for_aliasing_signature_config(1787 env,1788 aliasing,1789 into,1790 receiver,1791 args,1792 &[],1793 loc.as_ref(),1794 &mut context.aliasing_config_temp_cache,1795 )?;1796 if let Some(sig_effs) = sig_effects {1797 for se in sig_effs {1798 apply_effect(context, state, se, initialized, effects, env, func)?;1799 }1800 return Ok(());1801 }1802 }18031804 // Legacy signature1805 let mut todo_errors: Vec<react_compiler_diagnostics::CompilerErrorDetail> =1806 Vec::new();1807 let legacy_effects = compute_effects_for_legacy_signature(1808 state,1809 sig,1810 into,1811 receiver,1812 args,1813 loc.as_ref(),1814 env,1815 &context.function_values,1816 &mut todo_errors,1817 );1818 // Todo errors should short-circuit (TS throws throwTodo)1819 if let Some(err_detail) = todo_errors.into_iter().next() {1820 return Err(CompilerDiagnostic::from_detail(err_detail));1821 }1822 for le in legacy_effects {1823 apply_effect(context, state, le, initialized, effects, env, func)?;1824 }1825 } else {1826 // No signature: default behavior1827 apply_effect(1828 context,1829 state,1830 AliasingEffect::Create {1831 into: into.clone(),1832 value: ValueKind::Mutable,1833 reason: ValueReason::Other,1834 },1835 initialized,1836 effects,1837 env,1838 func,1839 )?;18401841 let all_operands = build_apply_operands(receiver, function, args);1842 for (operand, _is_function_operand, is_spread) in &all_operands {1843 // In TS, the check is `operand !== effect.function || effect.mutatesFunction`.1844 // This compares by reference identity, so for CallExpression/NewExpression1845 // where receiver === function, BOTH are skipped when !mutatesFunction.1846 if operand.identifier == function.identifier && !mutates_function {1847 // Don't mutate callee for non-mutating calls1848 } else {1849 apply_effect(1850 context,1851 state,1852 AliasingEffect::MutateTransitiveConditionally {1853 value: operand.clone(),1854 },1855 initialized,1856 effects,1857 env,1858 func,1859 )?;1860 }18611862 if *is_spread {1863 let ty = &env.types1864 [env.identifiers[operand.identifier.0 as usize].type_.0 as usize];1865 if let Some(mutate_iter) = conditionally_mutate_iterator(operand, ty) {1866 apply_effect(1867 context,1868 state,1869 mutate_iter,1870 initialized,1871 effects,1872 env,1873 func,1874 )?;1875 }1876 }18771878 apply_effect(1879 context,1880 state,1881 AliasingEffect::MaybeAlias {1882 from: operand.clone(),1883 into: into.clone(),1884 },1885 initialized,1886 effects,1887 env,1888 func,1889 )?;18901891 // In TS, `other === arg` compares the Place extracted from1892 // `otherArg` with the original `arg` element. For Identifier1893 // args, the extracted Place IS the arg, so this is a reference1894 // identity check. For Spread args, the extracted Place is1895 // `.place` which is never `===` the Spread wrapper object,1896 // so NO pairs are skipped when the outer arg is a Spread1897 // (including self-pairs, producing self-captures).1898 for (other, _other_is_func, _other_is_spread) in &all_operands {1899 if !is_spread && other.identifier == operand.identifier {1900 continue;1901 }1902 apply_effect(1903 context,1904 state,1905 AliasingEffect::Capture {1906 from: operand.clone(),1907 into: other.clone(),1908 },1909 initialized,1910 effects,1911 env,1912 func,1913 )?;1914 }1915 }1916 }1917 }1918 ref eff @ (AliasingEffect::Mutate { .. }1919 | AliasingEffect::MutateConditionally { .. }1920 | AliasingEffect::MutateTransitive { .. }1921 | AliasingEffect::MutateTransitiveConditionally { .. }) => {1922 let (mutate_place, variant) = match eff {1923 AliasingEffect::Mutate { value, .. } => (value, MutateVariant::Mutate),1924 AliasingEffect::MutateConditionally { value } => {1925 (value, MutateVariant::MutateConditionally)1926 }1927 AliasingEffect::MutateTransitive { value } => {1928 (value, MutateVariant::MutateTransitive)1929 }1930 AliasingEffect::MutateTransitiveConditionally { value } => {1931 (value, MutateVariant::MutateTransitiveConditionally)1932 }1933 _ => unreachable!(),1934 };1935 let value = mutate_place;1936 let mutation_kind = state.mutate_with_loc(variant, value.identifier, env, value.loc);1937 if mutation_kind == MutationResult::Mutate {1938 effects.push(effect.clone());1939 } else if mutation_kind == MutationResult::MutateRef {1940 // no-op1941 } else if mutation_kind != MutationResult::None1942 && matches!(1943 variant,1944 MutateVariant::Mutate | MutateVariant::MutateTransitive1945 )1946 {1947 let abstract_value = state.kind(value.identifier);19481949 let ident = &env.identifiers[value.identifier.0 as usize];1950 let decl_id = ident.declaration_id;19511952 if mutation_kind == MutationResult::MutateFrozen1953 && context.hoisted_context_declarations.contains_key(&decl_id)1954 {1955 let variable = match &ident.name {1956 Some(react_compiler_hir::IdentifierName::Named(n)) => {1957 Some(format!("`{}`", n))1958 }1959 _ => None,1960 };1961 let hoisted_access = context1962 .hoisted_context_declarations1963 .get(&decl_id)1964 .cloned()1965 .flatten();1966 let mut diagnostic = CompilerDiagnostic::new(1967 ErrorCategory::Immutability,1968 "Cannot access variable before it is declared",1969 Some(format!(1970 "{} is accessed before it is declared, which prevents the earlier access from updating when this value changes over time",1971 variable.as_deref().unwrap_or("This variable")1972 )),1973 );1974 if let Some(ref access) = hoisted_access {1975 if access.loc != value.loc {1976 diagnostic.details.push(1977 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {1978 loc: access.loc,1979 message: Some(format!(1980 "{} accessed before it is declared",1981 variable.as_deref().unwrap_or("variable")1982 )),1983 identifier_name: None,1984 },1985 );1986 }1987 }1988 diagnostic.details.push(1989 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {1990 loc: value.loc,1991 message: Some(format!(1992 "{} is declared here",1993 variable.as_deref().unwrap_or("variable")1994 )),1995 identifier_name: None,1996 },1997 );1998 apply_effect(1999 context,2000 state,
Findings
✓ No findings reported for this file.