Warning: Direct indexing (e.g., `vec[i]`, `slice[i]`) panics on out-of-bounds access. Prefer using `.get(index)` or `.get_mut(index)` which return Option<&T>/Option<&mut T>.
let block = &func.body.blocks[&block_id];
1// Copyright (c) Meta Platforms, Inc. and affiliates.2//3// This source code is licensed under the MIT license found in the4// LICENSE file in the root directory of this source tree.56//! Constant propagation/folding pass.7//!8//! Applies Sparse Conditional Constant Propagation to the given function.9//! We use abstract interpretation to record known constant values for identifiers,10//! with lack of a value indicating that the identifier does not have a known11//! constant value.12//!13//! Instructions which can be compile-time evaluated *and* whose operands are known14//! constants are replaced with the resulting constant value.15//!16//! This pass also exploits SSA form, tracking constant values of local variables.17//! For example, in `let x = 4; let y = x + 1` we know that `x = 4` in the binary18//! expression and can replace it with `Constant 5`.19//!20//! This pass also visits conditionals (currently only IfTerminal) and can prune21//! unreachable branches when the condition is a known truthy/falsey constant.22//! The pass uses fixpoint iteration, looping until no additional updates can be23//! performed.24//!25//! Analogous to TS `Optimization/ConstantPropagation.ts`.2627use rustc_hash::FxHashMap;2829use react_compiler_diagnostics::JsString;30use react_compiler_hir::environment::Environment;31use react_compiler_hir::{32 BinaryOperator, BlockKind, FloatValue, FunctionId, GotoVariant, HirFunction, IdentifierId,33 InstructionValue, NonLocalBinding, Phi, Place, PrimitiveValue, PropertyLiteral, SourceLocation,34 Terminal, UnaryOperator, UpdateOperator, format_js_number,35};36use react_compiler_lowering::{37 get_reverse_postordered_blocks, mark_instruction_ids, mark_predecessors,38 remove_dead_do_while_statements, remove_unnecessary_try_catch, remove_unreachable_for_updates,39};40use react_compiler_ssa::enter_ssa::placeholder_function;4142use crate::merge_consecutive_blocks::merge_consecutive_blocks;4344// =============================================================================45// Constant type — mirrors TS `type Constant = Primitive | LoadGlobal`46// The loc is preserved so that when we replace an instruction value with the47// constant, we use the loc from the original definition site (matching TS).48// =============================================================================4950#[derive(Debug, Clone)]51enum Constant {52 Primitive {53 value: PrimitiveValue,54 loc: Option<SourceLocation>,55 },56 LoadGlobal {57 binding: NonLocalBinding,58 loc: Option<SourceLocation>,59 },60}6162impl Constant {63 fn into_instruction_value(self) -> InstructionValue {64 match self {65 Constant::Primitive { value, loc } => InstructionValue::Primitive { value, loc },66 Constant::LoadGlobal { binding, loc } => InstructionValue::LoadGlobal { binding, loc },67 }68 }69}7071/// Map of known constant values. Uses FxHashMap (not IndexMap) since iteration72/// order does not affect correctness — this map is only used for lookups.73type Constants = FxHashMap<IdentifierId, Constant>;7475// =============================================================================76// Public entry point77// =============================================================================7879pub fn constant_propagation(func: &mut HirFunction, env: &mut Environment) {80 let mut constants: Constants = FxHashMap::default();81 constant_propagation_impl(func, env, &mut constants);82}8384fn constant_propagation_impl(85 func: &mut HirFunction,86 env: &mut Environment,87 constants: &mut Constants,88) {89 loop {90 let have_terminals_changed = apply_constant_propagation(func, env, constants);91 if !have_terminals_changed {92 break;93 }94 /*95 * If terminals have changed then blocks may have become newly unreachable.96 * Re-run minification of the graph (incl reordering instruction ids)97 */98 func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions);99 remove_unreachable_for_updates(&mut func.body);100 remove_dead_do_while_statements(&mut func.body);101 remove_unnecessary_try_catch(&mut func.body);102 mark_instruction_ids(&mut func.body, &mut func.instructions);103 mark_predecessors(&mut func.body);104105 // Now that predecessors are updated, prune phi operands that can never be reached106 for (_block_id, block) in func.body.blocks.iter_mut() {107 for phi in &mut block.phis {108 phi.operands109 .retain(|pred, _operand| block.preds.contains(pred));110 }111 }112113 /*114 * By removing some phi operands, there may be phis that were not previously115 * redundant but now are116 */117 react_compiler_ssa::eliminate_redundant_phi(func, env);118119 /*120 * Finally, merge together any blocks that are now guaranteed to execute121 * consecutively122 */123 merge_consecutive_blocks(func, &mut env.functions);124125 // TODO: port assertConsistentIdentifiers(fn) and assertTerminalSuccessorsExist(fn)126 // from TS HIR validation. These are debug assertions that verify structural127 // invariants after the CFG cleanup helpers run.128 }129}130131fn apply_constant_propagation(132 func: &mut HirFunction,133 env: &mut Environment,134 constants: &mut Constants,135) -> bool {136 let mut has_changes = false;137138 let block_ids: Vec<_> = func.body.blocks.keys().copied().collect();139 for block_id in block_ids {140 let block = &func.body.blocks[&block_id];141142 // Initialize phi values if all operands have the same known constant value143 let phi_updates: Vec<(IdentifierId, Constant)> = block144 .phis145 .iter()146 .filter_map(|phi| {147 let value = evaluate_phi(phi, constants)?;148 Some((phi.place.identifier, value))149 })150 .collect();151 for (id, value) in phi_updates {152 constants.insert(id, value);153 }154155 let block = &func.body.blocks[&block_id];156 let instr_ids = block.instructions.clone();157 let block_kind = block.kind;158 let instr_count = instr_ids.len();159160 for (i, instr_id) in instr_ids.iter().enumerate() {161 if block_kind == BlockKind::Sequence && i == instr_count - 1 {162 /*163 * evaluating the last value of a value block can break order of evaluation,164 * skip these instructions165 */166 continue;167 }168 let result = evaluate_instruction(constants, func, env, *instr_id);169 if let Some(value) = result {170 let lvalue_id = func.instructions[instr_id.0 as usize].lvalue.identifier;171 constants.insert(lvalue_id, value);172 }173 }174175 let block = &func.body.blocks[&block_id];176 match &block.terminal {177 Terminal::If {178 test,179 consequent,180 alternate,181 id,182 loc,183 ..184 } => {185 let test_value = read(constants, test);186 if let Some(Constant::Primitive {187 value: ref prim, ..188 }) = test_value189 {190 has_changes = true;191 let target_block_id = if is_truthy(prim) {192 *consequent193 } else {194 *alternate195 };196 let terminal = Terminal::Goto {197 variant: GotoVariant::Break,198 block: target_block_id,199 id: *id,200 loc: *loc,201 };202 func.body.blocks.get_mut(&block_id).unwrap().terminal = terminal;203 }204 }205 Terminal::Unsupported { .. }206 | Terminal::Unreachable { .. }207 | Terminal::Throw { .. }208 | Terminal::Return { .. }209 | Terminal::Goto { .. }210 | Terminal::Branch { .. }211 | Terminal::Switch { .. }212 | Terminal::DoWhile { .. }213 | Terminal::While { .. }214 | Terminal::For { .. }215 | Terminal::ForOf { .. }216 | Terminal::ForIn { .. }217 | Terminal::Logical { .. }218 | Terminal::Ternary { .. }219 | Terminal::Optional { .. }220 | Terminal::Label { .. }221 | Terminal::Sequence { .. }222 | Terminal::MaybeThrow { .. }223 | Terminal::Try { .. }224 | Terminal::Scope { .. }225 | Terminal::PrunedScope { .. } => {226 // no-op227 }228 }229 }230231 has_changes232}233234// =============================================================================235// Phi evaluation236// =============================================================================237238fn evaluate_phi(phi: &Phi, constants: &Constants) -> Option<Constant> {239 let mut value: Option<Constant> = None;240 for (_pred, operand) in &phi.operands {241 let operand_value = constants.get(&operand.identifier)?;242243 match &value {244 None => {245 // first iteration of the loop246 value = Some(operand_value.clone());247 continue;248 }249 Some(current) => match (current, operand_value) {250 (Constant::Primitive { value: a, .. }, Constant::Primitive { value: b, .. }) => {251 // Use JS strict equality semantics: NaN !== NaN252 if !js_strict_equal(a, b) {253 return None;254 }255 }256 (257 Constant::LoadGlobal { binding: a, .. },258 Constant::LoadGlobal { binding: b, .. },259 ) => {260 // different global values, can't constant propagate261 if a.name() != b.name() {262 return None;263 }264 }265 // found different kinds of constants, can't constant propagate266 (Constant::Primitive { .. }, Constant::LoadGlobal { .. })267 | (Constant::LoadGlobal { .. }, Constant::Primitive { .. }) => {268 return None;269 }270 },271 }272 }273 value274}275276// =============================================================================277// Instruction evaluation278// =============================================================================279280fn evaluate_instruction(281 constants: &mut Constants,282 func: &mut HirFunction,283 env: &mut Environment,284 instr_id: react_compiler_hir::InstructionId,285) -> Option<Constant> {286 let instr = &func.instructions[instr_id.0 as usize];287 match &instr.value {288 InstructionValue::Primitive { value, loc } => Some(Constant::Primitive {289 value: value.clone(),290 loc: *loc,291 }),292 InstructionValue::LoadGlobal { binding, loc } => Some(Constant::LoadGlobal {293 binding: binding.clone(),294 loc: *loc,295 }),296 InstructionValue::ComputedLoad {297 object,298 property,299 loc,300 } => {301 let prop_value = read(constants, property);302 if let Some(Constant::Primitive {303 value: ref prim, ..304 }) = prop_value305 {306 match prim {307 PrimitiveValue::String(s) if s.as_str().is_some_and(is_valid_identifier) => {308 let object = object.clone();309 let loc = *loc;310 let new_property =311 PropertyLiteral::String(s.as_str().expect("guarded utf8").to_string());312 func.instructions[instr_id.0 as usize].value =313 InstructionValue::PropertyLoad {314 object,315 property: new_property,316 loc,317 };318 }319 PrimitiveValue::Number(n) => {320 let object = object.clone();321 let loc = *loc;322 let new_property = PropertyLiteral::Number(*n);323 func.instructions[instr_id.0 as usize].value =324 InstructionValue::PropertyLoad {325 object,326 property: new_property,327 loc,328 };329 }330 PrimitiveValue::Null331 | PrimitiveValue::Undefined332 | PrimitiveValue::Boolean(_)333 | PrimitiveValue::String(_) => {}334 }335 }336 None337 }338 InstructionValue::ComputedStore {339 object,340 property,341 value,342 loc,343 } => {344 let prop_value = read(constants, property);345 if let Some(Constant::Primitive {346 value: ref prim, ..347 }) = prop_value348 {349 match prim {350 PrimitiveValue::String(s) if s.as_str().is_some_and(is_valid_identifier) => {351 let object = object.clone();352 let store_value = value.clone();353 let loc = *loc;354 let new_property =355 PropertyLiteral::String(s.as_str().expect("guarded utf8").to_string());356 func.instructions[instr_id.0 as usize].value =357 InstructionValue::PropertyStore {358 object,359 property: new_property,360 value: store_value,361 loc,362 };363 }364 PrimitiveValue::Number(n) => {365 let object = object.clone();366 let store_value = value.clone();367 let loc = *loc;368 let new_property = PropertyLiteral::Number(*n);369 func.instructions[instr_id.0 as usize].value =370 InstructionValue::PropertyStore {371 object,372 property: new_property,373 value: store_value,374 loc,375 };376 }377 PrimitiveValue::Null378 | PrimitiveValue::Undefined379 | PrimitiveValue::Boolean(_)380 | PrimitiveValue::String(_) => {}381 }382 }383 None384 }385 InstructionValue::PostfixUpdate {386 lvalue,387 operation,388 value,389 loc,390 } => {391 let previous = read(constants, value);392 if let Some(Constant::Primitive {393 value: PrimitiveValue::Number(n),394 loc: prev_loc,395 }) = previous396 {397 let prev_val = n.value();398 let next_val = match operation {399 UpdateOperator::Increment => prev_val + 1.0,400 UpdateOperator::Decrement => prev_val - 1.0,401 };402 // Store the updated value for the lvalue403 let lvalue_id = lvalue.identifier;404 constants.insert(405 lvalue_id,406 Constant::Primitive {407 value: PrimitiveValue::Number(FloatValue::new(next_val)),408 loc: *loc,409 },410 );411 // But return the value prior to the update (preserving its original loc)412 return Some(Constant::Primitive {413 value: PrimitiveValue::Number(n),414 loc: prev_loc,415 });416 }417 None418 }419 InstructionValue::PrefixUpdate {420 lvalue,421 operation,422 value,423 loc,424 } => {425 let previous = read(constants, value);426 if let Some(Constant::Primitive {427 value: PrimitiveValue::Number(n),428 ..429 }) = previous430 {431 let prev_val = n.value();432 let next_val = match operation {433 UpdateOperator::Increment => prev_val + 1.0,434 UpdateOperator::Decrement => prev_val - 1.0,435 };436 let result = Constant::Primitive {437 value: PrimitiveValue::Number(FloatValue::new(next_val)),438 loc: *loc,439 };440 // Store and return the updated value441 let lvalue_id = lvalue.identifier;442 constants.insert(lvalue_id, result.clone());443 return Some(result);444 }445 None446 }447 InstructionValue::UnaryExpression {448 operator,449 value,450 loc,451 } => match operator {452 UnaryOperator::Not => {453 let operand = read(constants, value);454 if let Some(Constant::Primitive {455 value: ref prim, ..456 }) = operand457 {458 let negated = !is_truthy(prim);459 let loc = *loc;460 let result = Constant::Primitive {461 value: PrimitiveValue::Boolean(negated),462 loc,463 };464 func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {465 value: PrimitiveValue::Boolean(negated),466 loc,467 };468 return Some(result);469 }470 None471 }472 UnaryOperator::Minus => {473 let operand = read(constants, value);474 if let Some(Constant::Primitive {475 value: PrimitiveValue::Number(n),476 ..477 }) = operand478 {479 let negated = n.value() * -1.0;480 let loc = *loc;481 let result = Constant::Primitive {482 value: PrimitiveValue::Number(FloatValue::new(negated)),483 loc,484 };485 func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {486 value: PrimitiveValue::Number(FloatValue::new(negated)),487 loc,488 };489 return Some(result);490 }491 None492 }493 UnaryOperator::Plus494 | UnaryOperator::BitwiseNot495 | UnaryOperator::TypeOf496 | UnaryOperator::Void => None,497 },498 InstructionValue::BinaryExpression {499 operator,500 left,501 right,502 loc,503 } => {504 let lhs_value = read(constants, left);505 let rhs_value = read(constants, right);506 if let (507 Some(Constant::Primitive { value: lhs, .. }),508 Some(Constant::Primitive { value: rhs, .. }),509 ) = (&lhs_value, &rhs_value)510 {511 let result = evaluate_binary_op(*operator, lhs, rhs);512 if let Some(ref prim) = result {513 let loc = *loc;514 func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {515 value: prim.clone(),516 loc,517 };518 return Some(Constant::Primitive {519 value: prim.clone(),520 loc,521 });522 }523 }524 None525 }526 InstructionValue::PropertyLoad {527 object,528 property,529 loc,530 } => {531 let object_value = read(constants, object);532 if let Some(Constant::Primitive {533 value: PrimitiveValue::String(ref s),534 ..535 }) = object_value536 {537 if let PropertyLiteral::String(prop_name) = property {538 if prop_name == "length" {539 // Use UTF-16 code unit count to match JS .length semantics540 let len = s.len_utf16() as f64;541 let loc = *loc;542 let result = Constant::Primitive {543 value: PrimitiveValue::Number(FloatValue::new(len)),544 loc,545 };546 func.instructions[instr_id.0 as usize].value =547 InstructionValue::Primitive {548 value: PrimitiveValue::Number(FloatValue::new(len)),549 loc,550 };551 return Some(result);552 }553 }554 }555 None556 }557 InstructionValue::TemplateLiteral {558 subexprs,559 quasis,560 loc,561 } => {562 if subexprs.is_empty() {563 // No subexpressions: join all cooked quasis564 let mut result_string = String::new();565 for q in quasis {566 match &q.cooked {567 Some(cooked) => result_string.push_str(cooked),568 None => return None,569 }570 }571 let loc = *loc;572 let result = Constant::Primitive {573 value: PrimitiveValue::String(JsString::from_marker_string(&result_string)),574 loc,575 };576 func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {577 value: PrimitiveValue::String(JsString::from_marker_string(&result_string)),578 loc,579 };580 return Some(result);581 }582583 if subexprs.len() != quasis.len() - 1 {584 return None;585 }586587 if quasis.iter().any(|q| q.cooked.is_none()) {588 return None;589 }590591 let mut quasi_index = 0usize;592 let mut result_string = quasis[quasi_index].cooked.as_ref().unwrap().clone();593 quasi_index += 1;594595 for sub_expr in subexprs {596 let sub_expr_value = read(constants, sub_expr);597 let sub_prim = match sub_expr_value {598 Some(Constant::Primitive { ref value, .. }) => value,599 _ => return None,600 };601602 let expression_str = match sub_prim {603 PrimitiveValue::Null => "null".to_string(),604 PrimitiveValue::Boolean(b) => b.to_string(),605 PrimitiveValue::Number(n) => format_js_number(n.value()),606 PrimitiveValue::String(s) => s.to_marker_string(),607 // TS rejects undefined subexpression values608 PrimitiveValue::Undefined => return None,609 };610611 let suffix = match &quasis[quasi_index].cooked {612 Some(s) => s.clone(),613 None => return None,614 };615 quasi_index += 1;616617 result_string.push_str(&expression_str);618 result_string.push_str(&suffix);619 }620621 let loc = *loc;622 let result = Constant::Primitive {623 value: PrimitiveValue::String(JsString::from_marker_string(&result_string)),624 loc,625 };626 func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {627 value: PrimitiveValue::String(JsString::from_marker_string(&result_string)),628 loc,629 };630 Some(result)631 }632 InstructionValue::LoadLocal { place, .. } => {633 let place_value = read(constants, place);634 if let Some(ref constant) = place_value {635 // Replace the LoadLocal with the constant value (including the constant's original loc)636 func.instructions[instr_id.0 as usize].value =637 constant.clone().into_instruction_value();638 }639 place_value640 }641 InstructionValue::StoreLocal { lvalue, value, .. } => {642 let place_value = read(constants, value);643 if let Some(ref constant) = place_value {644 let lvalue_id = lvalue.place.identifier;645 constants.insert(lvalue_id, constant.clone());646 }647 place_value648 }649 InstructionValue::FunctionExpression { lowered_func, .. } => {650 let func_id = lowered_func.func;651 process_inner_function(func_id, env, constants);652 None653 }654 InstructionValue::ObjectMethod { lowered_func, .. } => {655 let func_id = lowered_func.func;656 process_inner_function(func_id, env, constants);657 None658 }659 InstructionValue::StartMemoize { deps, .. } => {660 if let Some(deps) = deps {661 // Two-phase: collect which deps are constant, then mutate662 let const_dep_indices: Vec<usize> = deps663 .iter()664 .enumerate()665 .filter_map(|(i, dep)| {666 if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal {667 value,668 ..669 } = &dep.root670 {671 let pv = read(constants, value);672 if matches!(pv, Some(Constant::Primitive { .. })) {673 return Some(i);674 }675 }676 None677 })678 .collect();679 for idx in const_dep_indices {680 if let InstructionValue::StartMemoize {681 deps: Some(ref mut deps),682 ..683 } = func.instructions[instr_id.0 as usize].value684 {685 if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal {686 constant,687 ..688 } = &mut deps[idx].root689 {690 *constant = true;691 }692 }693 }694 }695 None696 }697 // All other instruction kinds: no constant folding698 InstructionValue::LoadContext { .. }699 | InstructionValue::DeclareLocal { .. }700 | InstructionValue::DeclareContext { .. }701 | InstructionValue::StoreContext { .. }702 | InstructionValue::Destructure { .. }703 | InstructionValue::JSXText { .. }704 | InstructionValue::NewExpression { .. }705 | InstructionValue::CallExpression { .. }706 | InstructionValue::MethodCall { .. }707 | InstructionValue::TypeCastExpression { .. }708 | InstructionValue::JsxExpression { .. }709 | InstructionValue::ObjectExpression { .. }710 | InstructionValue::ArrayExpression { .. }711 | InstructionValue::JsxFragment { .. }712 | InstructionValue::RegExpLiteral { .. }713 | InstructionValue::MetaProperty { .. }714 | InstructionValue::PropertyStore { .. }715 | InstructionValue::PropertyDelete { .. }716 | InstructionValue::ComputedDelete { .. }717 | InstructionValue::StoreGlobal { .. }718 | InstructionValue::TaggedTemplateExpression { .. }719 | InstructionValue::Await { .. }720 | InstructionValue::GetIterator { .. }721 | InstructionValue::IteratorNext { .. }722 | InstructionValue::NextPropertyOf { .. }723 | InstructionValue::Debugger { .. }724 | InstructionValue::FinishMemoize { .. }725 | InstructionValue::UnsupportedNode { .. } => None,726 }727}728729// =============================================================================730// Inner function processing731// =============================================================================732733fn process_inner_function(func_id: FunctionId, env: &mut Environment, constants: &mut Constants) {734 let mut inner = std::mem::replace(735 &mut env.functions[func_id.0 as usize],736 placeholder_function(),737 );738 constant_propagation_impl(&mut inner, env, constants);739 env.functions[func_id.0 as usize] = inner;740}741742// =============================================================================743// Helper: read constant for a place744// =============================================================================745746fn read(constants: &Constants, place: &Place) -> Option<Constant> {747 constants.get(&place.identifier).cloned()748}749750// =============================================================================751// Helper: is_valid_identifier752// =============================================================================753754/// Check if a string is a valid JavaScript identifier.755/// Supports Unicode identifier characters per ECMAScript spec (ID_Start / ID_Continue).756/// Rejects JS reserved words (matching Babel's `isValidIdentifier` default behavior).757fn is_valid_identifier(s: &str) -> bool {758 if s.is_empty() {759 return false;760 }761 let mut chars = s.chars();762 match chars.next() {763 Some(c) if is_id_start(c) => {}764 _ => return false,765 }766 if !chars.all(is_id_continue) {767 return false;768 }769 !is_reserved_word(s)770}771772/// JS reserved words that cannot be used as identifiers.773/// Includes keywords, future reserved words, and strict mode reserved words.774fn is_reserved_word(s: &str) -> bool {775 matches!(776 s,777 "break"778 | "case"779 | "catch"780 | "continue"781 | "debugger"782 | "default"783 | "do"784 | "else"785 | "finally"786 | "for"787 | "function"788 | "if"789 | "in"790 | "instanceof"791 | "new"792 | "return"793 | "switch"794 | "this"795 | "throw"796 | "try"797 | "typeof"798 | "var"799 | "void"800 | "while"801 | "with"802 | "class"803 | "const"804 | "enum"805 | "export"806 | "extends"807 | "import"808 | "super"809 | "implements"810 | "interface"811 | "let"812 | "package"813 | "private"814 | "protected"815 | "public"816 | "static"817 | "yield"818 | "await"819 | "delete"820 | "null"821 | "true"822 | "false"823 )824}825826/// Check if a character is valid as the start of a JS identifier (ID_Start + _ + $).827fn is_id_start(c: char) -> bool {828 c == '_' || c == '$' || c.is_alphabetic()829}830831/// Check if a character is valid as a continuation of a JS identifier (ID_Continue + $ + \u200C + \u200D).832fn is_id_continue(c: char) -> bool {833 c == '$'834 || c == '_'835 || c.is_alphanumeric()836 || c == '\u{200C}' // ZWNJ837 || c == '\u{200D}' // ZWJ838}839840// =============================================================================841// Helper: is_truthy for PrimitiveValue842// =============================================================================843844fn is_truthy(value: &PrimitiveValue) -> bool {845 match value {846 PrimitiveValue::Null => false,847 PrimitiveValue::Undefined => false,848 PrimitiveValue::Boolean(b) => *b,849 PrimitiveValue::Number(n) => {850 let v = n.value();851 v != 0.0 && !v.is_nan()852 }853 PrimitiveValue::String(s) => s.len_utf16() != 0,854 }855}856857// =============================================================================858// Binary operation evaluation859// =============================================================================860861fn evaluate_binary_op(862 operator: BinaryOperator,863 lhs: &PrimitiveValue,864 rhs: &PrimitiveValue,865) -> Option<PrimitiveValue> {866 match operator {867 BinaryOperator::Add => match (lhs, rhs) {868 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(PrimitiveValue::Number(869 FloatValue::new(l.value() + r.value()),870 )),871 (PrimitiveValue::String(l), PrimitiveValue::String(r)) => {872 // Concatenate as code units: JS `+` can pair up surrogate873 // halves split across the operands.874 let mut units = l.code_units();875 units.extend(r.code_units());876 Some(PrimitiveValue::String(877 react_compiler_diagnostics::JsString::from_code_units(units),878 ))879 }880 _ => None,881 },882 BinaryOperator::Subtract => match (lhs, rhs) {883 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(PrimitiveValue::Number(884 FloatValue::new(l.value() - r.value()),885 )),886 _ => None,887 },888 BinaryOperator::Multiply => match (lhs, rhs) {889 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(PrimitiveValue::Number(890 FloatValue::new(l.value() * r.value()),891 )),892 _ => None,893 },894 BinaryOperator::Divide => match (lhs, rhs) {895 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(PrimitiveValue::Number(896 FloatValue::new(l.value() / r.value()),897 )),898 _ => None,899 },900 BinaryOperator::Modulo => match (lhs, rhs) {901 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(PrimitiveValue::Number(902 FloatValue::new(l.value() % r.value()),903 )),904 _ => None,905 },906 BinaryOperator::Exponent => match (lhs, rhs) {907 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(PrimitiveValue::Number(908 FloatValue::new(l.value().powf(r.value())),909 )),910 _ => None,911 },912 BinaryOperator::BitwiseOr => match (lhs, rhs) {913 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {914 let result = js_to_int32(l.value()) | js_to_int32(r.value());915 Some(PrimitiveValue::Number(FloatValue::new(result as f64)))916 }917 _ => None,918 },919 BinaryOperator::BitwiseAnd => match (lhs, rhs) {920 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {921 let result = js_to_int32(l.value()) & js_to_int32(r.value());922 Some(PrimitiveValue::Number(FloatValue::new(result as f64)))923 }924 _ => None,925 },926 BinaryOperator::BitwiseXor => match (lhs, rhs) {927 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {928 let result = js_to_int32(l.value()) ^ js_to_int32(r.value());929 Some(PrimitiveValue::Number(FloatValue::new(result as f64)))930 }931 _ => None,932 },933 BinaryOperator::ShiftLeft => match (lhs, rhs) {934 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {935 let result = js_to_int32(l.value()) << (js_to_uint32(r.value()) & 0x1f);936 Some(PrimitiveValue::Number(FloatValue::new(result as f64)))937 }938 _ => None,939 },940 BinaryOperator::ShiftRight => match (lhs, rhs) {941 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {942 let result = js_to_int32(l.value()) >> (js_to_uint32(r.value()) & 0x1f);943 Some(PrimitiveValue::Number(FloatValue::new(result as f64)))944 }945 _ => None,946 },947 BinaryOperator::UnsignedShiftRight => match (lhs, rhs) {948 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {949 let result = js_to_uint32(l.value()) >> (js_to_uint32(r.value()) & 0x1f);950 Some(PrimitiveValue::Number(FloatValue::new(result as f64)))951 }952 _ => None,953 },954 BinaryOperator::LessThan => match (lhs, rhs) {955 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {956 Some(PrimitiveValue::Boolean(l.value() < r.value()))957 }958 _ => None,959 },960 BinaryOperator::LessEqual => match (lhs, rhs) {961 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {962 Some(PrimitiveValue::Boolean(l.value() <= r.value()))963 }964 _ => None,965 },966 BinaryOperator::GreaterThan => match (lhs, rhs) {967 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {968 Some(PrimitiveValue::Boolean(l.value() > r.value()))969 }970 _ => None,971 },972 BinaryOperator::GreaterEqual => match (lhs, rhs) {973 (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {974 Some(PrimitiveValue::Boolean(l.value() >= r.value()))975 }976 _ => None,977 },978 BinaryOperator::StrictEqual => Some(PrimitiveValue::Boolean(js_strict_equal(lhs, rhs))),979 BinaryOperator::StrictNotEqual => Some(PrimitiveValue::Boolean(!js_strict_equal(lhs, rhs))),980 BinaryOperator::Equal => Some(PrimitiveValue::Boolean(js_abstract_equal(lhs, rhs))),981 BinaryOperator::NotEqual => Some(PrimitiveValue::Boolean(!js_abstract_equal(lhs, rhs))),982 BinaryOperator::In | BinaryOperator::InstanceOf => None,983 }984}985986// =============================================================================987// JavaScript equality semantics988// =============================================================================989990fn js_strict_equal(lhs: &PrimitiveValue, rhs: &PrimitiveValue) -> bool {991 match (lhs, rhs) {992 (PrimitiveValue::Null, PrimitiveValue::Null) => true,993 (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true,994 (PrimitiveValue::Boolean(a), PrimitiveValue::Boolean(b)) => a == b,995 (PrimitiveValue::Number(a), PrimitiveValue::Number(b)) => {996 let av = a.value();997 let bv = b.value();998 // NaN !== NaN in JS999 if av.is_nan() || bv.is_nan() {1000 return false;1001 }1002 av == bv1003 }1004 (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b,1005 // Different types => false1006 _ => false,1007 }1008}10091010/// Convert a string to a number using JS `ToNumber` semantics.1011/// In JS: `""` → 0, `" "` → 0, `" 42 "` → 42, `"0x1A"` → 26, `"Infinity"` → Infinity.1012fn js_to_number(s: &str) -> f64 {1013 let trimmed = s.trim();1014 if trimmed.is_empty() {1015 return 0.0;1016 }1017 if trimmed == "Infinity" || trimmed == "+Infinity" {1018 return f64::INFINITY;1019 }1020 if trimmed == "-Infinity" {1021 return f64::NEG_INFINITY;1022 }1023 // Handle hex literals (0x/0X)1024 if trimmed.starts_with("0x") || trimmed.starts_with("0X") {1025 return match u64::from_str_radix(&trimmed[2..], 16) {1026 Ok(v) => v as f64,1027 Err(_) => f64::NAN,1028 };1029 }1030 // Handle octal literals (0o/0O)1031 if trimmed.starts_with("0o") || trimmed.starts_with("0O") {1032 return match u64::from_str_radix(&trimmed[2..], 8) {1033 Ok(v) => v as f64,1034 Err(_) => f64::NAN,1035 };1036 }1037 // Handle binary literals (0b/0B)1038 if trimmed.starts_with("0b") || trimmed.starts_with("0B") {1039 return match u64::from_str_radix(&trimmed[2..], 2) {1040 Ok(v) => v as f64,1041 Err(_) => f64::NAN,1042 };1043 }1044 trimmed.parse::<f64>().unwrap_or(f64::NAN)1045}10461047fn js_abstract_equal(lhs: &PrimitiveValue, rhs: &PrimitiveValue) -> bool {1048 match (lhs, rhs) {1049 (PrimitiveValue::Null, PrimitiveValue::Null) => true,1050 (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true,1051 (PrimitiveValue::Null, PrimitiveValue::Undefined)1052 | (PrimitiveValue::Undefined, PrimitiveValue::Null) => true,1053 (PrimitiveValue::Boolean(a), PrimitiveValue::Boolean(b)) => a == b,1054 (PrimitiveValue::Number(a), PrimitiveValue::Number(b)) => {1055 let av = a.value();1056 let bv = b.value();1057 if av.is_nan() || bv.is_nan() {1058 return false;1059 }1060 av == bv1061 }1062 (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b,1063 // Cross-type coercions for primitives1064 (PrimitiveValue::Number(n), PrimitiveValue::String(s))1065 | (PrimitiveValue::String(s), PrimitiveValue::Number(n)) => {1066 // String is coerced to number using JS ToNumber semantics.1067 // Ill-formed strings coerce to NaN, like any non-numeric text.1068 let sv = match s.as_str() {1069 Some(utf8) => js_to_number(utf8),1070 None => f64::NAN,1071 };1072 let nv = n.value();1073 if nv.is_nan() || sv.is_nan() {1074 false1075 } else {1076 nv == sv1077 }1078 }1079 (PrimitiveValue::Boolean(b), other) => {1080 let num = if *b { 1.0 } else { 0.0 };1081 js_abstract_equal(&PrimitiveValue::Number(FloatValue::new(num)), other)1082 }1083 (other, PrimitiveValue::Boolean(b)) => {1084 let num = if *b { 1.0 } else { 0.0 };1085 js_abstract_equal(other, &PrimitiveValue::Number(FloatValue::new(num)))1086 }1087 // null/undefined vs number/string => false1088 _ => false,1089 }1090}10911092// =============================================================================1093// JavaScript Number.toString() approximation1094// =============================================================================10951096/// ECMAScript ToInt32: convert f64 to i32 with modular (wrapping) semantics.1097fn js_to_int32(n: f64) -> i32 {1098 if n.is_nan() || n.is_infinite() || n == 0.0 {1099 return 0;1100 }1101 // Truncate, then wrap to 32 bits1102 let int64 = (n.trunc() as i64) & 0xFFFFFFFF;1103 // Reinterpret as signed i321104 if int64 >= 0x80000000 {1105 (int64 as u32) as i321106 } else {1107 int64 as i321108 }1109}11101111/// ECMAScript ToUint32: convert f64 to u32 with modular (wrapping) semantics.1112fn js_to_uint32(n: f64) -> u32 {1113 js_to_int32(n) as u321114}
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.