1use rustc_hash::{FxHashMap, FxHashSet};23use react_compiler_diagnostics::{4 CompilerDiagnostic, CompilerDiagnosticDetail, ErrorCategory, SourceLocation,5};6use react_compiler_hir::environment::Environment;7use react_compiler_hir::object_shape::HookKind;8use react_compiler_hir::visitors::{9 each_instruction_value_operand as canonical_each_instruction_value_operand,10 each_pattern_operand, each_terminal_operand,11};12use react_compiler_hir::{13 AliasingEffect, BlockId, HirFunction, Identifier, IdentifierId, InstructionValue, Place,14 PrimitiveValue, PropertyLiteral, Terminal, Type, UnaryOperator,15};1617const ERROR_DESCRIPTION: &str = "React refs are values that are not needed for rendering. \18 Refs should only be accessed outside of render, such as in event handlers or effects. \19 Accessing a ref value (the `current` property) during render can cause your component \20 not to update as expected (https://react.dev/reference/react/useRef)";2122// --- RefId ---2324type RefId = u32;2526static REF_ID_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);2728fn next_ref_id() -> RefId {29 REF_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)30}3132// --- RefAccessType / RefAccessRefType / RefFnType ---3334/// Corresponds to TS `RefAccessType`.35///36/// PartialEq matches the TS `tyEqual` semantics: Ref ignores ref_id,37/// RefValue compares loc but ignores ref_id. This is critical for fixpoint38/// convergence — join creates fresh ref_ids, and comparing them would39/// prevent the environment from stabilizing.40#[derive(Debug, Clone)]41enum RefAccessType {42 None,43 Nullable,44 Guard {45 ref_id: RefId,46 },47 Ref {48 ref_id: RefId,49 },50 RefValue {51 loc: Option<SourceLocation>,52 ref_id: Option<RefId>,53 },54 Structure {55 value: Option<Box<RefAccessRefType>>,56 fn_type: Option<RefFnType>,57 },58}5960impl PartialEq for RefAccessType {61 fn eq(&self, other: &Self) -> bool {62 match (self, other) {63 (RefAccessType::None, RefAccessType::None) => true,64 (RefAccessType::Nullable, RefAccessType::Nullable) => true,65 (RefAccessType::Guard { ref_id: a }, RefAccessType::Guard { ref_id: b }) => a == b,66 (RefAccessType::Ref { .. }, RefAccessType::Ref { .. }) => true,67 (RefAccessType::RefValue { loc: a, .. }, RefAccessType::RefValue { loc: b, .. }) => {68 a == b69 }70 (71 RefAccessType::Structure {72 value: a_val,73 fn_type: a_fn,74 },75 RefAccessType::Structure {76 value: b_val,77 fn_type: b_fn,78 },79 ) => a_val == b_val && a_fn == b_fn,80 _ => false,81 }82 }83}8485/// Corresponds to TS `RefAccessRefType` — the subset of `RefAccessType` that can appear86/// inside `Structure.value` and be joined via `join_ref_access_ref_types`.87///88/// PartialEq mirrors RefAccessType: Ref ignores ref_id, RefValue compares89/// loc only.90#[derive(Debug, Clone)]91enum RefAccessRefType {92 Ref {93 ref_id: RefId,94 },95 RefValue {96 loc: Option<SourceLocation>,97 ref_id: Option<RefId>,98 },99 Structure {100 value: Option<Box<RefAccessRefType>>,101 fn_type: Option<RefFnType>,102 },103}104105impl PartialEq for RefAccessRefType {106 fn eq(&self, other: &Self) -> bool {107 match (self, other) {108 (RefAccessRefType::Ref { .. }, RefAccessRefType::Ref { .. }) => true,109 (110 RefAccessRefType::RefValue { loc: a, .. },111 RefAccessRefType::RefValue { loc: b, .. },112 ) => a == b,113 (114 RefAccessRefType::Structure {115 value: a_val,116 fn_type: a_fn,117 },118 RefAccessRefType::Structure {119 value: b_val,120 fn_type: b_fn,121 },122 ) => a_val == b_val && a_fn == b_fn,123 _ => false,124 }125 }126}127128#[derive(Debug, Clone, PartialEq)]129struct RefFnType {130 read_ref_effect: bool,131 return_type: Box<RefAccessType>,132}133134impl RefAccessType {135 /// Try to convert a `RefAccessType` to a `RefAccessRefType` (the Ref/RefValue/Structure subset).136 fn to_ref_type(&self) -> Option<RefAccessRefType> {137 match self {138 RefAccessType::Ref { ref_id } => Some(RefAccessRefType::Ref { ref_id: *ref_id }),139 RefAccessType::RefValue { loc, ref_id } => Some(RefAccessRefType::RefValue {140 loc: *loc,141 ref_id: *ref_id,142 }),143 RefAccessType::Structure { value, fn_type } => Some(RefAccessRefType::Structure {144 value: value.clone(),145 fn_type: fn_type.clone(),146 }),147 _ => None,148 }149 }150151 /// Convert a `RefAccessRefType` back to a `RefAccessType`.152 fn from_ref_type(ref_type: &RefAccessRefType) -> Self {153 match ref_type {154 RefAccessRefType::Ref { ref_id } => RefAccessType::Ref { ref_id: *ref_id },155 RefAccessRefType::RefValue { loc, ref_id } => RefAccessType::RefValue {156 loc: *loc,157 ref_id: *ref_id,158 },159 RefAccessRefType::Structure { value, fn_type } => RefAccessType::Structure {160 value: value.clone(),161 fn_type: fn_type.clone(),162 },163 }164 }165}166167// --- Join operations ---168169fn join_ref_access_ref_types(a: &RefAccessRefType, b: &RefAccessRefType) -> RefAccessRefType {170 match (a, b) {171 (172 RefAccessRefType::RefValue { ref_id: a_id, .. },173 RefAccessRefType::RefValue { ref_id: b_id, .. },174 ) => {175 if a_id == b_id {176 a.clone()177 } else {178 RefAccessRefType::RefValue {179 loc: None,180 ref_id: None,181 }182 }183 }184 (RefAccessRefType::RefValue { .. }, _) => RefAccessRefType::RefValue {185 loc: None,186 ref_id: None,187 },188 (_, RefAccessRefType::RefValue { .. }) => RefAccessRefType::RefValue {189 loc: None,190 ref_id: None,191 },192 (RefAccessRefType::Ref { ref_id: a_id }, RefAccessRefType::Ref { ref_id: b_id }) => {193 if a_id == b_id {194 a.clone()195 } else {196 RefAccessRefType::Ref {197 ref_id: next_ref_id(),198 }199 }200 }201 (RefAccessRefType::Ref { .. }, _) | (_, RefAccessRefType::Ref { .. }) => {202 RefAccessRefType::Ref {203 ref_id: next_ref_id(),204 }205 }206 (207 RefAccessRefType::Structure {208 value: a_value,209 fn_type: a_fn,210 },211 RefAccessRefType::Structure {212 value: b_value,213 fn_type: b_fn,214 },215 ) => {216 let fn_type = match (a_fn, b_fn) {217 (None, other) | (other, None) => other.clone(),218 (Some(a_fn), Some(b_fn)) => Some(RefFnType {219 read_ref_effect: a_fn.read_ref_effect || b_fn.read_ref_effect,220 return_type: Box::new(join_ref_access_types(221 &a_fn.return_type,222 &b_fn.return_type,223 )),224 }),225 };226 let value = match (a_value, b_value) {227 (None, other) | (other, None) => other.clone(),228 (Some(a_val), Some(b_val)) => {229 Some(Box::new(join_ref_access_ref_types(a_val, b_val)))230 }231 };232 RefAccessRefType::Structure { value, fn_type }233 }234 }235}236237fn join_ref_access_types(a: &RefAccessType, b: &RefAccessType) -> RefAccessType {238 match (a, b) {239 (RefAccessType::None, other) | (other, RefAccessType::None) => other.clone(),240 (RefAccessType::Guard { ref_id: a_id }, RefAccessType::Guard { ref_id: b_id }) => {241 if a_id == b_id {242 a.clone()243 } else {244 RefAccessType::None245 }246 }247 (RefAccessType::Guard { .. }, RefAccessType::Nullable)248 | (RefAccessType::Nullable, RefAccessType::Guard { .. }) => RefAccessType::None,249 (RefAccessType::Guard { .. }, other) | (other, RefAccessType::Guard { .. }) => {250 other.clone()251 }252 (RefAccessType::Nullable, other) | (other, RefAccessType::Nullable) => other.clone(),253 _ => match (a.to_ref_type(), b.to_ref_type()) {254 (Some(a_ref), Some(b_ref)) => {255 RefAccessType::from_ref_type(&join_ref_access_ref_types(&a_ref, &b_ref))256 }257 (Some(r), None) | (None, Some(r)) => RefAccessType::from_ref_type(&r),258 _ => RefAccessType::None,259 },260 }261}262263fn join_ref_access_types_many(types: &[RefAccessType]) -> RefAccessType {264 types265 .iter()266 .fold(RefAccessType::None, |acc, t| join_ref_access_types(&acc, t))267}268269// --- Env ---270271struct Env {272 changed: bool,273 data: FxHashMap<IdentifierId, RefAccessType>,274 temporaries: FxHashMap<IdentifierId, Place>,275}276277impl Env {278 fn new() -> Self {279 Self {280 changed: false,281 data: FxHashMap::default(),282 temporaries: FxHashMap::default(),283 }284 }285286 fn define(&mut self, key: IdentifierId, value: Place) {287 self.temporaries.insert(key, value);288 }289290 fn reset_changed(&mut self) {291 self.changed = false;292 }293294 fn has_changed(&self) -> bool {295 self.changed296 }297298 fn get(&self, key: IdentifierId) -> Option<&RefAccessType> {299 let operand_id = self300 .temporaries301 .get(&key)302 .map(|p| p.identifier)303 .unwrap_or(key);304 self.data.get(&operand_id)305 }306307 fn set(&mut self, key: IdentifierId, value: RefAccessType) {308 let operand_id = self309 .temporaries310 .get(&key)311 .map(|p| p.identifier)312 .unwrap_or(key);313 let current = self.data.get(&operand_id);314 let widened_value = join_ref_access_types(&value, current.unwrap_or(&RefAccessType::None));315 if current.is_none() && widened_value == RefAccessType::None {316 // No change needed317 } else if current.map_or(true, |c| c != &widened_value) {318 self.changed = true;319 }320 self.data.insert(operand_id, widened_value);321 }322}323324// --- Helper functions ---325326fn ref_type_of_type(id: IdentifierId, identifiers: &[Identifier], types: &[Type]) -> RefAccessType {327 let identifier = &identifiers[id.0 as usize];328 let ty = &types[identifier.type_.0 as usize];329 if react_compiler_hir::is_ref_value_type(ty) {330 RefAccessType::RefValue {331 loc: None,332 ref_id: None,333 }334 } else if react_compiler_hir::is_use_ref_type(ty) {335 RefAccessType::Ref {336 ref_id: next_ref_id(),337 }338 } else {339 RefAccessType::None340 }341}342343fn is_ref_type(id: IdentifierId, identifiers: &[Identifier], types: &[Type]) -> bool {344 let identifier = &identifiers[id.0 as usize];345 react_compiler_hir::is_use_ref_type(&types[identifier.type_.0 as usize])346}347348fn is_ref_value_type(id: IdentifierId, identifiers: &[Identifier], types: &[Type]) -> bool {349 let identifier = &identifiers[id.0 as usize];350 react_compiler_hir::is_ref_value_type(&types[identifier.type_.0 as usize])351}352353fn destructure(ty: &RefAccessType) -> RefAccessType {354 match ty {355 RefAccessType::Structure {356 value: Some(inner), ..357 } => destructure(&RefAccessType::from_ref_type(inner)),358 other => other.clone(),359 }360}361362// --- Validation helpers ---363364fn validate_no_direct_ref_value_access(365 errors: &mut Vec<CompilerDiagnostic>,366 operand: &Place,367 env: &Env,368) {369 if let Some(ty) = env.get(operand.identifier) {370 let ty = destructure(ty);371 if let RefAccessType::RefValue { loc, .. } = &ty {372 errors.push(373 CompilerDiagnostic::new(374 ErrorCategory::Refs,375 "Cannot access refs during render",376 Some(ERROR_DESCRIPTION.to_string()),377 )378 .with_detail(CompilerDiagnosticDetail::Error {379 loc: loc.or(operand.loc),380 message: Some("Cannot access ref value during render".to_string()),381 identifier_name: None,382 }),383 );384 }385 }386}387388fn validate_no_ref_value_access(errors: &mut Vec<CompilerDiagnostic>, env: &Env, operand: &Place) {389 if let Some(ty) = env.get(operand.identifier) {390 let ty = destructure(ty);391 match &ty {392 RefAccessType::RefValue { loc, .. } => {393 errors.push(394 CompilerDiagnostic::new(395 ErrorCategory::Refs,396 "Cannot access refs during render",397 Some(ERROR_DESCRIPTION.to_string()),398 )399 .with_detail(CompilerDiagnosticDetail::Error {400 loc: loc.or(operand.loc),401 message: Some("Cannot access ref value during render".to_string()),402 identifier_name: None,403 }),404 );405 }406 RefAccessType::Structure {407 fn_type: Some(fn_type),408 ..409 } if fn_type.read_ref_effect => {410 errors.push(411 CompilerDiagnostic::new(412 ErrorCategory::Refs,413 "Cannot access refs during render",414 Some(ERROR_DESCRIPTION.to_string()),415 )416 .with_detail(CompilerDiagnosticDetail::Error {417 loc: operand.loc,418 message: Some("Cannot access ref value during render".to_string()),419 identifier_name: None,420 }),421 );422 }423 _ => {}424 }425 }426}427428fn validate_no_ref_passed_to_function(429 errors: &mut Vec<CompilerDiagnostic>,430 env: &Env,431 operand: &Place,432 loc: Option<SourceLocation>,433) {434 if let Some(ty) = env.get(operand.identifier) {435 let ty = destructure(ty);436 match &ty {437 RefAccessType::Ref { .. } | RefAccessType::RefValue { .. } => {438 let error_loc = if let RefAccessType::RefValue { loc: ref_loc, .. } = &ty {439 ref_loc.or(loc)440 } else {441 loc442 };443 errors.push(444 CompilerDiagnostic::new(445 ErrorCategory::Refs,446 "Cannot access refs during render",447 Some(ERROR_DESCRIPTION.to_string()),448 )449 .with_detail(CompilerDiagnosticDetail::Error {450 loc: error_loc,451 message: Some(452 "Passing a ref to a function may read its value during render"453 .to_string(),454 ),455 identifier_name: None,456 }),457 );458 }459 RefAccessType::Structure {460 fn_type: Some(fn_type),461 ..462 } if fn_type.read_ref_effect => {463 errors.push(464 CompilerDiagnostic::new(465 ErrorCategory::Refs,466 "Cannot access refs during render",467 Some(ERROR_DESCRIPTION.to_string()),468 )469 .with_detail(CompilerDiagnosticDetail::Error {470 loc,471 message: Some(472 "Passing a ref to a function may read its value during render"473 .to_string(),474 ),475 identifier_name: None,476 }),477 );478 }479 _ => {}480 }481 }482}483484fn validate_no_ref_update(485 errors: &mut Vec<CompilerDiagnostic>,486 env: &Env,487 operand: &Place,488 loc: Option<SourceLocation>,489) {490 if let Some(ty) = env.get(operand.identifier) {491 let ty = destructure(ty);492 match &ty {493 RefAccessType::Ref { .. } | RefAccessType::RefValue { .. } => {494 let error_loc = if let RefAccessType::RefValue { loc: ref_loc, .. } = &ty {495 ref_loc.or(loc)496 } else {497 loc498 };499 errors.push(500 CompilerDiagnostic::new(501 ErrorCategory::Refs,502 "Cannot access refs during render",503 Some(ERROR_DESCRIPTION.to_string()),504 )505 .with_detail(CompilerDiagnosticDetail::Error {506 loc: error_loc,507 message: Some("Cannot update ref during render".to_string()),508 identifier_name: None,509 }),510 );511 }512 _ => {}513 }514 }515}516517fn guard_check(errors: &mut Vec<CompilerDiagnostic>, operand: &Place, env: &Env) {518 if matches!(519 env.get(operand.identifier),520 Some(RefAccessType::Guard { .. })521 ) {522 errors.push(523 CompilerDiagnostic::new(524 ErrorCategory::Refs,525 "Cannot access refs during render",526 Some(ERROR_DESCRIPTION.to_string()),527 )528 .with_detail(CompilerDiagnosticDetail::Error {529 loc: operand.loc,530 message: Some("Cannot access ref value during render".to_string()),531 identifier_name: None,532 }),533 );534 }535}536537// --- Main entry point ---538539pub fn validate_no_ref_access_in_render(func: &HirFunction, env: &mut Environment) {540 let mut ref_env = Env::new();541 collect_temporaries_sidemap(func, &mut ref_env, &env.identifiers, &env.types);542 let mut errors: Vec<CompilerDiagnostic> = Vec::new();543 validate_no_ref_access_in_render_impl(544 func,545 &env.identifiers,546 &env.types,547 &env.functions,548 &*env,549 &mut ref_env,550 &mut errors,551 );552 for diagnostic in errors {553 env.record_diagnostic(diagnostic);554 }555}556557fn collect_temporaries_sidemap(558 func: &HirFunction,559 env: &mut Env,560 identifiers: &[Identifier],561 types: &[Type],562) {563 for (_, block) in &func.body.blocks {564 for &instr_id in &block.instructions {565 let instr = &func.instructions[instr_id.0 as usize];566 match &instr.value {567 InstructionValue::LoadLocal { place, .. } => {568 let temp = env569 .temporaries570 .get(&place.identifier)571 .cloned()572 .unwrap_or_else(|| place.clone());573 env.define(instr.lvalue.identifier, temp);574 }575 InstructionValue::StoreLocal { lvalue, value, .. } => {576 let temp = env577 .temporaries578 .get(&value.identifier)579 .cloned()580 .unwrap_or_else(|| value.clone());581 env.define(instr.lvalue.identifier, temp.clone());582 env.define(lvalue.place.identifier, temp);583 }584 InstructionValue::PropertyLoad {585 object, property, ..586 } => {587 if is_ref_type(object.identifier, identifiers, types)588 && *property == PropertyLiteral::String("current".to_string())589 {590 continue;591 }592 let temp = env593 .temporaries594 .get(&object.identifier)595 .cloned()596 .unwrap_or_else(|| object.clone());597 env.define(instr.lvalue.identifier, temp);598 }599 _ => {}600 }601 }602 }603}604605fn validate_no_ref_access_in_render_impl(606 func: &HirFunction,607 identifiers: &[Identifier],608 types: &[Type],609 functions: &[HirFunction],610 env: &Environment,611 ref_env: &mut Env,612 errors: &mut Vec<CompilerDiagnostic>,613) -> RefAccessType {614 let mut return_values: Vec<RefAccessType> = Vec::new();615616 // Process params617 for param in &func.params {618 let place = match param {619 react_compiler_hir::ParamPattern::Place(p) => p,620 react_compiler_hir::ParamPattern::Spread(s) => &s.place,621 };622 ref_env.set(623 place.identifier,624 ref_type_of_type(place.identifier, identifiers, types),625 );626 }627628 // Collect identifiers that are interpolated as JSX children629 let mut interpolated_as_jsx: FxHashSet<IdentifierId> = FxHashSet::default();630 for (_, block) in &func.body.blocks {631 for &instr_id in &block.instructions {632 let instr = &func.instructions[instr_id.0 as usize];633 match &instr.value {634 InstructionValue::JsxExpression {635 children: Some(children),636 ..637 } => {638 for child in children {639 interpolated_as_jsx.insert(child.identifier);640 }641 }642 InstructionValue::JsxFragment { children, .. } => {643 for child in children {644 interpolated_as_jsx.insert(child.identifier);645 }646 }647 _ => {}648 }649 }650 }651652 // Fixed-point iteration (up to 10 iterations)653 for iteration in 0..10 {654 if iteration > 0 && !ref_env.has_changed() {655 break;656 }657 ref_env.reset_changed();658 return_values.clear();659 let mut safe_blocks: Vec<(BlockId, RefId)> = Vec::new();660661 for (_, block) in &func.body.blocks {662 safe_blocks.retain(|(block_id, _)| *block_id != block.id);663664 // Process phis665 for phi in &block.phis {666 let phi_types: Vec<RefAccessType> = phi667 .operands668 .values()669 .map(|operand| {670 ref_env671 .get(operand.identifier)672 .cloned()673 .unwrap_or(RefAccessType::None)674 })675 .collect();676 ref_env.set(phi.place.identifier, join_ref_access_types_many(&phi_types));677 }678679 // Process instructions680 for &instr_id in &block.instructions {681 let instr = &func.instructions[instr_id.0 as usize];682 match &instr.value {683 InstructionValue::JsxExpression { .. }684 | InstructionValue::JsxFragment { .. } => {685 for operand in &canonical_each_instruction_value_operand(&instr.value, env)686 {687 validate_no_direct_ref_value_access(errors, operand, ref_env);688 }689 }690 InstructionValue::ComputedLoad {691 object, property, ..692 } => {693 validate_no_direct_ref_value_access(errors, property, ref_env);694 let obj_type = ref_env.get(object.identifier).cloned();695 let lookup_type = match &obj_type {696 Some(RefAccessType::Structure {697 value: Some(value), ..698 }) => Some(RefAccessType::from_ref_type(value)),699 Some(RefAccessType::Ref { ref_id }) => Some(RefAccessType::RefValue {700 loc: instr.loc,701 ref_id: Some(*ref_id),702 }),703 _ => None,704 };705 ref_env.set(706 instr.lvalue.identifier,707 lookup_type.unwrap_or_else(|| {708 ref_type_of_type(instr.lvalue.identifier, identifiers, types)709 }),710 );711 }712 InstructionValue::PropertyLoad { object, .. } => {713 let obj_type = ref_env.get(object.identifier).cloned();714 let lookup_type = match &obj_type {715 Some(RefAccessType::Structure {716 value: Some(value), ..717 }) => Some(RefAccessType::from_ref_type(value)),718 Some(RefAccessType::Ref { ref_id }) => Some(RefAccessType::RefValue {719 loc: instr.loc,720 ref_id: Some(*ref_id),721 }),722 _ => None,723 };724 ref_env.set(725 instr.lvalue.identifier,726 lookup_type.unwrap_or_else(|| {727 ref_type_of_type(instr.lvalue.identifier, identifiers, types)728 }),729 );730 }731 InstructionValue::TypeCastExpression { value, .. } => {732 ref_env.set(733 instr.lvalue.identifier,734 ref_env.get(value.identifier).cloned().unwrap_or_else(|| {735 ref_type_of_type(instr.lvalue.identifier, identifiers, types)736 }),737 );738 }739 InstructionValue::LoadContext { place, .. }740 | InstructionValue::LoadLocal { place, .. } => {741 ref_env.set(742 instr.lvalue.identifier,743 ref_env.get(place.identifier).cloned().unwrap_or_else(|| {744 ref_type_of_type(instr.lvalue.identifier, identifiers, types)745 }),746 );747 }748 InstructionValue::StoreContext { lvalue, value, .. }749 | InstructionValue::StoreLocal { lvalue, value, .. } => {750 ref_env.set(751 lvalue.place.identifier,752 ref_env.get(value.identifier).cloned().unwrap_or_else(|| {753 ref_type_of_type(lvalue.place.identifier, identifiers, types)754 }),755 );756 ref_env.set(757 instr.lvalue.identifier,758 ref_env.get(value.identifier).cloned().unwrap_or_else(|| {759 ref_type_of_type(instr.lvalue.identifier, identifiers, types)760 }),761 );762 }763 InstructionValue::Destructure { value, lvalue, .. } => {764 let obj_type = ref_env.get(value.identifier).cloned();765 let lookup_type = match &obj_type {766 Some(RefAccessType::Structure {767 value: Some(value), ..768 }) => Some(RefAccessType::from_ref_type(value)),769 _ => None,770 };771 ref_env.set(772 instr.lvalue.identifier,773 lookup_type.clone().unwrap_or_else(|| {774 ref_type_of_type(instr.lvalue.identifier, identifiers, types)775 }),776 );777 for pattern_place in each_pattern_operand(&lvalue.pattern) {778 ref_env.set(779 pattern_place.identifier,780 lookup_type.clone().unwrap_or_else(|| {781 ref_type_of_type(pattern_place.identifier, identifiers, types)782 }),783 );784 }785 }786 InstructionValue::ObjectMethod { lowered_func, .. }787 | InstructionValue::FunctionExpression { lowered_func, .. } => {788 let inner = &functions[lowered_func.func.0 as usize];789 let mut inner_errors: Vec<CompilerDiagnostic> = Vec::new();790 let result = validate_no_ref_access_in_render_impl(791 inner,792 identifiers,793 types,794 functions,795 env,796 ref_env,797 &mut inner_errors,798 );799 let (return_type, read_ref_effect) = if inner_errors.is_empty() {800 (result, false)801 } else {802 (RefAccessType::None, true)803 };804 ref_env.set(805 instr.lvalue.identifier,806 RefAccessType::Structure {807 value: None,808 fn_type: Some(RefFnType {809 read_ref_effect,810 return_type: Box::new(return_type),811 }),812 },813 );814 }815 InstructionValue::MethodCall { property, .. }816 | InstructionValue::CallExpression {817 callee: property, ..818 } => {819 let callee = property;820 let mut return_type = RefAccessType::None;821 let fn_type = ref_env.get(callee.identifier).cloned();822 let mut did_error = false;823824 if let Some(RefAccessType::Structure {825 fn_type: Some(fn_ty),826 ..827 }) = &fn_type828 {829 return_type = *fn_ty.return_type.clone();830 if fn_ty.read_ref_effect {831 did_error = true;832 errors.push(833 CompilerDiagnostic::new(834 ErrorCategory::Refs,835 "Cannot access refs during render",836 Some(ERROR_DESCRIPTION.to_string()),837 )838 .with_detail(839 CompilerDiagnosticDetail::Error {840 loc: callee.loc,841 message: Some(842 "This function accesses a ref value".to_string(),843 ),844 identifier_name: None,845 },846 ),847 );848 }849 }850851 /*852 * If we already reported an error on this instruction, don't report853 * duplicate errors854 */855 if !did_error {856 let is_ref_lvalue =857 is_ref_type(instr.lvalue.identifier, identifiers, types);858 let callee_identifier = &identifiers[callee.identifier.0 as usize];859 let callee_type = &types[callee_identifier.type_.0 as usize];860 let hook_kind = env.get_hook_kind_for_type(callee_type).ok().flatten();861862 if is_ref_lvalue863 || (hook_kind.is_some()864 && !matches!(hook_kind, Some(&HookKind::UseState))865 && !matches!(hook_kind, Some(&HookKind::UseReducer)))866 {867 for operand in868 &canonical_each_instruction_value_operand(&instr.value, env)869 {870 /*871 * Allow passing refs or ref-accessing functions when:872 * 1. lvalue is a ref (mergeRefs pattern)873 * 2. calling hooks (independently validated)874 */875 validate_no_direct_ref_value_access(errors, operand, ref_env);876 }877 } else if interpolated_as_jsx.contains(&instr.lvalue.identifier) {878 for operand in879 &canonical_each_instruction_value_operand(&instr.value, env)880 {881 /*882 * Special case: the lvalue is passed as a jsx child883 */884 validate_no_ref_value_access(errors, ref_env, operand);885 }886 } else if hook_kind.is_none() {887 if let Some(ref effects) = instr.effects {888 /*889 * For non-hook functions with known aliasing effects,890 * use the effects to determine what validation to apply.891 * Track visited id:kind pairs to avoid duplicate errors.892 */893 let mut visited_effects: FxHashSet<String> =894 FxHashSet::default();895 for effect in effects {896 let (place, validation) = match effect {897 AliasingEffect::Freeze { value, .. } => {898 (Some(value), "direct-ref")899 }900 AliasingEffect::Mutate { value, .. }901 | AliasingEffect::MutateTransitive { value, .. }902 | AliasingEffect::MutateConditionally {903 value, ..904 }905 | AliasingEffect::MutateTransitiveConditionally {906 value,907 ..908 } => (Some(value), "ref-passed"),909 AliasingEffect::Render { place, .. } => {910 (Some(place), "ref-passed")911 }912 AliasingEffect::Capture { from, .. }913 | AliasingEffect::Alias { from, .. }914 | AliasingEffect::MaybeAlias { from, .. }915 | AliasingEffect::Assign { from, .. }916 | AliasingEffect::CreateFrom { from, .. } => {917 (Some(from), "ref-passed")918 }919 AliasingEffect::ImmutableCapture { from, .. } => {920 /*921 * ImmutableCapture: check whether the same922 * operand also has a Freeze effect to923 * distinguish known signatures from924 * downgraded defaults.925 */926 let is_frozen = effects.iter().any(|e| {927 matches!(928 e,929 AliasingEffect::Freeze { value, .. }930 if value.identifier == from.identifier931 )932 });933 (934 Some(from),935 if is_frozen {936 "direct-ref"937 } else {938 "ref-passed"939 },940 )941 }942 _ => (None, "none"),943 };944 if let Some(place) = place {945 if validation != "none" {946 let key = format!(947 "{}:{}",948 place.identifier.0, validation949 );950 if visited_effects.insert(key) {951 if validation == "direct-ref" {952 validate_no_direct_ref_value_access(953 errors, place, ref_env,954 );955 } else {956 validate_no_ref_passed_to_function(957 errors, ref_env, place, place.loc,958 );959 }960 }961 }962 }963 }964 } else {965 for operand in966 &canonical_each_instruction_value_operand(&instr.value, env)967 {968 validate_no_ref_passed_to_function(969 errors,970 ref_env,971 operand,972 operand.loc,973 );974 }975 }976 } else {977 for operand in978 &canonical_each_instruction_value_operand(&instr.value, env)979 {980 validate_no_ref_passed_to_function(981 errors,982 ref_env,983 operand,984 operand.loc,985 );986 }987 }988 }989 ref_env.set(instr.lvalue.identifier, return_type);990 }991 InstructionValue::ObjectExpression { .. }992 | InstructionValue::ArrayExpression { .. } => {993 let operands = canonical_each_instruction_value_operand(&instr.value, env);994 let mut types_vec: Vec<RefAccessType> = Vec::new();995 for operand in &operands {996 validate_no_direct_ref_value_access(errors, operand, ref_env);997 types_vec.push(998 ref_env999 .get(operand.identifier)1000 .cloned()1001 .unwrap_or(RefAccessType::None),1002 );1003 }1004 let value = join_ref_access_types_many(&types_vec);1005 match &value {1006 RefAccessType::None1007 | RefAccessType::Guard { .. }1008 | RefAccessType::Nullable => {1009 ref_env.set(instr.lvalue.identifier, RefAccessType::None);1010 }1011 _ => {1012 ref_env.set(1013 instr.lvalue.identifier,1014 RefAccessType::Structure {1015 value: value.to_ref_type().map(Box::new),1016 fn_type: None,1017 },1018 );1019 }1020 }1021 }1022 InstructionValue::PropertyDelete { object, .. }1023 | InstructionValue::PropertyStore { object, .. }1024 | InstructionValue::ComputedDelete { object, .. }1025 | InstructionValue::ComputedStore { object, .. } => {1026 let target = ref_env.get(object.identifier).cloned();1027 let mut found_safe = false;1028 if matches!(&instr.value, InstructionValue::PropertyStore { .. }) {1029 if let Some(RefAccessType::Ref { ref_id }) = &target {1030 if let Some(pos) = safe_blocks.iter().position(|(_, r)| r == ref_id)1031 {1032 safe_blocks.remove(pos);1033 found_safe = true;1034 }1035 }1036 }1037 if !found_safe {1038 validate_no_ref_update(errors, ref_env, object, instr.loc);1039 }1040 match &instr.value {1041 InstructionValue::ComputedDelete { property, .. }1042 | InstructionValue::ComputedStore { property, .. } => {1043 validate_no_ref_value_access(errors, ref_env, property);1044 }1045 _ => {}1046 }1047 match &instr.value {1048 InstructionValue::ComputedStore { value, .. }1049 | InstructionValue::PropertyStore { value, .. } => {1050 validate_no_direct_ref_value_access(errors, value, ref_env);1051 let value_type = ref_env.get(value.identifier).cloned();1052 if let Some(RefAccessType::Structure { .. }) = &value_type {1053 let mut object_type = value_type.unwrap();1054 if let Some(t) = &target {1055 object_type = join_ref_access_types(&object_type, t);1056 }1057 ref_env.set(object.identifier, object_type);1058 }1059 }1060 _ => {}1061 }1062 }1063 InstructionValue::StartMemoize { .. }1064 | InstructionValue::FinishMemoize { .. } => {}1065 InstructionValue::LoadGlobal { binding, .. } => {1066 if binding.name() == "undefined" {1067 ref_env.set(instr.lvalue.identifier, RefAccessType::Nullable);1068 }1069 }1070 InstructionValue::Primitive { value, .. } => {1071 if matches!(value, PrimitiveValue::Null | PrimitiveValue::Undefined) {1072 ref_env.set(instr.lvalue.identifier, RefAccessType::Nullable);1073 }1074 }1075 InstructionValue::UnaryExpression {1076 operator, value, ..1077 } => {1078 if *operator == UnaryOperator::Not {1079 if let Some(RefAccessType::RefValue {1080 ref_id: Some(ref_id),1081 ..1082 }) = ref_env.get(value.identifier).cloned().as_ref()1083 {1084 /*1085 * Record an error suggesting the `if (ref.current == null)` pattern,1086 * but also record the lvalue as a guard so that we don't emit a1087 * second error for the write to the ref1088 */1089 ref_env.set(1090 instr.lvalue.identifier,1091 RefAccessType::Guard { ref_id: *ref_id },1092 );1093 errors.push(1094 CompilerDiagnostic::new(1095 ErrorCategory::Refs,1096 "Cannot access refs during render",1097 Some(ERROR_DESCRIPTION.to_string()),1098 )1099 .with_detail(CompilerDiagnosticDetail::Error {1100 loc: value.loc,1101 message: Some(1102 "Cannot access ref value during render"1103 .to_string(),1104 ),1105 identifier_name: None,1106 })1107 .with_detail(CompilerDiagnosticDetail::Hint {1108 message: "To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`".to_string(),1109 }),1110 );1111 } else {1112 validate_no_ref_value_access(errors, ref_env, value);1113 }1114 } else {1115 validate_no_ref_value_access(errors, ref_env, value);1116 }1117 }1118 InstructionValue::BinaryExpression { left, right, .. } => {1119 let left_type = ref_env.get(left.identifier).cloned();1120 let right_type = ref_env.get(right.identifier).cloned();1121 let mut nullish = false;1122 let mut found_ref_id: Option<RefId> = None;11231124 if let Some(RefAccessType::RefValue {1125 ref_id: Some(id), ..1126 }) = &left_type1127 {1128 found_ref_id = Some(*id);1129 } else if let Some(RefAccessType::RefValue {1130 ref_id: Some(id), ..1131 }) = &right_type1132 {1133 found_ref_id = Some(*id);1134 }11351136 if matches!(&left_type, Some(RefAccessType::Nullable)) {1137 nullish = true;1138 } else if matches!(&right_type, Some(RefAccessType::Nullable)) {1139 nullish = true;1140 }11411142 if let Some(ref_id) = found_ref_id {1143 if nullish {1144 ref_env1145 .set(instr.lvalue.identifier, RefAccessType::Guard { ref_id });1146 } else {1147 validate_no_ref_value_access(errors, ref_env, left);1148 validate_no_ref_value_access(errors, ref_env, right);1149 }1150 } else {1151 validate_no_ref_value_access(errors, ref_env, left);1152 validate_no_ref_value_access(errors, ref_env, right);1153 }1154 }1155 _ => {1156 for operand in &canonical_each_instruction_value_operand(&instr.value, env)1157 {1158 validate_no_ref_value_access(errors, ref_env, operand);1159 }1160 }1161 }11621163 // Guard values are derived from ref.current, so they can only be used1164 // in if statement targets1165 for operand in &canonical_each_instruction_value_operand(&instr.value, env) {1166 guard_check(errors, operand, ref_env);1167 }11681169 if is_ref_type(instr.lvalue.identifier, identifiers, types)1170 && !matches!(1171 ref_env.get(instr.lvalue.identifier),1172 Some(RefAccessType::Ref { .. })1173 )1174 {1175 let existing = ref_env1176 .get(instr.lvalue.identifier)1177 .cloned()1178 .unwrap_or(RefAccessType::None);1179 ref_env.set(1180 instr.lvalue.identifier,1181 join_ref_access_types(1182 &existing,1183 &RefAccessType::Ref {1184 ref_id: next_ref_id(),1185 },1186 ),1187 );1188 }1189 if is_ref_value_type(instr.lvalue.identifier, identifiers, types)1190 && !matches!(1191 ref_env.get(instr.lvalue.identifier),1192 Some(RefAccessType::RefValue { .. })1193 )1194 {1195 let existing = ref_env1196 .get(instr.lvalue.identifier)1197 .cloned()1198 .unwrap_or(RefAccessType::None);1199 ref_env.set(1200 instr.lvalue.identifier,1201 join_ref_access_types(1202 &existing,1203 &RefAccessType::RefValue {1204 loc: instr.loc,1205 ref_id: None,1206 },1207 ),1208 );1209 }1210 }12111212 // Check if terminal is an `if` — push safe block for guard1213 if let Terminal::If {1214 test, fallthrough, ..1215 } = &block.terminal1216 {1217 if let Some(RefAccessType::Guard { ref_id }) = ref_env.get(test.identifier) {1218 if !safe_blocks.iter().any(|(_, r)| r == ref_id) {1219 safe_blocks.push((*fallthrough, *ref_id));1220 }1221 }1222 }12231224 // Process terminal operands1225 for operand in &each_terminal_operand(&block.terminal) {1226 if !matches!(&block.terminal, Terminal::Return { .. }) {1227 validate_no_ref_value_access(errors, ref_env, operand);1228 if !matches!(&block.terminal, Terminal::If { .. }) {1229 guard_check(errors, operand, ref_env);1230 }1231 } else {1232 // Allow functions containing refs to be returned, but not direct ref values1233 validate_no_direct_ref_value_access(errors, operand, ref_env);1234 guard_check(errors, operand, ref_env);1235 if let Some(ty) = ref_env.get(operand.identifier) {1236 return_values.push(ty.clone());1237 }1238 }1239 }1240 }12411242 if !errors.is_empty() {1243 return RefAccessType::None;1244 }1245 }12461247 if ref_env.has_changed() {1248 errors.push(CompilerDiagnostic::new(1249 react_compiler_diagnostics::ErrorCategory::Invariant,1250 "Ref type environment did not converge",1251 None,1252 ));1253 return RefAccessType::None;1254 }12551256 join_ref_access_types_many(&return_values)1257}