main
rs 3,730 lines 148 KB
Raw
1 // Copyright (c) Meta Platforms, Inc. and affiliates.
2 //
3 // This source code is licensed under the MIT license found in the
4 // LICENSE file in the root directory of this source tree.
5
6 //! Infers the mutation/aliasing effects for instructions and terminals.
7 //!
8 //! Ported from TypeScript `src/Inference/InferMutationAliasingEffects.ts`.
9 //!
10 //! This pass uses abstract interpretation to compute effects describing
11 //! creation, aliasing, mutation, freezing, and error conditions for each
12 //! instruction and terminal in the HIR.
13
14 use indexmap::IndexMap;
15 use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
16
17 use react_compiler_diagnostics::CompilerDiagnostic;
18 use react_compiler_diagnostics::CompilerDiagnosticDetail;
19 use react_compiler_diagnostics::ErrorCategory;
20 use react_compiler_hir::AliasingEffect;
21 use react_compiler_hir::AliasingSignature;
22 use react_compiler_hir::BlockId;
23 use react_compiler_hir::DeclarationId;
24 use react_compiler_hir::Effect;
25 use react_compiler_hir::FunctionId;
26 use react_compiler_hir::HirFunction;
27 use react_compiler_hir::IdentifierId;
28 use react_compiler_hir::InstructionKind;
29 use react_compiler_hir::InstructionValue;
30 use react_compiler_hir::MutationReason;
31 use react_compiler_hir::ParamPattern;
32 use react_compiler_hir::Place;
33 use react_compiler_hir::PlaceOrSpread;
34 use react_compiler_hir::PlaceOrSpreadOrHole;
35 use react_compiler_hir::ReactFunctionType;
36 use react_compiler_hir::SourceLocation;
37 use react_compiler_hir::Type;
38 use react_compiler_hir::environment::Environment;
39 use react_compiler_hir::object_shape::BUILT_IN_ARRAY_ID;
40 use react_compiler_hir::object_shape::BUILT_IN_MAP_ID;
41 use react_compiler_hir::object_shape::BUILT_IN_SET_ID;
42 use react_compiler_hir::object_shape::FunctionSignature;
43 use react_compiler_hir::object_shape::HookKind;
44 use react_compiler_hir::type_config::ValueKind;
45 use react_compiler_hir::type_config::ValueReason;
46 use react_compiler_hir::visitors;
47
48 // =============================================================================
49 // Public entry point
50 // =============================================================================
51
52 /// Infers mutation/aliasing effects for all instructions and terminals in `func`.
53 ///
54 /// Corresponds to TS `inferMutationAliasingEffects(fn, {isFunctionExpression})`.
55 pub 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);
61
62 // Map of blocks to the last (merged) incoming state that was processed
63 let mut states_by_block: FxHashMap<BlockId, InferenceState> = FxHashMap::default();
64
65 // Initialize context variables
66 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 }
77
78 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 };
89
90 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, &param_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, &param_kind);
114 }
115 }
116
117 let mut queued_states: IndexMap<BlockId, InferenceState, FxBuildHasher> = IndexMap::default();
118
119 // Queue helper
120 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 }
142
143 queue(
144 &mut queued_states,
145 &states_by_block,
146 func.body.entry,
147 initial_state,
148 );
149
150 let hoisted_context_declarations = find_hoisted_context_declarations(func, env);
151 let non_mutating_spreads = find_non_mutated_destructure_spreads(func, env);
152
153 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 };
165
166 let mut iteration_count = 0;
167
168 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 }
178
179 // Collect block IDs to process in order
180 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 };
186
187 states_by_block.insert(block_id, incoming_state.clone());
188 let mut state = incoming_state;
189
190 infer_block(&mut context, &mut state, block_id, func, env)?;
191
192 // 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_info
197 .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 loc
201 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_info
204 .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 }
222
223 // Queue successors
224 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 }
235
236 Ok(())
237 }
238
239 // =============================================================================
240 // ValueId: replaces InstructionValue identity as allocation-site key
241 // =============================================================================
242
243 /// Unique allocation-site identifier, replacing TS's object-identity on InstructionValue.
244 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245 struct ValueId(u32);
246
247 use std::sync::atomic::AtomicU32;
248 use std::sync::atomic::Ordering;
249 static NEXT_VALUE_ID: AtomicU32 = AtomicU32::new(1);
250
251 impl ValueId {
252 fn new() -> Self {
253 ValueId(NEXT_VALUE_ID.fetch_add(1, Ordering::Relaxed))
254 }
255 }
256
257 // =============================================================================
258 // AbstractValue
259 // =============================================================================
260
261 #[derive(Debug, Clone, Copy)]
262 struct AbstractValue {
263 kind: ValueKind,
264 reason: ValueReasonSet,
265 }
266
267 /// 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 so
269 /// that adding variants upstream cannot overflow the set.
270 const VALUE_REASON_CAPACITY: usize = 16;
271
272 /// An insertion-ordered set of [`ValueReason`]s, stored inline.
273 ///
274 /// This is a deliberate replacement for `IndexSet`, enabling insertion-order
275 /// memory while avoiding any heap allocation. At `AbstractValue`'s scale, this
276 /// 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 set
278 /// can hold at most one of each variant, so the members fit into a fixed inline
279 /// array. `ValueReason` is implemented as a single byte, so this struct is
280 /// ~18 bytes on the stack.
281 ///
282 /// Insertion order is preserved deliberately: [`primary_reason`] returns the
283 /// first non-`Other` member, matching the iteration order of the `Set` used by
284 /// the TypeScript implementation this is ported from.
285 #[derive(Debug, Clone, Copy)]
286 struct ValueReasonSet {
287 /// Members in insertion order. Only the first `len` entries are meaningful.
288 members: [ValueReason; VALUE_REASON_CAPACITY],
289 len: u8,
290 }
291
292 impl Default for ValueReasonSet {
293 fn default() -> Self {
294 ValueReasonSet {
295 members: [ValueReason::Other; VALUE_REASON_CAPACITY],
296 len: 0,
297 }
298 }
299 }
300
301 impl ValueReasonSet {
302 fn single(reason: ValueReason) -> Self {
303 let mut set = Self::default();
304 set.insert(reason);
305 set
306 }
307
308 fn contains(&self, reason: ValueReason) -> bool {
309 self.members[..self.len as usize].contains(&reason)
310 }
311
312 fn iter(&self) -> impl Iterator<Item = ValueReason> + '_ {
313 self.members[..self.len as usize].iter().copied()
314 }
315
316 /// 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 }
330
331 /// 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 }
335
336 /// Adds every member of `other`, keeping `self`'s existing order and
337 /// 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 }
344
345 // =============================================================================
346 // InferenceState
347 // =============================================================================
348
349 /// The abstract state tracked during inference.
350 /// Uses interior mutability via a struct with direct fields (no Rc needed since
351 /// we always have exclusive access in the pass).
352 #[derive(Debug, Clone)]
353 struct InferenceState {
354 is_function_expression: bool,
355 /// The kind of each value, based on its allocation site
356 values: FxHashMap<ValueId, AbstractValue>,
357 /// The set of values pointed to by each identifier
358 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 location
362 /// of the Place that triggered the uninitialized access.
363 uninitialized_access: std::cell::Cell<Option<(IdentifierId, Option<SourceLocation>)>>,
364 }
365
366 impl 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 }
375
376 /// 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 }
410
411 fn initialize(&mut self, value_id: ValueId, kind: AbstractValue) {
412 self.values.insert(value_id, kind);
413 }
414
415 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 }
420
421 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 identifiers
426 // Use a deterministic ID based on the from identifier
427 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 set
440 }
441 };
442 self.variables.insert(into, values);
443 }
444
445 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 }
457
458 fn is_defined(&self, place_id: IdentifierId) -> bool {
459 self.variables.contains_key(&place_id)
460 }
461
462 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 }
468
469 #[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_kind
481 }
482
483 fn kind(&self, place_id: IdentifierId) -> AbstractValue {
484 self.kind_with_loc(place_id, None)
485 }
486
487 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 TS
490 // behavior where freeze() is never called on undefined identifiers
491 // (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 true
503 }
504 ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => false,
505 }
506 }
507
508 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 captures
517 // if enableTransitivelyFreezeFunctionExpressions is set. We skip that here
518 // since we don't have access to the function arena from within state.
519 }
520
521 #[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 }
530
531 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 }
558
559 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;
562
563 // Merge values present in both
564 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.kind
568 || !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 other
576 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 }
582
583 // Merge variables present in both
584 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 other
602 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 }
608
609 if next_variables.is_none() && next_values.is_none() {
610 None
611 } 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 }
620
621 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 merge
634 }
635 if !values.is_empty() {
636 self.variables.insert(phi_place_id, values);
637 }
638 }
639 }
640
641 #[derive(Debug, Clone, Copy)]
642 enum MutateVariant {
643 Mutate,
644 MutateConditionally,
645 MutateTransitive,
646 MutateTransitiveConditionally,
647 }
648
649 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
650 enum MutationResult {
651 None,
652 Mutate,
653 MutateFrozen,
654 MutateGlobal,
655 MutateRef,
656 }
657
658 // =============================================================================
659 // Context
660 // =============================================================================
661
662 struct 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 identity
670 /// 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 up
673 /// locally-declared functions when processing Apply effects.
674 function_values: FxHashMap<ValueId, FunctionId>,
675 /// Cache of function expression signatures, keyed by FunctionId
676 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 allocation
679 /// across fixpoint iterations.
680 aliasing_config_temp_cache: FxHashMap<(IdentifierId, String), Place>,
681 }
682
683 impl 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 }
688
689 /// 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 *self
693 .effect_value_id_cache
694 .entry(hash)
695 .or_insert_with(ValueId::new)
696 }
697 }
698
699 struct InstructionSignature {
700 effects: Vec<AliasingEffect>,
701 }
702
703 // =============================================================================
704 // Helper: hash_effect
705 // =============================================================================
706
707 fn 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> = args
718 .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.0
732 )
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.0
740 ),
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.description
766 ),
767 AliasingEffect::MutateGlobal { place, error } => format!(
768 "MutateGlobal:{}:{}:{:?}",
769 place.identifier.0, error.reason, error.description
770 ),
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> = captures
787 .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 }
799
800 // =============================================================================
801 // merge helpers
802 // =============================================================================
803
804 fn 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 }
813
814 fn 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::Primitive
844 }
845
846 // =============================================================================
847 // Pre-passes
848 // =============================================================================
849
850 fn 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();
855
856 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 }
866
867 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::HoistedConst
874 || kind == InstructionKind::HoistedFunction
875 || kind == InstructionKind::HoistedLet
876 {
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 hoisted
894 }
895
896 fn 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 }
914
915 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_spreads
922 .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::Let
939 || 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) = prop
947 {
948 candidate_non_mutating_spreads
949 .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_spreads
958 .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 them
978 }
979 InstructionValue::PropertyLoad { .. } => {
980 // Properties must be frozen since the original value was frozen
981 }
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_spreads
999 .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_spreads
1011 .get(&operand.identifier)
1012 .copied()
1013 {
1014 candidate_non_mutating_spreads.remove(&spread);
1015 }
1016 }
1017 }
1018 }
1019 }
1020 }
1021 }
1022
1023 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_mutating
1030 }
1031
1032 // =============================================================================
1033 // inferParam
1034 // =============================================================================
1035
1036 fn 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 }
1045
1046 // =============================================================================
1047 // inferBlock
1048 // =============================================================================
1049
1050 fn 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];
1058
1059 // Process phis
1060 let phis: Vec<(IdentifierId, IndexMap<BlockId, Place, FxBuildHasher>)> = block
1061 .phis
1062 .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 }
1068
1069 // Process instructions
1070 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;
1073
1074 // Compute signature if not cached
1075 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 }
1084
1085 // Apply signature
1086 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 }
1096
1097 // Process terminal
1098 // Determine what terminal action to take without holding borrows
1099 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 };
1126
1127 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.terminal
1162 {
1163 *term_effects = if terminal_effects.is_empty() {
1164 None
1165 } 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.terminal
1180 {
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 }
1192
1193 // =============================================================================
1194 // applySignature
1195 // =============================================================================
1196
1197 fn 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();
1206
1207 // For function instructions, validate frozen mutation
1208 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 } = effect
1254 {
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 }
1270
1271 // Track which values we've already initialized
1272 let mut initialized: FxHashSet<IdentifierId> = FxHashSet::default();
1273
1274 // Get the cached signature effects
1275 let sig = context.instruction_signature_cache.get(&instr_idx).unwrap();
1276 let sig_effects: Vec<AliasingEffect> = sig.effects.clone();
1277
1278 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 }
1289
1290 // 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 have
1292 // 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 }
1304
1305 Ok(if effects.is_empty() {
1306 None
1307 } else {
1308 Some(effects)
1309 })
1310 }
1311
1312 // =============================================================================
1313 // Transitive freeze helper
1314 // =============================================================================
1315
1316 /// Recursively freeze through FunctionExpression captures. If `value_id`
1317 /// corresponds to a FunctionExpression, freeze each of its context captures
1318 /// and recurse into any that are themselves FunctionExpressions. This matches
1319 /// the TS `freezeValue` → `freeze` → `freezeValue` recursion chain.
1320 fn 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 .context
1330 .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 captures
1346 freeze_function_captures_transitive(state, context, env, vid, reason);
1347 }
1348 }
1349 ValueKind::Frozen | ValueKind::Global | ValueKind::Primitive => {
1350 // Already frozen or immutable — no-op
1351 }
1352 }
1353 }
1354 }
1355 }
1356
1357 // =============================================================================
1358 // applyEffect
1359 // =============================================================================
1360
1361 fn 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 enabled
1377 // (matches TS freezeValue which recurses into func.context)
1378 let enable_transitive = env.config.enable_preserve_existing_memoization_guarantees
1379 || env.config.enable_transitively_freeze_function_expressions;
1380 if enable_transitive {
1381 // Recursively freeze through function captures. The TS
1382 // freezeValue() calls freeze() on each capture, which
1383 // calls freezeValue() again — creating a transitive
1384 // 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 types
1418 }
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());
1485
1486 // Check if function is mutable
1487 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::Mutable
1493 });
1494
1495 let inner_func = &env.functions[function_id.0 as usize];
1496 let has_tracked_side_effects = inner_func
1497 .aliasing_effects
1498 .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);
1510
1511 let captures_ref = inner_func
1512 .context
1513 .iter()
1514 .any(|operand| is_ref_or_ref_value_for_id(env, operand.identifier));
1515
1516 let is_mutable = has_captures || has_tracked_side_effects || captures_ref;
1517
1518 // Update context variable effects
1519 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::Primitive
1529 || kind == ValueKind::Frozen
1530 || kind == ValueKind::Global
1531 {
1532 // Downgrade to Read - we need to mutate the inner function
1533 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 }
1541
1542 let value_id = context.get_or_create_value_id(&effect);
1543 // Track this value as a function expression so Apply can look it up
1544 context.function_values.insert(value_id, function_id);
1545 state.initialize(
1546 value_id,
1547 AbstractValue {
1548 kind: if is_mutable {
1549 ValueKind::Mutable
1550 } else {
1551 ValueKind::Frozen
1552 },
1553 reason: ValueReasonSet::default(),
1554 },
1555 );
1556 state.define(into.identifier, value_id);
1557
1558 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 );
1583
1584 // Check destination kind
1585 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 };
1591
1592 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 };
1599
1600 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_alias
1615 {
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 = *context
1658 .effect_value_id_cache
1659 .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 = *context
1674 .effect_value_id_cache
1675 .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 expression
1702 // 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 expression
1711 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 = context
1716 .function_signature_cache
1717 .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 first
1733 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 function
1781 return Err(diagnostic);
1782 }
1783 }
1784
1785 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 }
1803
1804 // Legacy signature
1805 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 behavior
1827 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 )?;
1840
1841 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/NewExpression
1845 // where receiver === function, BOTH are skipped when !mutatesFunction.
1846 if operand.identifier == function.identifier && !mutates_function {
1847 // Don't mutate callee for non-mutating calls
1848 } 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 }
1861
1862 if *is_spread {
1863 let ty = &env.types
1864 [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 }
1877
1878 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 )?;
1890
1891 // In TS, `other === arg` compares the Place extracted from
1892 // `otherArg` with the original `arg` element. For Identifier
1893 // args, the extracted Place IS the arg, so this is a reference
1894 // identity check. For Spread args, the extracted Place is
1895 // `.place` which is never `===` the Spread wrapper object,
1896 // so NO pairs are skipped when the outer arg is a Spread
1897 // (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-op
1941 } else if mutation_kind != MutationResult::None
1942 && matches!(
1943 variant,
1944 MutateVariant::Mutate | MutateVariant::MutateTransitive
1945 )
1946 {
1947 let abstract_value = state.kind(value.identifier);
1948
1949 let ident = &env.identifiers[value.identifier.0 as usize];
1950 let decl_id = ident.declaration_id;
1951
1952 if mutation_kind == MutationResult::MutateFrozen
1953 && 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 = context
1962 .hoisted_context_declarations
1963 .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,
2001 AliasingEffect::MutateFrozen {
2002 place: value.clone(),
2003 error: diagnostic,
2004 },
2005 initialized,
2006 effects,
2007 env,
2008 func,
2009 )?;
2010 } else {
2011 let reason_str = get_write_error_reason(&abstract_value);
2012 let variable = match &ident.name {
2013 Some(react_compiler_hir::IdentifierName::Named(n)) => format!("`{}`", n),
2014 _ => "value".to_string(),
2015 };
2016 let mut diagnostic = CompilerDiagnostic::new(
2017 ErrorCategory::Immutability,
2018 "This value cannot be modified",
2019 Some(reason_str),
2020 );
2021 diagnostic.details.push(
2022 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
2023 loc: value.loc,
2024 message: Some(format!("{} cannot be modified", variable)),
2025 identifier_name: None,
2026 },
2027 );
2028
2029 if let AliasingEffect::Mutate {
2030 reason: Some(MutationReason::AssignCurrentProperty),
2031 ..
2032 } = &effect
2033 {
2034 diagnostic.details.push(react_compiler_diagnostics::CompilerDiagnosticDetail::Hint {
2035 message: "Hint: If this value is a Ref (value returned by `useRef()`), rename the variable to end in \"Ref\".".to_string(),
2036 });
2037 }
2038
2039 let error_kind = if abstract_value.kind == ValueKind::Frozen {
2040 AliasingEffect::MutateFrozen {
2041 place: value.clone(),
2042 error: diagnostic,
2043 }
2044 } else {
2045 AliasingEffect::MutateGlobal {
2046 place: value.clone(),
2047 error: diagnostic,
2048 }
2049 };
2050 apply_effect(context, state, error_kind, initialized, effects, env, func)?;
2051 }
2052 }
2053 }
2054 AliasingEffect::Impure { .. }
2055 | AliasingEffect::Render { .. }
2056 | AliasingEffect::MutateFrozen { .. }
2057 | AliasingEffect::MutateGlobal { .. } => {
2058 effects.push(effect.clone());
2059 }
2060 }
2061 Ok(())
2062 }
2063
2064 // =============================================================================
2065 // computeSignatureForInstruction
2066 // =============================================================================
2067
2068 fn compute_signature_for_instruction(
2069 context: &mut Context,
2070 env: &Environment,
2071 instr: &react_compiler_hir::Instruction,
2072 _func: &HirFunction,
2073 ) -> InstructionSignature {
2074 let lvalue = &instr.lvalue;
2075 let value = &instr.value;
2076 let mut effects: Vec<AliasingEffect> = Vec::new();
2077
2078 match value {
2079 InstructionValue::ArrayExpression { elements, .. } => {
2080 effects.push(AliasingEffect::Create {
2081 into: lvalue.clone(),
2082 value: ValueKind::Mutable,
2083 reason: ValueReason::Other,
2084 });
2085 for element in elements {
2086 match element {
2087 react_compiler_hir::ArrayElement::Place(p) => {
2088 effects.push(AliasingEffect::Capture {
2089 from: p.clone(),
2090 into: lvalue.clone(),
2091 });
2092 }
2093 react_compiler_hir::ArrayElement::Spread(s) => {
2094 let ty = &env.types
2095 [env.identifiers[s.place.identifier.0 as usize].type_.0 as usize];
2096 if let Some(mutate_iter) = conditionally_mutate_iterator(&s.place, ty) {
2097 effects.push(mutate_iter);
2098 }
2099 effects.push(AliasingEffect::Capture {
2100 from: s.place.clone(),
2101 into: lvalue.clone(),
2102 });
2103 }
2104 react_compiler_hir::ArrayElement::Hole => {}
2105 }
2106 }
2107 }
2108 InstructionValue::ObjectExpression { properties, .. } => {
2109 effects.push(AliasingEffect::Create {
2110 into: lvalue.clone(),
2111 value: ValueKind::Mutable,
2112 reason: ValueReason::Other,
2113 });
2114 for property in properties {
2115 match property {
2116 react_compiler_hir::ObjectPropertyOrSpread::Property(p) => {
2117 effects.push(AliasingEffect::Capture {
2118 from: p.place.clone(),
2119 into: lvalue.clone(),
2120 });
2121 }
2122 react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => {
2123 effects.push(AliasingEffect::Capture {
2124 from: s.place.clone(),
2125 into: lvalue.clone(),
2126 });
2127 }
2128 }
2129 }
2130 }
2131 InstructionValue::Await {
2132 value: await_value, ..
2133 } => {
2134 effects.push(AliasingEffect::Create {
2135 into: lvalue.clone(),
2136 value: ValueKind::Mutable,
2137 reason: ValueReason::Other,
2138 });
2139 effects.push(AliasingEffect::MutateTransitiveConditionally {
2140 value: await_value.clone(),
2141 });
2142 effects.push(AliasingEffect::Capture {
2143 from: await_value.clone(),
2144 into: lvalue.clone(),
2145 });
2146 }
2147 InstructionValue::NewExpression { callee, args, loc } => {
2148 let sig = get_function_call_signature(env, callee.identifier)
2149 .ok()
2150 .flatten();
2151 effects.push(AliasingEffect::Apply {
2152 receiver: callee.clone(),
2153 function: callee.clone(),
2154 mutates_function: false,
2155 args: args.iter().map(place_or_spread_to_hole).collect(),
2156 into: lvalue.clone(),
2157 signature: sig,
2158 loc: *loc,
2159 });
2160 }
2161 InstructionValue::CallExpression { callee, args, loc } => {
2162 let sig = get_function_call_signature(env, callee.identifier)
2163 .ok()
2164 .flatten();
2165 effects.push(AliasingEffect::Apply {
2166 receiver: callee.clone(),
2167 function: callee.clone(),
2168 mutates_function: true,
2169 args: args.iter().map(place_or_spread_to_hole).collect(),
2170 into: lvalue.clone(),
2171 signature: sig,
2172 loc: *loc,
2173 });
2174 }
2175 InstructionValue::MethodCall {
2176 receiver,
2177 property,
2178 args,
2179 loc,
2180 } => {
2181 let sig = get_function_call_signature(env, property.identifier)
2182 .ok()
2183 .flatten();
2184 effects.push(AliasingEffect::Apply {
2185 receiver: receiver.clone(),
2186 function: property.clone(),
2187 mutates_function: false,
2188 args: args.iter().map(place_or_spread_to_hole).collect(),
2189 into: lvalue.clone(),
2190 signature: sig,
2191 loc: *loc,
2192 });
2193 }
2194 InstructionValue::PropertyDelete { object, .. }
2195 | InstructionValue::ComputedDelete { object, .. } => {
2196 effects.push(AliasingEffect::Create {
2197 into: lvalue.clone(),
2198 value: ValueKind::Primitive,
2199 reason: ValueReason::Other,
2200 });
2201 effects.push(AliasingEffect::Mutate {
2202 value: object.clone(),
2203 reason: None,
2204 });
2205 }
2206 InstructionValue::PropertyLoad { object, .. }
2207 | InstructionValue::ComputedLoad { object, .. } => {
2208 let ty = &env.types[env.identifiers[lvalue.identifier.0 as usize].type_.0 as usize];
2209 if react_compiler_hir::is_primitive_type(ty) {
2210 effects.push(AliasingEffect::Create {
2211 into: lvalue.clone(),
2212 value: ValueKind::Primitive,
2213 reason: ValueReason::Other,
2214 });
2215 } else {
2216 effects.push(AliasingEffect::CreateFrom {
2217 from: object.clone(),
2218 into: lvalue.clone(),
2219 });
2220 }
2221 }
2222 InstructionValue::PropertyStore {
2223 object,
2224 property,
2225 value: store_value,
2226 ..
2227 } => {
2228 let mutation_reason: Option<MutationReason> = {
2229 let obj_ty =
2230 &env.types[env.identifiers[object.identifier.0 as usize].type_.0 as usize];
2231 if let react_compiler_hir::PropertyLiteral::String(prop_name) = property {
2232 if prop_name == "current" && matches!(obj_ty, Type::TypeVar { .. }) {
2233 Some(MutationReason::AssignCurrentProperty)
2234 } else {
2235 None
2236 }
2237 } else {
2238 None
2239 }
2240 };
2241 effects.push(AliasingEffect::Mutate {
2242 value: object.clone(),
2243 reason: mutation_reason,
2244 });
2245 effects.push(AliasingEffect::Capture {
2246 from: store_value.clone(),
2247 into: object.clone(),
2248 });
2249 effects.push(AliasingEffect::Create {
2250 into: lvalue.clone(),
2251 value: ValueKind::Primitive,
2252 reason: ValueReason::Other,
2253 });
2254 }
2255 InstructionValue::ComputedStore {
2256 object,
2257 value: store_value,
2258 ..
2259 } => {
2260 effects.push(AliasingEffect::Mutate {
2261 value: object.clone(),
2262 reason: None,
2263 });
2264 effects.push(AliasingEffect::Capture {
2265 from: store_value.clone(),
2266 into: object.clone(),
2267 });
2268 effects.push(AliasingEffect::Create {
2269 into: lvalue.clone(),
2270 value: ValueKind::Primitive,
2271 reason: ValueReason::Other,
2272 });
2273 }
2274 InstructionValue::FunctionExpression { lowered_func, .. }
2275 | InstructionValue::ObjectMethod { lowered_func, .. } => {
2276 let inner_func = &env.functions[lowered_func.func.0 as usize];
2277 let captures: Vec<Place> = inner_func
2278 .context
2279 .iter()
2280 .filter(|operand| operand.effect == Effect::Capture)
2281 .cloned()
2282 .collect();
2283 effects.push(AliasingEffect::CreateFunction {
2284 into: lvalue.clone(),
2285 function_id: lowered_func.func,
2286 captures,
2287 });
2288 }
2289 InstructionValue::GetIterator { collection, .. } => {
2290 effects.push(AliasingEffect::Create {
2291 into: lvalue.clone(),
2292 value: ValueKind::Mutable,
2293 reason: ValueReason::Other,
2294 });
2295 let ty = &env.types[env.identifiers[collection.identifier.0 as usize].type_.0 as usize];
2296 if is_builtin_collection_type(ty) {
2297 effects.push(AliasingEffect::Capture {
2298 from: collection.clone(),
2299 into: lvalue.clone(),
2300 });
2301 } else {
2302 effects.push(AliasingEffect::Alias {
2303 from: collection.clone(),
2304 into: lvalue.clone(),
2305 });
2306 effects.push(AliasingEffect::MutateTransitiveConditionally {
2307 value: collection.clone(),
2308 });
2309 }
2310 }
2311 InstructionValue::IteratorNext {
2312 iterator,
2313 collection,
2314 ..
2315 } => {
2316 effects.push(AliasingEffect::MutateConditionally {
2317 value: iterator.clone(),
2318 });
2319 effects.push(AliasingEffect::CreateFrom {
2320 from: collection.clone(),
2321 into: lvalue.clone(),
2322 });
2323 }
2324 InstructionValue::NextPropertyOf { .. } => {
2325 effects.push(AliasingEffect::Create {
2326 into: lvalue.clone(),
2327 value: ValueKind::Primitive,
2328 reason: ValueReason::Other,
2329 });
2330 }
2331 InstructionValue::JsxExpression {
2332 tag,
2333 props,
2334 children,
2335 ..
2336 } => {
2337 effects.push(AliasingEffect::Create {
2338 into: lvalue.clone(),
2339 value: ValueKind::Frozen,
2340 reason: ValueReason::JsxCaptured,
2341 });
2342 for operand in visitors::each_instruction_value_operand(value, env) {
2343 effects.push(AliasingEffect::Freeze {
2344 value: operand.clone(),
2345 reason: ValueReason::JsxCaptured,
2346 });
2347 effects.push(AliasingEffect::Capture {
2348 from: operand.clone(),
2349 into: lvalue.clone(),
2350 });
2351 }
2352 if let JsxTag::Place(tag_place) = tag {
2353 effects.push(AliasingEffect::Render {
2354 place: tag_place.clone(),
2355 });
2356 }
2357 if let Some(ch) = children {
2358 for child in ch {
2359 effects.push(AliasingEffect::Render {
2360 place: child.clone(),
2361 });
2362 }
2363 }
2364 for prop in props {
2365 if let react_compiler_hir::JsxAttribute::Attribute {
2366 place: prop_place, ..
2367 } = prop
2368 {
2369 let prop_ty = &env.types
2370 [env.identifiers[prop_place.identifier.0 as usize].type_.0 as usize];
2371 if let Type::Function { return_type, .. } = prop_ty {
2372 if react_compiler_hir::is_jsx_type(return_type)
2373 || is_phi_with_jsx(return_type)
2374 {
2375 effects.push(AliasingEffect::Render {
2376 place: prop_place.clone(),
2377 });
2378 }
2379 }
2380 }
2381 }
2382 }
2383 InstructionValue::JsxFragment { children: _, .. } => {
2384 effects.push(AliasingEffect::Create {
2385 into: lvalue.clone(),
2386 value: ValueKind::Frozen,
2387 reason: ValueReason::JsxCaptured,
2388 });
2389 for operand in visitors::each_instruction_value_operand(value, env) {
2390 effects.push(AliasingEffect::Freeze {
2391 value: operand.clone(),
2392 reason: ValueReason::JsxCaptured,
2393 });
2394 effects.push(AliasingEffect::Capture {
2395 from: operand.clone(),
2396 into: lvalue.clone(),
2397 });
2398 }
2399 }
2400 InstructionValue::DeclareLocal { lvalue: dl, .. } => {
2401 effects.push(AliasingEffect::Create {
2402 into: dl.place.clone(),
2403 value: ValueKind::Primitive,
2404 reason: ValueReason::Other,
2405 });
2406 effects.push(AliasingEffect::Create {
2407 into: lvalue.clone(),
2408 value: ValueKind::Primitive,
2409 reason: ValueReason::Other,
2410 });
2411 }
2412 InstructionValue::Destructure {
2413 lvalue: dl,
2414 value: dest_value,
2415 ..
2416 } => {
2417 for pat_item in each_pattern_items(&dl.pattern) {
2418 match pat_item {
2419 PatternItem::Place(place) => {
2420 let ty = &env.types
2421 [env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2422 if react_compiler_hir::is_primitive_type(ty) {
2423 effects.push(AliasingEffect::Create {
2424 into: place.clone(),
2425 value: ValueKind::Primitive,
2426 reason: ValueReason::Other,
2427 });
2428 } else {
2429 effects.push(AliasingEffect::CreateFrom {
2430 from: dest_value.clone(),
2431 into: place.clone(),
2432 });
2433 }
2434 }
2435 PatternItem::Spread(place) => {
2436 let value_kind = if context.non_mutating_spreads.contains(&place.identifier)
2437 {
2438 ValueKind::Frozen
2439 } else {
2440 ValueKind::Mutable
2441 };
2442 effects.push(AliasingEffect::Create {
2443 into: place.clone(),
2444 reason: ValueReason::Other,
2445 value: value_kind,
2446 });
2447 effects.push(AliasingEffect::Capture {
2448 from: dest_value.clone(),
2449 into: place.clone(),
2450 });
2451 }
2452 }
2453 }
2454 effects.push(AliasingEffect::Assign {
2455 from: dest_value.clone(),
2456 into: lvalue.clone(),
2457 });
2458 }
2459 InstructionValue::LoadContext { place, .. } => {
2460 effects.push(AliasingEffect::CreateFrom {
2461 from: place.clone(),
2462 into: lvalue.clone(),
2463 });
2464 }
2465 InstructionValue::DeclareContext { lvalue: dcl, .. } => {
2466 let decl_id = env.identifiers[dcl.place.identifier.0 as usize].declaration_id;
2467 let kind = dcl.kind;
2468 if !context.hoisted_context_declarations.contains_key(&decl_id)
2469 || kind == InstructionKind::HoistedConst
2470 || kind == InstructionKind::HoistedFunction
2471 || kind == InstructionKind::HoistedLet
2472 {
2473 effects.push(AliasingEffect::Create {
2474 into: dcl.place.clone(),
2475 value: ValueKind::Mutable,
2476 reason: ValueReason::Other,
2477 });
2478 } else {
2479 effects.push(AliasingEffect::Mutate {
2480 value: dcl.place.clone(),
2481 reason: None,
2482 });
2483 }
2484 effects.push(AliasingEffect::Create {
2485 into: lvalue.clone(),
2486 value: ValueKind::Primitive,
2487 reason: ValueReason::Other,
2488 });
2489 }
2490 InstructionValue::StoreContext {
2491 lvalue: scl,
2492 value: sc_value,
2493 ..
2494 } => {
2495 let decl_id = env.identifiers[scl.place.identifier.0 as usize].declaration_id;
2496 if scl.kind == InstructionKind::Reassign
2497 || context.hoisted_context_declarations.contains_key(&decl_id)
2498 {
2499 effects.push(AliasingEffect::Mutate {
2500 value: scl.place.clone(),
2501 reason: None,
2502 });
2503 } else {
2504 effects.push(AliasingEffect::Create {
2505 into: scl.place.clone(),
2506 value: ValueKind::Mutable,
2507 reason: ValueReason::Other,
2508 });
2509 }
2510 effects.push(AliasingEffect::Capture {
2511 from: sc_value.clone(),
2512 into: scl.place.clone(),
2513 });
2514 effects.push(AliasingEffect::Assign {
2515 from: sc_value.clone(),
2516 into: lvalue.clone(),
2517 });
2518 }
2519 InstructionValue::LoadLocal { place, .. } => {
2520 effects.push(AliasingEffect::Assign {
2521 from: place.clone(),
2522 into: lvalue.clone(),
2523 });
2524 }
2525 InstructionValue::StoreLocal {
2526 lvalue: sl,
2527 value: sl_value,
2528 ..
2529 } => {
2530 effects.push(AliasingEffect::Assign {
2531 from: sl_value.clone(),
2532 into: sl.place.clone(),
2533 });
2534 effects.push(AliasingEffect::Assign {
2535 from: sl_value.clone(),
2536 into: lvalue.clone(),
2537 });
2538 }
2539 InstructionValue::PostfixUpdate {
2540 lvalue: pf_lvalue, ..
2541 }
2542 | InstructionValue::PrefixUpdate {
2543 lvalue: pf_lvalue, ..
2544 } => {
2545 effects.push(AliasingEffect::Create {
2546 into: lvalue.clone(),
2547 value: ValueKind::Primitive,
2548 reason: ValueReason::Other,
2549 });
2550 effects.push(AliasingEffect::Create {
2551 into: pf_lvalue.clone(),
2552 value: ValueKind::Primitive,
2553 reason: ValueReason::Other,
2554 });
2555 }
2556 InstructionValue::StoreGlobal {
2557 name,
2558 value: sg_value,
2559 loc: _,
2560 ..
2561 } => {
2562 let variable = format!("`{}`", name);
2563 let mut diagnostic = CompilerDiagnostic::new(
2564 ErrorCategory::Globals,
2565 "Cannot reassign variables declared outside of the component/hook",
2566 Some(format!(
2567 "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)",
2568 variable
2569 )),
2570 );
2571 diagnostic.details.push(
2572 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
2573 loc: instr.loc,
2574 message: Some(format!("{} cannot be reassigned", variable)),
2575 identifier_name: None,
2576 },
2577 );
2578 effects.push(AliasingEffect::MutateGlobal {
2579 place: sg_value.clone(),
2580 error: diagnostic,
2581 });
2582 effects.push(AliasingEffect::Assign {
2583 from: sg_value.clone(),
2584 into: lvalue.clone(),
2585 });
2586 }
2587 InstructionValue::TypeCastExpression {
2588 value: tc_value, ..
2589 } => {
2590 effects.push(AliasingEffect::Assign {
2591 from: tc_value.clone(),
2592 into: lvalue.clone(),
2593 });
2594 }
2595 InstructionValue::LoadGlobal { .. } => {
2596 effects.push(AliasingEffect::Create {
2597 into: lvalue.clone(),
2598 value: ValueKind::Global,
2599 reason: ValueReason::Global,
2600 });
2601 }
2602 InstructionValue::StartMemoize { .. } | InstructionValue::FinishMemoize { .. } => {
2603 if env.config.enable_preserve_existing_memoization_guarantees {
2604 for operand in visitors::each_instruction_value_operand(value, env) {
2605 effects.push(AliasingEffect::Freeze {
2606 value: operand.clone(),
2607 reason: ValueReason::HookCaptured,
2608 });
2609 }
2610 }
2611 effects.push(AliasingEffect::Create {
2612 into: lvalue.clone(),
2613 value: ValueKind::Primitive,
2614 reason: ValueReason::Other,
2615 });
2616 }
2617 // All primitive-creating instructions
2618 InstructionValue::TaggedTemplateExpression { .. }
2619 | InstructionValue::BinaryExpression { .. }
2620 | InstructionValue::Debugger { .. }
2621 | InstructionValue::JSXText { .. }
2622 | InstructionValue::MetaProperty { .. }
2623 | InstructionValue::Primitive { .. }
2624 | InstructionValue::RegExpLiteral { .. }
2625 | InstructionValue::TemplateLiteral { .. }
2626 | InstructionValue::UnaryExpression { .. }
2627 | InstructionValue::UnsupportedNode { .. } => {
2628 effects.push(AliasingEffect::Create {
2629 into: lvalue.clone(),
2630 value: ValueKind::Primitive,
2631 reason: ValueReason::Other,
2632 });
2633 }
2634 }
2635
2636 InstructionSignature { effects }
2637 }
2638
2639 // =============================================================================
2640 // Legacy signature support
2641 // =============================================================================
2642
2643 fn compute_effects_for_legacy_signature(
2644 state: &InferenceState,
2645 signature: &FunctionSignature,
2646 lvalue: &Place,
2647 receiver: &Place,
2648 args: &[PlaceOrSpreadOrHole],
2649 _loc: Option<&SourceLocation>,
2650 env: &Environment,
2651 function_values: &FxHashMap<ValueId, FunctionId>,
2652 todo_errors: &mut Vec<react_compiler_diagnostics::CompilerErrorDetail>,
2653 ) -> Vec<AliasingEffect> {
2654 let return_value_reason = signature.return_value_reason.unwrap_or(ValueReason::Other);
2655 let mut effects: Vec<AliasingEffect> = Vec::new();
2656
2657 effects.push(AliasingEffect::Create {
2658 into: lvalue.clone(),
2659 value: signature.return_value_kind,
2660 reason: return_value_reason,
2661 });
2662
2663 if signature.impure && env.config.validate_no_impure_functions_in_render {
2664 let mut diagnostic = CompilerDiagnostic::new(
2665 ErrorCategory::Purity,
2666 "Cannot call impure function during render",
2667 Some(format!(
2668 "{}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)",
2669 if let Some(ref name) = signature.canonical_name {
2670 format!("`{}` is an impure function. ", name)
2671 } else {
2672 String::new()
2673 }
2674 )),
2675 );
2676 diagnostic.details.push(
2677 react_compiler_diagnostics::CompilerDiagnosticDetail::Error {
2678 loc: _loc.copied(),
2679 message: Some("Cannot call impure function".to_string()),
2680 identifier_name: None,
2681 },
2682 );
2683 effects.push(AliasingEffect::Impure {
2684 place: receiver.clone(),
2685 error: diagnostic,
2686 });
2687 }
2688
2689 // TODO: check signature.known_incompatible and throw (TS line 2351-2370)
2690 // This requires threading Result through apply_effect/apply_signature.
2691
2692 // If the function is mutable only if operands are mutable, and all
2693 // arguments are immutable/non-mutating, short-circuit with simple aliasing.
2694 if signature.mutable_only_if_operands_are_mutable
2695 && are_arguments_immutable_and_non_mutating(state, args, env, function_values)
2696 {
2697 effects.push(AliasingEffect::Alias {
2698 from: receiver.clone(),
2699 into: lvalue.clone(),
2700 });
2701 for arg in args {
2702 match arg {
2703 PlaceOrSpreadOrHole::Hole => continue,
2704 PlaceOrSpreadOrHole::Place(place)
2705 | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2706 effects.push(AliasingEffect::ImmutableCapture {
2707 from: place.clone(),
2708 into: lvalue.clone(),
2709 });
2710 }
2711 }
2712 }
2713 return effects;
2714 }
2715
2716 let mut stores: Vec<Place> = Vec::new();
2717 let mut captures: Vec<Place> = Vec::new();
2718
2719 let mut visit = |place: &Place, effect: Effect, effects: &mut Vec<AliasingEffect>| match effect
2720 {
2721 Effect::Store => {
2722 effects.push(AliasingEffect::Mutate {
2723 value: place.clone(),
2724 reason: None,
2725 });
2726 stores.push(place.clone());
2727 }
2728 Effect::Capture => {
2729 captures.push(place.clone());
2730 }
2731 Effect::ConditionallyMutate => {
2732 effects.push(AliasingEffect::MutateTransitiveConditionally {
2733 value: place.clone(),
2734 });
2735 }
2736 Effect::ConditionallyMutateIterator => {
2737 let ty = &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2738 if let Some(mutate_iter) = conditionally_mutate_iterator(place, ty) {
2739 effects.push(mutate_iter);
2740 }
2741 effects.push(AliasingEffect::Capture {
2742 from: place.clone(),
2743 into: lvalue.clone(),
2744 });
2745 }
2746 Effect::Freeze => {
2747 effects.push(AliasingEffect::Freeze {
2748 value: place.clone(),
2749 reason: return_value_reason,
2750 });
2751 }
2752 Effect::Mutate => {
2753 effects.push(AliasingEffect::MutateTransitive {
2754 value: place.clone(),
2755 });
2756 }
2757 Effect::Read => {
2758 effects.push(AliasingEffect::ImmutableCapture {
2759 from: place.clone(),
2760 into: lvalue.clone(),
2761 });
2762 }
2763 _ => {}
2764 };
2765
2766 if signature.callee_effect != Effect::Capture {
2767 effects.push(AliasingEffect::Alias {
2768 from: receiver.clone(),
2769 into: lvalue.clone(),
2770 });
2771 }
2772
2773 visit(receiver, signature.callee_effect, &mut effects);
2774 for (i, arg) in args.iter().enumerate() {
2775 match arg {
2776 PlaceOrSpreadOrHole::Hole => continue,
2777 PlaceOrSpreadOrHole::Place(place)
2778 | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2779 let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_));
2780 let sig_effect = if !is_spread && i < signature.positional_params.len() {
2781 signature.positional_params[i]
2782 } else {
2783 signature.rest_param.unwrap_or(Effect::ConditionallyMutate)
2784 };
2785 let (effect, err_detail) = get_argument_effect(sig_effect, is_spread, place.loc);
2786 if let Some(d) = err_detail {
2787 todo_errors.push(d);
2788 }
2789 visit(place, effect, &mut effects);
2790 }
2791 }
2792 }
2793
2794 if !captures.is_empty() {
2795 if stores.is_empty() {
2796 for capture in &captures {
2797 effects.push(AliasingEffect::Alias {
2798 from: capture.clone(),
2799 into: lvalue.clone(),
2800 });
2801 }
2802 } else {
2803 for capture in &captures {
2804 for store in &stores {
2805 effects.push(AliasingEffect::Capture {
2806 from: capture.clone(),
2807 into: store.clone(),
2808 });
2809 }
2810 }
2811 }
2812 }
2813
2814 effects
2815 }
2816
2817 fn get_argument_effect(
2818 sig_effect: Effect,
2819 is_spread: bool,
2820 spread_loc: Option<SourceLocation>,
2821 ) -> (
2822 Effect,
2823 Option<react_compiler_diagnostics::CompilerErrorDetail>,
2824 ) {
2825 if !is_spread {
2826 (sig_effect, None)
2827 } else if sig_effect == Effect::Mutate || sig_effect == Effect::ConditionallyMutate {
2828 (sig_effect, None)
2829 } else {
2830 // Spread with Freeze effect is unsupported for hook arguments
2831 // (matches TS CompilerError.throwTodo)
2832 let detail = if sig_effect == Effect::Freeze {
2833 Some(react_compiler_diagnostics::CompilerErrorDetail {
2834 reason: "Support spread syntax for hook arguments".to_string(),
2835 description: None,
2836 category: ErrorCategory::Todo,
2837 loc: spread_loc,
2838 suggestions: None,
2839 })
2840 } else {
2841 None
2842 };
2843 (Effect::ConditionallyMutateIterator, detail)
2844 }
2845 }
2846
2847 /// Returns true if all of the arguments are both non-mutable (immutable or frozen)
2848 /// _and_ are not functions which might mutate their arguments.
2849 ///
2850 /// Corresponds to TS `areArgumentsImmutableAndNonMutating`.
2851 fn are_arguments_immutable_and_non_mutating(
2852 state: &InferenceState,
2853 args: &[PlaceOrSpreadOrHole],
2854 env: &Environment,
2855 function_values: &FxHashMap<ValueId, FunctionId>,
2856 ) -> bool {
2857 for arg in args {
2858 match arg {
2859 PlaceOrSpreadOrHole::Hole => continue,
2860 PlaceOrSpreadOrHole::Place(place)
2861 | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2862 // Check if it's a function type with a known signature
2863 let is_place = matches!(arg, PlaceOrSpreadOrHole::Place(_));
2864 if is_place {
2865 let ty =
2866 &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2867 if let Type::Function { .. } = ty {
2868 let fn_shape = env.get_function_signature(ty).ok().flatten();
2869 if let Some(fn_sig) = fn_shape {
2870 let has_mutable_param = fn_sig
2871 .positional_params
2872 .iter()
2873 .any(|e| is_known_mutable_effect(*e));
2874 let has_mutable_rest = fn_sig
2875 .rest_param
2876 .map_or(false, |e| is_known_mutable_effect(e));
2877 return !has_mutable_param && !has_mutable_rest;
2878 }
2879 }
2880 }
2881
2882 let kind = state.kind(place.identifier);
2883 match kind.kind {
2884 ValueKind::Primitive | ValueKind::Frozen => {
2885 // Immutable values are ok, continue checking
2886 }
2887 _ => {
2888 return false;
2889 }
2890 }
2891
2892 // Check if any value for this place is a function expression
2893 // that mutates its parameters (TS lines 2545-2557)
2894 let value_ids = state.values_for(place.identifier);
2895 for vid in &value_ids {
2896 if let Some(&func_id) = function_values.get(vid) {
2897 let inner_func = &env.functions[func_id.0 as usize];
2898 let mutates_params = inner_func.params.iter().any(|param| {
2899 let param_id = match param {
2900 ParamPattern::Place(p) => p.identifier,
2901 ParamPattern::Spread(s) => s.place.identifier,
2902 };
2903 let ident = &env.identifiers[param_id.0 as usize];
2904 ident.mutable_range.end.0 > ident.mutable_range.start.0 + 1
2905 });
2906 if mutates_params {
2907 return false;
2908 }
2909 }
2910 }
2911 }
2912 }
2913 }
2914 true
2915 }
2916
2917 fn is_known_mutable_effect(effect: Effect) -> bool {
2918 matches!(
2919 effect,
2920 Effect::Store
2921 | Effect::Mutate
2922 | Effect::ConditionallyMutate
2923 | Effect::ConditionallyMutateIterator
2924 )
2925 }
2926
2927 // =============================================================================
2928 // Aliasing signature config support (new-style signatures)
2929 // =============================================================================
2930
2931 fn compute_effects_for_aliasing_signature_config(
2932 env: &mut Environment,
2933 config: &react_compiler_hir::type_config::AliasingSignatureConfig,
2934 lvalue: &Place,
2935 receiver: &Place,
2936 args: &[PlaceOrSpreadOrHole],
2937 context: &[Place],
2938 _loc: Option<&SourceLocation>,
2939 temp_cache: &mut FxHashMap<(IdentifierId, String), Place>,
2940 ) -> Result<Option<Vec<AliasingEffect>>, CompilerDiagnostic> {
2941 // Build substitutions from config strings to places
2942 let mut substitutions: FxHashMap<String, Vec<Place>> = FxHashMap::default();
2943 substitutions.insert(config.receiver.clone(), vec![receiver.clone()]);
2944 substitutions.insert(config.returns.clone(), vec![lvalue.clone()]);
2945
2946 let mut mutable_spreads: FxHashSet<IdentifierId> = FxHashSet::default();
2947
2948 for (i, arg) in args.iter().enumerate() {
2949 match arg {
2950 PlaceOrSpreadOrHole::Hole => continue,
2951 PlaceOrSpreadOrHole::Place(place)
2952 | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
2953 if i < config.params.len() && !matches!(arg, PlaceOrSpreadOrHole::Spread(_)) {
2954 substitutions.insert(config.params[i].clone(), vec![place.clone()]);
2955 } else if let Some(ref rest) = config.rest {
2956 substitutions
2957 .entry(rest.clone())
2958 .or_default()
2959 .push(place.clone());
2960 } else {
2961 return Ok(None);
2962 }
2963
2964 if matches!(arg, PlaceOrSpreadOrHole::Spread(_)) {
2965 let ty =
2966 &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
2967 let mutate_iterator = conditionally_mutate_iterator(place, ty);
2968 if mutate_iterator.is_some() {
2969 mutable_spreads.insert(place.identifier);
2970 }
2971 }
2972 }
2973 }
2974 }
2975
2976 for operand in context {
2977 let ident = &env.identifiers[operand.identifier.0 as usize];
2978 if let Some(ref name) = ident.name {
2979 substitutions.insert(format!("@{}", name.value()), vec![operand.clone()]);
2980 }
2981 }
2982
2983 // Create temporaries (cached by lvalue + temp_name to be stable across fixpoint iterations)
2984 for temp_name in &config.temporaries {
2985 let cache_key = (lvalue.identifier, temp_name.clone());
2986 let temp_place = temp_cache
2987 .entry(cache_key)
2988 .or_insert_with(|| create_temp_place(env, receiver.loc))
2989 .clone();
2990 substitutions.insert(temp_name.clone(), vec![temp_place]);
2991 }
2992
2993 let mut effects: Vec<AliasingEffect> = Vec::new();
2994
2995 for eff_config in &config.effects {
2996 match eff_config {
2997 react_compiler_hir::type_config::AliasingEffectConfig::Freeze { value, reason } => {
2998 let values = substitutions.get(value).cloned().unwrap_or_default();
2999 for v in values {
3000 if mutable_spreads.contains(&v.identifier) {
3001 return Err(CompilerDiagnostic::todo(
3002 "Support spread syntax for hook arguments",
3003 v.loc,
3004 ));
3005 }
3006 effects.push(AliasingEffect::Freeze { value: v, reason: *reason });
3007 }
3008 }
3009 react_compiler_hir::type_config::AliasingEffectConfig::Create { into, value, reason } => {
3010 let intos = substitutions.get(into).cloned().unwrap_or_default();
3011 for v in intos {
3012 effects.push(AliasingEffect::Create { into: v, value: *value, reason: *reason });
3013 }
3014 }
3015 react_compiler_hir::type_config::AliasingEffectConfig::CreateFrom { from, into } => {
3016 let froms = substitutions.get(from).cloned().unwrap_or_default();
3017 let intos = substitutions.get(into).cloned().unwrap_or_default();
3018 for f in &froms {
3019 for t in &intos {
3020 effects.push(AliasingEffect::CreateFrom { from: f.clone(), into: t.clone() });
3021 }
3022 }
3023 }
3024 react_compiler_hir::type_config::AliasingEffectConfig::Assign { from, into } => {
3025 let froms = substitutions.get(from).cloned().unwrap_or_default();
3026 let intos = substitutions.get(into).cloned().unwrap_or_default();
3027 for f in &froms {
3028 for t in &intos {
3029 effects.push(AliasingEffect::Assign { from: f.clone(), into: t.clone() });
3030 }
3031 }
3032 }
3033 react_compiler_hir::type_config::AliasingEffectConfig::Alias { from, into } => {
3034 let froms = substitutions.get(from).cloned().unwrap_or_default();
3035 let intos = substitutions.get(into).cloned().unwrap_or_default();
3036 for f in &froms {
3037 for t in &intos {
3038 effects.push(AliasingEffect::Alias { from: f.clone(), into: t.clone() });
3039 }
3040 }
3041 }
3042 react_compiler_hir::type_config::AliasingEffectConfig::Capture { from, into } => {
3043 let froms = substitutions.get(from).cloned().unwrap_or_default();
3044 let intos = substitutions.get(into).cloned().unwrap_or_default();
3045 for f in &froms {
3046 for t in &intos {
3047 effects.push(AliasingEffect::Capture { from: f.clone(), into: t.clone() });
3048 }
3049 }
3050 }
3051 react_compiler_hir::type_config::AliasingEffectConfig::ImmutableCapture { from, into } => {
3052 let froms = substitutions.get(from).cloned().unwrap_or_default();
3053 let intos = substitutions.get(into).cloned().unwrap_or_default();
3054 for f in &froms {
3055 for t in &intos {
3056 effects.push(AliasingEffect::ImmutableCapture { from: f.clone(), into: t.clone() });
3057 }
3058 }
3059 }
3060 react_compiler_hir::type_config::AliasingEffectConfig::Impure { place } => {
3061 let values = substitutions.get(place).cloned().unwrap_or_default();
3062 for v in values {
3063 effects.push(AliasingEffect::Impure {
3064 place: v,
3065 error: CompilerDiagnostic::new(ErrorCategory::Purity, "Impure function call", None),
3066 });
3067 }
3068 }
3069 react_compiler_hir::type_config::AliasingEffectConfig::Mutate { value } => {
3070 let values = substitutions.get(value).cloned().unwrap_or_default();
3071 for v in values {
3072 effects.push(AliasingEffect::Mutate { value: v, reason: None });
3073 }
3074 }
3075 react_compiler_hir::type_config::AliasingEffectConfig::MutateTransitiveConditionally { value } => {
3076 let values = substitutions.get(value).cloned().unwrap_or_default();
3077 for v in values {
3078 effects.push(AliasingEffect::MutateTransitiveConditionally { value: v });
3079 }
3080 }
3081 react_compiler_hir::type_config::AliasingEffectConfig::Apply { receiver: r, function: f, mutates_function, args: a, into: i } => {
3082 let recv = substitutions.get(r).and_then(|v| v.first()).cloned();
3083 let func = substitutions.get(f).and_then(|v| v.first()).cloned();
3084 let into = substitutions.get(i).and_then(|v| v.first()).cloned();
3085 if let (Some(recv), Some(func), Some(into)) = (recv, func, into) {
3086 let mut apply_args: Vec<PlaceOrSpreadOrHole> = Vec::new();
3087 for arg in a {
3088 match arg {
3089 react_compiler_hir::type_config::ApplyArgConfig::Hole { .. } => {
3090 apply_args.push(PlaceOrSpreadOrHole::Hole);
3091 }
3092 react_compiler_hir::type_config::ApplyArgConfig::Place(name) => {
3093 if let Some(places) = substitutions.get(name) {
3094 if let Some(p) = places.first() {
3095 apply_args.push(PlaceOrSpreadOrHole::Place(p.clone()));
3096 }
3097 }
3098 }
3099 react_compiler_hir::type_config::ApplyArgConfig::Spread { place: name, .. } => {
3100 if let Some(places) = substitutions.get(name) {
3101 if let Some(p) = places.first() {
3102 apply_args.push(PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place: p.clone() }));
3103 }
3104 }
3105 }
3106 }
3107 }
3108 effects.push(AliasingEffect::Apply {
3109 receiver: recv,
3110 function: func,
3111 mutates_function: *mutates_function,
3112 args: apply_args,
3113 into,
3114 signature: None,
3115 loc: _loc.copied(),
3116 });
3117 } else {
3118 return Ok(None);
3119 }
3120 }
3121 }
3122 }
3123
3124 Ok(Some(effects))
3125 }
3126
3127 // =============================================================================
3128 // Function expression signature building
3129 // =============================================================================
3130
3131 /// Build an AliasingSignature from a function expression's params/returns/aliasing effects.
3132 /// Corresponds to TS `buildSignatureFromFunctionExpression`.
3133 fn build_signature_from_function_expression(
3134 env: &mut Environment,
3135 func_id: FunctionId,
3136 ) -> AliasingSignature {
3137 let inner_func = &env.functions[func_id.0 as usize];
3138 let mut params: Vec<IdentifierId> = Vec::new();
3139 let mut rest: Option<IdentifierId> = None;
3140 for param in &inner_func.params {
3141 match param {
3142 ParamPattern::Place(p) => params.push(p.identifier),
3143 ParamPattern::Spread(s) => rest = Some(s.place.identifier),
3144 }
3145 }
3146 let returns = inner_func.returns.identifier;
3147 let aliasing_effects = inner_func.aliasing_effects.clone().unwrap_or_default();
3148 let loc = inner_func.loc;
3149
3150 if rest.is_none() {
3151 let temp = create_temp_place(env, loc);
3152 rest = Some(temp.identifier);
3153 }
3154
3155 AliasingSignature {
3156 receiver: IdentifierId(0),
3157 params,
3158 rest,
3159 returns,
3160 effects: aliasing_effects,
3161 temporaries: Vec::new(),
3162 }
3163 }
3164
3165 /// Compute effects by substituting an AliasingSignature (IdentifierId-based)
3166 /// with actual arguments. Corresponds to TS `computeEffectsForSignature`.
3167 fn compute_effects_for_aliasing_signature(
3168 env: &mut Environment,
3169 signature: &AliasingSignature,
3170 lvalue: &Place,
3171 receiver: &Place,
3172 args: &[PlaceOrSpreadOrHole],
3173 context: &[Place],
3174 _loc: Option<&SourceLocation>,
3175 ) -> Result<Option<Vec<AliasingEffect>>, CompilerDiagnostic> {
3176 if signature.params.len() > args.len()
3177 || (args.len() > signature.params.len() && signature.rest.is_none())
3178 {
3179 return Ok(None);
3180 }
3181
3182 let mut mutable_spreads: FxHashSet<IdentifierId> = FxHashSet::default();
3183 let mut substitutions: FxHashMap<IdentifierId, Vec<Place>> = FxHashMap::default();
3184 substitutions.insert(signature.receiver, vec![receiver.clone()]);
3185 substitutions.insert(signature.returns, vec![lvalue.clone()]);
3186
3187 for (i, arg) in args.iter().enumerate() {
3188 match arg {
3189 PlaceOrSpreadOrHole::Hole => continue,
3190 PlaceOrSpreadOrHole::Place(place)
3191 | PlaceOrSpreadOrHole::Spread(react_compiler_hir::SpreadPattern { place }) => {
3192 let is_spread = matches!(arg, PlaceOrSpreadOrHole::Spread(_));
3193 if !is_spread && i < signature.params.len() {
3194 substitutions.insert(signature.params[i], vec![place.clone()]);
3195 } else if let Some(rest_id) = signature.rest {
3196 substitutions
3197 .entry(rest_id)
3198 .or_default()
3199 .push(place.clone());
3200 } else {
3201 return Ok(None);
3202 }
3203
3204 if is_spread {
3205 let ty =
3206 &env.types[env.identifiers[place.identifier.0 as usize].type_.0 as usize];
3207 let mutate_iterator = conditionally_mutate_iterator(place, ty);
3208 if mutate_iterator.is_some() {
3209 mutable_spreads.insert(place.identifier);
3210 }
3211 }
3212 }
3213 }
3214 }
3215
3216 // Add context variable substitutions (identity mapping)
3217 for operand in context {
3218 substitutions.insert(operand.identifier, vec![operand.clone()]);
3219 }
3220
3221 // Create temporaries
3222 for temp in &signature.temporaries {
3223 let temp_place = create_temp_place(env, receiver.loc);
3224 substitutions.insert(temp.identifier, vec![temp_place]);
3225 }
3226
3227 let mut effects: Vec<AliasingEffect> = Vec::new();
3228
3229 for eff in &signature.effects {
3230 match eff {
3231 AliasingEffect::MaybeAlias { from, into }
3232 | AliasingEffect::Assign { from, into }
3233 | AliasingEffect::ImmutableCapture { from, into }
3234 | AliasingEffect::Alias { from, into }
3235 | AliasingEffect::CreateFrom { from, into }
3236 | AliasingEffect::Capture { from, into } => {
3237 let from_places = substitutions
3238 .get(&from.identifier)
3239 .cloned()
3240 .unwrap_or_default();
3241 let to_places = substitutions
3242 .get(&into.identifier)
3243 .cloned()
3244 .unwrap_or_default();
3245 for f in &from_places {
3246 for t in &to_places {
3247 effects.push(match eff {
3248 AliasingEffect::MaybeAlias { .. } => AliasingEffect::MaybeAlias {
3249 from: f.clone(),
3250 into: t.clone(),
3251 },
3252 AliasingEffect::Assign { .. } => AliasingEffect::Assign {
3253 from: f.clone(),
3254 into: t.clone(),
3255 },
3256 AliasingEffect::ImmutableCapture { .. } => {
3257 AliasingEffect::ImmutableCapture {
3258 from: f.clone(),
3259 into: t.clone(),
3260 }
3261 }
3262 AliasingEffect::Alias { .. } => AliasingEffect::Alias {
3263 from: f.clone(),
3264 into: t.clone(),
3265 },
3266 AliasingEffect::CreateFrom { .. } => AliasingEffect::CreateFrom {
3267 from: f.clone(),
3268 into: t.clone(),
3269 },
3270 AliasingEffect::Capture { .. } => AliasingEffect::Capture {
3271 from: f.clone(),
3272 into: t.clone(),
3273 },
3274 _ => unreachable!(),
3275 });
3276 }
3277 }
3278 }
3279 AliasingEffect::Impure { place, error } => {
3280 let values = substitutions
3281 .get(&place.identifier)
3282 .cloned()
3283 .unwrap_or_default();
3284 for v in values {
3285 effects.push(AliasingEffect::Impure {
3286 place: v,
3287 error: error.clone(),
3288 });
3289 }
3290 }
3291 AliasingEffect::MutateFrozen { place, error } => {
3292 let values = substitutions
3293 .get(&place.identifier)
3294 .cloned()
3295 .unwrap_or_default();
3296 for v in values {
3297 effects.push(AliasingEffect::MutateFrozen {
3298 place: v,
3299 error: error.clone(),
3300 });
3301 }
3302 }
3303 AliasingEffect::MutateGlobal { place, error } => {
3304 let values = substitutions
3305 .get(&place.identifier)
3306 .cloned()
3307 .unwrap_or_default();
3308 for v in values {
3309 effects.push(AliasingEffect::MutateGlobal {
3310 place: v,
3311 error: error.clone(),
3312 });
3313 }
3314 }
3315 AliasingEffect::Render { place } => {
3316 let values = substitutions
3317 .get(&place.identifier)
3318 .cloned()
3319 .unwrap_or_default();
3320 for v in values {
3321 effects.push(AliasingEffect::Render { place: v });
3322 }
3323 }
3324 AliasingEffect::Mutate { value, reason } => {
3325 let values = substitutions
3326 .get(&value.identifier)
3327 .cloned()
3328 .unwrap_or_default();
3329 for v in values {
3330 effects.push(AliasingEffect::Mutate {
3331 value: v,
3332 reason: reason.clone(),
3333 });
3334 }
3335 }
3336 AliasingEffect::MutateConditionally { value } => {
3337 let values = substitutions
3338 .get(&value.identifier)
3339 .cloned()
3340 .unwrap_or_default();
3341 for v in values {
3342 effects.push(AliasingEffect::MutateConditionally { value: v });
3343 }
3344 }
3345 AliasingEffect::MutateTransitive { value } => {
3346 let values = substitutions
3347 .get(&value.identifier)
3348 .cloned()
3349 .unwrap_or_default();
3350 for v in values {
3351 effects.push(AliasingEffect::MutateTransitive { value: v });
3352 }
3353 }
3354 AliasingEffect::MutateTransitiveConditionally { value } => {
3355 let values = substitutions
3356 .get(&value.identifier)
3357 .cloned()
3358 .unwrap_or_default();
3359 for v in values {
3360 effects.push(AliasingEffect::MutateTransitiveConditionally { value: v });
3361 }
3362 }
3363 AliasingEffect::Freeze { value, reason } => {
3364 let values = substitutions
3365 .get(&value.identifier)
3366 .cloned()
3367 .unwrap_or_default();
3368 for v in values {
3369 if mutable_spreads.contains(&v.identifier) {
3370 return Err(CompilerDiagnostic::todo(
3371 "Support spread syntax for hook arguments",
3372 v.loc,
3373 ));
3374 }
3375 effects.push(AliasingEffect::Freeze {
3376 value: v,
3377 reason: *reason,
3378 });
3379 }
3380 }
3381 AliasingEffect::Create {
3382 into,
3383 value,
3384 reason,
3385 } => {
3386 let intos = substitutions
3387 .get(&into.identifier)
3388 .cloned()
3389 .unwrap_or_default();
3390 for v in intos {
3391 effects.push(AliasingEffect::Create {
3392 into: v,
3393 value: *value,
3394 reason: *reason,
3395 });
3396 }
3397 }
3398 AliasingEffect::Apply {
3399 receiver: r,
3400 function: f,
3401 mutates_function: mf,
3402 args: a,
3403 into: i,
3404 signature: s,
3405 loc: _l,
3406 } => {
3407 let recv = substitutions
3408 .get(&r.identifier)
3409 .and_then(|v| v.first())
3410 .cloned();
3411 let func = substitutions
3412 .get(&f.identifier)
3413 .and_then(|v| v.first())
3414 .cloned();
3415 let apply_into = substitutions
3416 .get(&i.identifier)
3417 .and_then(|v| v.first())
3418 .cloned();
3419 if let (Some(recv), Some(func), Some(apply_into)) = (recv, func, apply_into) {
3420 let mut apply_args: Vec<PlaceOrSpreadOrHole> = Vec::new();
3421 for arg in a {
3422 match arg {
3423 PlaceOrSpreadOrHole::Hole => apply_args.push(PlaceOrSpreadOrHole::Hole),
3424 PlaceOrSpreadOrHole::Place(p) => {
3425 if let Some(places) = substitutions.get(&p.identifier) {
3426 if let Some(place) = places.first() {
3427 apply_args.push(PlaceOrSpreadOrHole::Place(place.clone()));
3428 }
3429 }
3430 }
3431 PlaceOrSpreadOrHole::Spread(sp) => {
3432 if let Some(places) = substitutions.get(&sp.place.identifier) {
3433 if let Some(place) = places.first() {
3434 apply_args.push(PlaceOrSpreadOrHole::Spread(
3435 react_compiler_hir::SpreadPattern {
3436 place: place.clone(),
3437 },
3438 ));
3439 }
3440 }
3441 }
3442 }
3443 }
3444 effects.push(AliasingEffect::Apply {
3445 receiver: recv,
3446 function: func,
3447 mutates_function: *mf,
3448 args: apply_args,
3449 into: apply_into,
3450 signature: s.clone(),
3451 loc: _loc.copied(),
3452 });
3453 } else {
3454 return Ok(None);
3455 }
3456 }
3457 AliasingEffect::CreateFunction { .. } => {
3458 // Not supported in signature substitution
3459 return Ok(None);
3460 }
3461 }
3462 }
3463
3464 Ok(Some(effects))
3465 }
3466
3467 // =============================================================================
3468 // Helpers
3469 // =============================================================================
3470
3471 /// Select the primary (most specific) reason from a set of reasons.
3472 /// TS uses `[...set][0]` which returns the first-inserted element;
3473 /// since the primary reason is always inserted first, this effectively
3474 /// picks the most specific non-Other reason. We replicate this by
3475 /// preferring any non-Other reason over Other.
3476 fn primary_reason(reasons: &ValueReasonSet) -> ValueReason {
3477 for r in reasons.iter() {
3478 if r != ValueReason::Other {
3479 return r;
3480 }
3481 }
3482 ValueReason::Other
3483 }
3484
3485 fn get_write_error_reason(abstract_value: &AbstractValue) -> String {
3486 if abstract_value.reason.contains(ValueReason::Global) {
3487 "Modifying a variable defined outside a component or hook is not allowed. Consider using an effect".to_string()
3488 } else if abstract_value.reason.contains(ValueReason::JsxCaptured) {
3489 "Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX".to_string()
3490 } else if abstract_value.reason.contains(ValueReason::Context) {
3491 "Modifying a value returned from 'useContext()' is not allowed.".to_string()
3492 } else if abstract_value
3493 .reason
3494 .contains(ValueReason::KnownReturnSignature)
3495 {
3496 "Modifying a value returned from a function whose return value should not be mutated"
3497 .to_string()
3498 } else if abstract_value
3499 .reason
3500 .contains(ValueReason::ReactiveFunctionArgument)
3501 {
3502 "Modifying component props or hook arguments is not allowed. Consider using a local variable instead".to_string()
3503 } else if abstract_value.reason.contains(ValueReason::State) {
3504 "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead".to_string()
3505 } else if abstract_value.reason.contains(ValueReason::ReducerState) {
3506 "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead".to_string()
3507 } else if abstract_value.reason.contains(ValueReason::Effect) {
3508 "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()
3509 } else if abstract_value.reason.contains(ValueReason::HookCaptured) {
3510 "Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook".to_string()
3511 } else if abstract_value.reason.contains(ValueReason::HookReturn) {
3512 "Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed".to_string()
3513 } else {
3514 "This modifies a variable that React considers immutable".to_string()
3515 }
3516 }
3517
3518 fn conditionally_mutate_iterator(place: &Place, ty: &Type) -> Option<AliasingEffect> {
3519 if !is_builtin_collection_type(ty) {
3520 Some(AliasingEffect::MutateTransitiveConditionally {
3521 value: place.clone(),
3522 })
3523 } else {
3524 None
3525 }
3526 }
3527
3528 fn is_builtin_collection_type(ty: &Type) -> bool {
3529 matches!(ty, Type::Object { shape_id: Some(id) }
3530 if id == BUILT_IN_ARRAY_ID || id == BUILT_IN_SET_ID || id == BUILT_IN_MAP_ID
3531 )
3532 }
3533
3534 fn get_function_call_signature(
3535 env: &Environment,
3536 callee_id: IdentifierId,
3537 ) -> Result<Option<FunctionSignature>, CompilerDiagnostic> {
3538 let ty = &env.types[env.identifiers[callee_id.0 as usize].type_.0 as usize];
3539 Ok(env.get_function_signature(ty)?.cloned())
3540 }
3541
3542 fn is_ref_or_ref_value_for_id(env: &Environment, id: IdentifierId) -> bool {
3543 let ty = &env.types[env.identifiers[id.0 as usize].type_.0 as usize];
3544 react_compiler_hir::is_ref_or_ref_value(ty)
3545 }
3546
3547 fn get_hook_kind_for_type<'a>(
3548 env: &'a Environment,
3549 ty: &Type,
3550 ) -> Result<Option<&'a HookKind>, CompilerDiagnostic> {
3551 env.get_hook_kind_for_type(ty)
3552 }
3553
3554 /// Format a Type for printPlace-style output, matching TS's `printType()`.
3555 fn format_type_for_print(ty: &Type) -> String {
3556 match ty {
3557 Type::Primitive => String::new(),
3558 Type::Function {
3559 shape_id,
3560 return_type,
3561 ..
3562 } => {
3563 if let Some(sid) = shape_id {
3564 let ret = format_type_for_print(return_type);
3565 if ret.is_empty() {
3566 format!(":TFunction<{}>()", sid)
3567 } else {
3568 format!(":TFunction<{}>(): {}", sid, ret)
3569 }
3570 } else {
3571 ":TFunction".to_string()
3572 }
3573 }
3574 Type::Object { shape_id } => {
3575 if let Some(sid) = shape_id {
3576 format!(":TObject<{}>", sid)
3577 } else {
3578 ":TObject".to_string()
3579 }
3580 }
3581 Type::Poly => ":TPoly".to_string(),
3582 Type::Phi { .. } => ":TPhi".to_string(),
3583 Type::Property { .. } => ":TProperty".to_string(),
3584 Type::TypeVar { .. } => String::new(),
3585 Type::ObjectMethod => ":TObjectMethod".to_string(),
3586 }
3587 }
3588
3589 fn is_phi_with_jsx(ty: &Type) -> bool {
3590 if let Type::Phi { operands } = ty {
3591 operands
3592 .iter()
3593 .any(|op| react_compiler_hir::is_jsx_type(op))
3594 } else {
3595 false
3596 }
3597 }
3598
3599 fn place_or_spread_to_hole(pos: &PlaceOrSpread) -> PlaceOrSpreadOrHole {
3600 match pos {
3601 PlaceOrSpread::Place(p) => PlaceOrSpreadOrHole::Place(p.clone()),
3602 PlaceOrSpread::Spread(s) => PlaceOrSpreadOrHole::Spread(s.clone()),
3603 }
3604 }
3605
3606 use react_compiler_hir::JsxTag;
3607
3608 fn build_apply_operands(
3609 receiver: &Place,
3610 function: &Place,
3611 args: &[PlaceOrSpreadOrHole],
3612 ) -> Vec<(Place, bool, bool)> {
3613 let mut result = vec![
3614 (receiver.clone(), false, false),
3615 (function.clone(), true, false),
3616 ];
3617 for arg in args {
3618 match arg {
3619 PlaceOrSpreadOrHole::Hole => continue,
3620 PlaceOrSpreadOrHole::Place(p) => result.push((p.clone(), false, false)),
3621 PlaceOrSpreadOrHole::Spread(s) => result.push((s.place.clone(), false, true)),
3622 }
3623 }
3624 result
3625 }
3626
3627 fn create_temp_place(env: &mut Environment, loc: Option<SourceLocation>) -> Place {
3628 let id = env.next_identifier_id();
3629 env.identifiers[id.0 as usize].loc = loc;
3630 Place {
3631 identifier: id,
3632 effect: Effect::Unknown,
3633 reactive: false,
3634 loc,
3635 }
3636 }
3637
3638 // =============================================================================
3639 // Terminal successor helper
3640 // =============================================================================
3641
3642 /// Returns the successor blocks used for traversal in mutation/aliasing inference.
3643 ///
3644 /// Matches the TS `eachTerminalSuccessor` which yields standard control-flow
3645 /// successors but NOT pseudo-successors (fallthroughs). Fallthroughs for
3646 /// Logical/Ternary/Optional and Try/Scope/PrunedScope are reached naturally
3647 /// via the block iteration order (blocks are stored in topological order).
3648 fn terminal_successors(terminal: &react_compiler_hir::Terminal) -> Vec<BlockId> {
3649 use react_compiler_hir::Terminal;
3650 match terminal {
3651 Terminal::Goto { block, .. } => vec![*block],
3652 Terminal::If {
3653 consequent,
3654 alternate,
3655 ..
3656 } => vec![*consequent, *alternate],
3657 Terminal::Branch {
3658 consequent,
3659 alternate,
3660 ..
3661 } => vec![*consequent, *alternate],
3662 Terminal::Switch { cases, .. } => cases.iter().map(|c| c.block).collect(),
3663 Terminal::For { init, .. } => vec![*init],
3664 Terminal::ForOf { init, .. } | Terminal::ForIn { init, .. } => vec![*init],
3665 Terminal::DoWhile { loop_block, .. } => vec![*loop_block],
3666 Terminal::While { test, .. } => vec![*test],
3667 Terminal::Return { .. }
3668 | Terminal::Throw { .. }
3669 | Terminal::Unreachable { .. }
3670 | Terminal::Unsupported { .. } => vec![],
3671 Terminal::Try { block, .. } => vec![*block],
3672 Terminal::MaybeThrow {
3673 continuation,
3674 handler,
3675 ..
3676 } => {
3677 let mut v = vec![*continuation];
3678 if let Some(h) = handler {
3679 v.push(*h);
3680 }
3681 v
3682 }
3683 Terminal::Label { block, .. } | Terminal::Sequence { block, .. } => vec![*block],
3684 Terminal::Logical { test, .. } | Terminal::Ternary { test, .. } => vec![*test],
3685 Terminal::Optional { test, .. } => vec![*test],
3686 Terminal::Scope { block, .. } | Terminal::PrunedScope { block, .. } => vec![*block],
3687 }
3688 }
3689
3690 /// Pattern item helper for Destructure.
3691 ///
3692 /// NOTE: This cannot use `visitors::each_pattern_operand` because callers need
3693 /// to distinguish Place from Spread elements — Spread elements get different
3694 /// aliasing effects (Create + Capture) vs Place elements (Create or CreateFrom).
3695 enum PatternItem<'a> {
3696 Place(&'a Place),
3697 Spread(&'a Place),
3698 }
3699
3700 fn each_pattern_items(pattern: &react_compiler_hir::Pattern) -> Vec<PatternItem<'_>> {
3701 let mut items = Vec::new();
3702 match pattern {
3703 react_compiler_hir::Pattern::Array(arr) => {
3704 for el in &arr.items {
3705 match el {
3706 react_compiler_hir::ArrayPatternElement::Place(p) => {
3707 items.push(PatternItem::Place(p))
3708 }
3709 react_compiler_hir::ArrayPatternElement::Spread(s) => {
3710 items.push(PatternItem::Spread(&s.place))
3711 }
3712 react_compiler_hir::ArrayPatternElement::Hole => {}
3713 }
3714 }
3715 }
3716 react_compiler_hir::Pattern::Object(obj) => {
3717 for prop in &obj.properties {
3718 match prop {
3719 react_compiler_hir::ObjectPropertyOrSpread::Property(p) => {
3720 items.push(PatternItem::Place(&p.place))
3721 }
3722 react_compiler_hir::ObjectPropertyOrSpread::Spread(s) => {
3723 items.push(PatternItem::Spread(&s.place))
3724 }
3725 }
3726 }
3727 }
3728 }
3729 items
3730 }