| 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 | //! Recursively analyzes nested function expressions and object methods to infer |
| 7 | //! their aliasing effect signatures. |
| 8 | //! |
| 9 | //! Ported from TypeScript `src/Inference/AnalyseFunctions.ts`. |
| 10 | //! |
| 11 | //! Runs inferMutationAliasingEffects, deadCodeElimination, |
| 12 | //! inferMutationAliasingRanges, rewriteInstructionKindsBasedOnReassignment, |
| 13 | //! and inferReactiveScopeVariables on each inner function. |
| 14 | |
| 15 | use indexmap::IndexMap; |
| 16 | use react_compiler_diagnostics::{CompilerDiagnostic, ErrorCategory}; |
| 17 | use react_compiler_hir::environment::Environment; |
| 18 | use rustc_hash::FxHashSet; |
| 19 | |
| 20 | use react_compiler_hir::{ |
| 21 | AliasingEffect, BlockId, Effect, EvaluationOrder, FunctionId, HIR, HirFunction, IdentifierId, |
| 22 | InstructionValue, Place, ReactFunctionType, |
| 23 | }; |
| 24 | |
| 25 | /// Analyse all nested function expressions and object methods in `func`. |
| 26 | /// |
| 27 | /// For each inner function found, runs `lower_with_mutation_aliasing` to infer |
| 28 | /// its aliasing effects, then resets context variable mutable ranges. |
| 29 | /// |
| 30 | /// The optional `debug_logger` callback is invoked after processing each inner |
| 31 | /// function, receiving `(&HirFunction, &Environment)` so the caller can produce |
| 32 | /// debug output. This mirrors the TS `fn.env.logger?.debugLogIRs` call inside |
| 33 | /// `lowerWithMutationAliasing`. |
| 34 | /// |
| 35 | /// Corresponds to TS `analyseFunctions(func: HIRFunction): void`. |
| 36 | pub fn analyse_functions<F>( |
| 37 | func: &mut HirFunction, |
| 38 | env: &mut Environment, |
| 39 | debug_logger: &mut F, |
| 40 | ) -> Result<(), CompilerDiagnostic> |
| 41 | where |
| 42 | F: FnMut(&HirFunction, &Environment), |
| 43 | { |
| 44 | // Collect FunctionIds from FunctionExpression/ObjectMethod instructions. |
| 45 | // We collect first to avoid borrow conflicts with env.functions. |
| 46 | let mut inner_func_ids: Vec<FunctionId> = Vec::new(); |
| 47 | for (_block_id, block) in &func.body.blocks { |
| 48 | for instr_id in &block.instructions { |
| 49 | let instr = &func.instructions[instr_id.0 as usize]; |
| 50 | match &instr.value { |
| 51 | InstructionValue::FunctionExpression { lowered_func, .. } |
| 52 | | InstructionValue::ObjectMethod { lowered_func, .. } => { |
| 53 | inner_func_ids.push(lowered_func.func); |
| 54 | } |
| 55 | _ => {} |
| 56 | } |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | // Process each inner function |
| 61 | for func_id in inner_func_ids { |
| 62 | // Take the inner function out of the arena to avoid borrow conflicts |
| 63 | let mut inner_func = std::mem::replace( |
| 64 | &mut env.functions[func_id.0 as usize], |
| 65 | placeholder_function(), |
| 66 | ); |
| 67 | |
| 68 | lower_with_mutation_aliasing(&mut inner_func, env, debug_logger)?; |
| 69 | |
| 70 | // If an invariant error was recorded, put the function back and stop processing |
| 71 | if env.has_invariant_errors() { |
| 72 | env.functions[func_id.0 as usize] = inner_func; |
| 73 | return Ok(()); |
| 74 | } |
| 75 | |
| 76 | // Reset mutable range for outer inferMutationAliasingEffects. |
| 77 | // |
| 78 | // NOTE: inferReactiveScopeVariables makes identifiers in the scope |
| 79 | // point to the *same* mutableRange instance (in TS). In Rust, scopes |
| 80 | // are stored in an arena, so we reset both the identifier's range |
| 81 | // and clear its scope. |
| 82 | for operand in &inner_func.context { |
| 83 | let new_range = env.new_mutable_range(EvaluationOrder(0), EvaluationOrder(0)); |
| 84 | let ident = &mut env.identifiers[operand.identifier.0 as usize]; |
| 85 | ident.mutable_range = new_range; |
| 86 | ident.scope = None; |
| 87 | } |
| 88 | |
| 89 | // Put the function back |
| 90 | env.functions[func_id.0 as usize] = inner_func; |
| 91 | } |
| 92 | |
| 93 | Ok(()) |
| 94 | } |
| 95 | |
| 96 | /// Run mutation/aliasing inference on an inner function. |
| 97 | /// |
| 98 | /// Corresponds to TS `lowerWithMutationAliasing(fn: HIRFunction): void`. |
| 99 | fn lower_with_mutation_aliasing<F>( |
| 100 | func: &mut HirFunction, |
| 101 | env: &mut Environment, |
| 102 | debug_logger: &mut F, |
| 103 | ) -> Result<(), CompilerDiagnostic> |
| 104 | where |
| 105 | F: FnMut(&HirFunction, &Environment), |
| 106 | { |
| 107 | // Phase 1: Recursively analyse nested functions first (depth-first) |
| 108 | analyse_functions(func, env, debug_logger)?; |
| 109 | |
| 110 | // inferMutationAliasingEffects on the inner function |
| 111 | crate::infer_mutation_aliasing_effects::infer_mutation_aliasing_effects(func, env, true)?; |
| 112 | |
| 113 | // Check for invariant errors (e.g., uninitialized value kind) |
| 114 | // In TS, these throw from within inferMutationAliasingEffects, aborting |
| 115 | // the rest of the function processing. |
| 116 | if env.has_invariant_errors() { |
| 117 | return Ok(()); |
| 118 | } |
| 119 | |
| 120 | // deadCodeElimination for inner functions |
| 121 | react_compiler_optimization::dead_code_elimination(func, env); |
| 122 | |
| 123 | // inferMutationAliasingRanges — returns the externally-visible function effects |
| 124 | let function_effects = |
| 125 | crate::infer_mutation_aliasing_ranges::infer_mutation_aliasing_ranges(func, env, true)?; |
| 126 | |
| 127 | // rewriteInstructionKindsBasedOnReassignment |
| 128 | if let Err(err) = react_compiler_ssa::rewrite_instruction_kinds_based_on_reassignment(func, env) |
| 129 | { |
| 130 | env.errors.merge(err); |
| 131 | return Ok(()); |
| 132 | } |
| 133 | |
| 134 | // inferReactiveScopeVariables on the inner function |
| 135 | crate::infer_reactive_scope_variables::infer_reactive_scope_variables(func, env)?; |
| 136 | |
| 137 | func.aliasing_effects = Some(function_effects.clone()); |
| 138 | |
| 139 | // Phase 2: Populate the Effect of each context variable to use in inferring |
| 140 | // the outer function. Corresponds to TS Phase 2 in lowerWithMutationAliasing. |
| 141 | let mut captured_or_mutated: FxHashSet<IdentifierId> = FxHashSet::default(); |
| 142 | for effect in &function_effects { |
| 143 | match effect { |
| 144 | AliasingEffect::Assign { from, .. } |
| 145 | | AliasingEffect::Alias { from, .. } |
| 146 | | AliasingEffect::Capture { from, .. } |
| 147 | | AliasingEffect::CreateFrom { from, .. } |
| 148 | | AliasingEffect::MaybeAlias { from, .. } => { |
| 149 | captured_or_mutated.insert(from.identifier); |
| 150 | } |
| 151 | AliasingEffect::Mutate { value, .. } |
| 152 | | AliasingEffect::MutateConditionally { value } |
| 153 | | AliasingEffect::MutateTransitive { value } |
| 154 | | AliasingEffect::MutateTransitiveConditionally { value } => { |
| 155 | captured_or_mutated.insert(value.identifier); |
| 156 | } |
| 157 | AliasingEffect::Impure { .. } |
| 158 | | AliasingEffect::Render { .. } |
| 159 | | AliasingEffect::MutateFrozen { .. } |
| 160 | | AliasingEffect::MutateGlobal { .. } |
| 161 | | AliasingEffect::CreateFunction { .. } |
| 162 | | AliasingEffect::Create { .. } |
| 163 | | AliasingEffect::Freeze { .. } |
| 164 | | AliasingEffect::ImmutableCapture { .. } => { |
| 165 | // no-op |
| 166 | } |
| 167 | AliasingEffect::Apply { .. } => { |
| 168 | return Err(CompilerDiagnostic::new( |
| 169 | ErrorCategory::Invariant, |
| 170 | "[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects", |
| 171 | None, |
| 172 | )); |
| 173 | } |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | for operand in &mut func.context { |
| 178 | if captured_or_mutated.contains(&operand.identifier) || operand.effect == Effect::Capture { |
| 179 | operand.effect = Effect::Capture; |
| 180 | } else { |
| 181 | operand.effect = Effect::Read; |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | // Log the inner function's state (mirrors TS: fn.env.logger?.debugLogIRs) |
| 186 | debug_logger(func, env); |
| 187 | |
| 188 | Ok(()) |
| 189 | } |
| 190 | |
| 191 | /// Create a placeholder HirFunction for temporarily swapping an inner function |
| 192 | /// out of `env.functions` via `std::mem::replace`. The placeholder is never |
| 193 | /// read — the real function is swapped back immediately after processing. |
| 194 | fn placeholder_function() -> HirFunction { |
| 195 | HirFunction { |
| 196 | loc: None, |
| 197 | id: None, |
| 198 | name_hint: None, |
| 199 | fn_type: ReactFunctionType::Other, |
| 200 | params: Vec::new(), |
| 201 | return_type_annotation: None, |
| 202 | returns: Place { |
| 203 | identifier: IdentifierId(0), |
| 204 | effect: Effect::Unknown, |
| 205 | reactive: false, |
| 206 | loc: None, |
| 207 | }, |
| 208 | context: Vec::new(), |
| 209 | body: HIR { |
| 210 | entry: BlockId(0), |
| 211 | blocks: IndexMap::default(), |
| 212 | }, |
| 213 | instructions: Vec::new(), |
| 214 | generator: false, |
| 215 | is_async: false, |
| 216 | directives: Vec::new(), |
| 217 | aliasing_effects: None, |
| 218 | } |
| 219 | } |