Skip to main content

react_compiler_optimization/
constant_propagation.rs

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//! 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 known
11//! constant value.
12//!
13//! Instructions which can be compile-time evaluated *and* whose operands are known
14//! 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 binary
18//! expression and can replace it with `Constant 5`.
19//!
20//! This pass also visits conditionals (currently only IfTerminal) and can prune
21//! unreachable branches when the condition is a known truthy/falsey constant.
22//! The pass uses fixpoint iteration, looping until no additional updates can be
23//! performed.
24//!
25//! Analogous to TS `Optimization/ConstantPropagation.ts`.
26
27use rustc_hash::FxHashMap;
28
29use 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;
41
42use crate::merge_consecutive_blocks::merge_consecutive_blocks;
43
44// =============================================================================
45// Constant type — mirrors TS `type Constant = Primitive | LoadGlobal`
46// The loc is preserved so that when we replace an instruction value with the
47// constant, we use the loc from the original definition site (matching TS).
48// =============================================================================
49
50#[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}
61
62impl 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}
70
71/// Map of known constant values. Uses FxHashMap (not FxIndexMap) since iteration
72/// order does not affect correctness — this map is only used for lookups.
73type Constants = FxHashMap<IdentifierId, Constant>;
74
75// =============================================================================
76// Public entry point
77// =============================================================================
78
79pub 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}
83
84fn 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);
104
105        // Now that predecessors are updated, prune phi operands that can never be reached
106        for (_block_id, block) in func.body.blocks.iter_mut() {
107            for phi in &mut block.phis {
108                phi.operands
109                    .retain(|pred, _operand| block.preds.contains(pred));
110            }
111        }
112
113        /*
114         * By removing some phi operands, there may be phis that were not previously
115         * redundant but now are
116         */
117        react_compiler_ssa::eliminate_redundant_phi(func, env);
118
119        /*
120         * Finally, merge together any blocks that are now guaranteed to execute
121         * consecutively
122         */
123        merge_consecutive_blocks(func, &mut env.functions);
124
125        // TODO: port assertConsistentIdentifiers(fn) and assertTerminalSuccessorsExist(fn)
126        // from TS HIR validation. These are debug assertions that verify structural
127        // invariants after the CFG cleanup helpers run.
128    }
129}
130
131fn apply_constant_propagation(
132    func: &mut HirFunction,
133    env: &mut Environment,
134    constants: &mut Constants,
135) -> bool {
136    let mut has_changes = false;
137
138    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];
141
142        // Initialize phi values if all operands have the same known constant value
143        let phi_updates: Vec<(IdentifierId, Constant)> = block
144            .phis
145            .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        }
154
155        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();
159
160        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 instructions
165                 */
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        }
174
175        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_value
189                {
190                    has_changes = true;
191                    let target_block_id = if is_truthy(prim) {
192                        *consequent
193                    } else {
194                        *alternate
195                    };
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-op
227            }
228        }
229    }
230
231    has_changes
232}
233
234// =============================================================================
235// Phi evaluation
236// =============================================================================
237
238fn 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)?;
242
243        match &value {
244            None => {
245                // first iteration of the loop
246                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 !== NaN
252                    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 propagate
261                    if a.name() != b.name() {
262                        return None;
263                    }
264                }
265                // found different kinds of constants, can't constant propagate
266                (Constant::Primitive { .. }, Constant::LoadGlobal { .. })
267                | (Constant::LoadGlobal { .. }, Constant::Primitive { .. }) => {
268                    return None;
269                }
270            },
271        }
272    }
273    value
274}
275
276// =============================================================================
277// Instruction evaluation
278// =============================================================================
279
280fn 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_value
305            {
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::Null
331                    | PrimitiveValue::Undefined
332                    | PrimitiveValue::Boolean(_)
333                    | PrimitiveValue::String(_) => {}
334                }
335            }
336            None
337        }
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_value
348            {
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::Null
378                    | PrimitiveValue::Undefined
379                    | PrimitiveValue::Boolean(_)
380                    | PrimitiveValue::String(_) => {}
381                }
382            }
383            None
384        }
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            }) = previous
396            {
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 lvalue
403                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            None
418        }
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            }) = previous
430            {
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 value
441                let lvalue_id = lvalue.identifier;
442                constants.insert(lvalue_id, result.clone());
443                return Some(result);
444            }
445            None
446        }
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                }) = operand
457                {
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                None
471            }
472            UnaryOperator::Minus => {
473                let operand = read(constants, value);
474                if let Some(Constant::Primitive {
475                    value: PrimitiveValue::Number(n),
476                    ..
477                }) = operand
478                {
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                None
492            }
493            UnaryOperator::Plus
494            | UnaryOperator::BitwiseNot
495            | UnaryOperator::TypeOf
496            | 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            None
525        }
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_value
536            {
537                if let PropertyLiteral::String(prop_name) = property {
538                    if prop_name == "length" {
539                        // Use UTF-16 code unit count to match JS .length semantics
540                        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            None
556        }
557        InstructionValue::TemplateLiteral {
558            subexprs,
559            quasis,
560            loc,
561        } => {
562            if subexprs.is_empty() {
563                // No subexpressions: join all cooked quasis
564                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            }
582
583            if subexprs.len() != quasis.len() - 1 {
584                return None;
585            }
586
587            if quasis.iter().any(|q| q.cooked.is_none()) {
588                return None;
589            }
590
591            let mut quasi_index = 0usize;
592            let mut result_string = quasis[quasi_index].cooked.as_ref().unwrap().clone();
593            quasi_index += 1;
594
595            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                };
601
602                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 values
608                    PrimitiveValue::Undefined => return None,
609                };
610
611                let suffix = match &quasis[quasi_index].cooked {
612                    Some(s) => s.clone(),
613                    None => return None,
614                };
615                quasi_index += 1;
616
617                result_string.push_str(&expression_str);
618                result_string.push_str(&suffix);
619            }
620
621            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_value
640        }
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_value
648        }
649        InstructionValue::FunctionExpression { lowered_func, .. } => {
650            let func_id = lowered_func.func;
651            process_inner_function(func_id, env, constants);
652            None
653        }
654        InstructionValue::ObjectMethod { lowered_func, .. } => {
655            let func_id = lowered_func.func;
656            process_inner_function(func_id, env, constants);
657            None
658        }
659        InstructionValue::StartMemoize { deps, .. } => {
660            if let Some(deps) = deps {
661                // Two-phase: collect which deps are constant, then mutate
662                let const_dep_indices: Vec<usize> = deps
663                    .iter()
664                    .enumerate()
665                    .filter_map(|(i, dep)| {
666                        if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal {
667                            value,
668                            ..
669                        } = &dep.root
670                        {
671                            let pv = read(constants, value);
672                            if matches!(pv, Some(Constant::Primitive { .. })) {
673                                return Some(i);
674                            }
675                        }
676                        None
677                    })
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].value
684                    {
685                        if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal {
686                            constant,
687                            ..
688                        } = &mut deps[idx].root
689                        {
690                            *constant = true;
691                        }
692                    }
693                }
694            }
695            None
696        }
697        // All other instruction kinds: no constant folding
698        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}
728
729// =============================================================================
730// Inner function processing
731// =============================================================================
732
733fn 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}
741
742// =============================================================================
743// Helper: read constant for a place
744// =============================================================================
745
746fn read(constants: &Constants, place: &Place) -> Option<Constant> {
747    constants.get(&place.identifier).cloned()
748}
749
750// =============================================================================
751// Helper: is_valid_identifier
752// =============================================================================
753
754/// 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}
771
772/// 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}
825
826/// 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}
830
831/// 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}' // ZWNJ
837        || c == '\u{200D}' // ZWJ
838}
839
840// =============================================================================
841// Helper: is_truthy for PrimitiveValue
842// =============================================================================
843
844fn 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}
856
857// =============================================================================
858// Binary operation evaluation
859// =============================================================================
860
861fn 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 surrogate
873                // 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}
985
986// =============================================================================
987// JavaScript equality semantics
988// =============================================================================
989
990fn 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 JS
999            if av.is_nan() || bv.is_nan() {
1000                return false;
1001            }
1002            av == bv
1003        }
1004        (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b,
1005        // Different types => false
1006        _ => false,
1007    }
1008}
1009
1010/// 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}
1046
1047fn 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 == bv
1061        }
1062        (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b,
1063        // Cross-type coercions for primitives
1064        (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                false
1075            } else {
1076                nv == sv
1077            }
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 => false
1088        _ => false,
1089    }
1090}
1091
1092// =============================================================================
1093// JavaScript Number.toString() approximation
1094// =============================================================================
1095
1096/// 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 bits
1102    let int64 = (n.trunc() as i64) & 0xFFFFFFFF;
1103    // Reinterpret as signed i32
1104    if int64 >= 0x80000000 {
1105        (int64 as u32) as i32
1106    } else {
1107        int64 as i32
1108    }
1109}
1110
1111/// ECMAScript ToUint32: convert f64 to u32 with modular (wrapping) semantics.
1112fn js_to_uint32(n: f64) -> u32 {
1113    js_to_int32(n) as u32
1114}