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 std::collections::HashMap;
28
29use react_compiler_hir::environment::Environment;
30use react_compiler_hir::{
31    BinaryOperator, BlockKind, FloatValue, FunctionId, GotoVariant, HirFunction, IdentifierId,
32    InstructionValue, NonLocalBinding, Phi, Place, PrimitiveValue, PropertyLiteral, SourceLocation,
33    Terminal, UnaryOperator, UpdateOperator, format_js_number,
34};
35use react_compiler_lowering::{
36    get_reverse_postordered_blocks, mark_instruction_ids, mark_predecessors,
37    remove_dead_do_while_statements, remove_unnecessary_try_catch, remove_unreachable_for_updates,
38};
39use react_compiler_ssa::enter_ssa::placeholder_function;
40
41use crate::merge_consecutive_blocks::merge_consecutive_blocks;
42
43// =============================================================================
44// Constant type — mirrors TS `type Constant = Primitive | LoadGlobal`
45// The loc is preserved so that when we replace an instruction value with the
46// constant, we use the loc from the original definition site (matching TS).
47// =============================================================================
48
49#[derive(Debug, Clone)]
50enum Constant {
51    Primitive {
52        value: PrimitiveValue,
53        loc: Option<SourceLocation>,
54    },
55    LoadGlobal {
56        binding: NonLocalBinding,
57        loc: Option<SourceLocation>,
58    },
59}
60
61impl Constant {
62    fn into_instruction_value(self) -> InstructionValue {
63        match self {
64            Constant::Primitive { value, loc } => InstructionValue::Primitive { value, loc },
65            Constant::LoadGlobal { binding, loc } => InstructionValue::LoadGlobal { binding, loc },
66        }
67    }
68}
69
70/// Map of known constant values. Uses HashMap (not IndexMap) since iteration
71/// order does not affect correctness — this map is only used for lookups.
72type Constants = HashMap<IdentifierId, Constant>;
73
74// =============================================================================
75// Public entry point
76// =============================================================================
77
78pub fn constant_propagation(func: &mut HirFunction, env: &mut Environment) {
79    let mut constants: Constants = HashMap::new();
80    constant_propagation_impl(func, env, &mut constants);
81}
82
83fn constant_propagation_impl(
84    func: &mut HirFunction,
85    env: &mut Environment,
86    constants: &mut Constants,
87) {
88    loop {
89        let have_terminals_changed = apply_constant_propagation(func, env, constants);
90        if !have_terminals_changed {
91            break;
92        }
93        /*
94         * If terminals have changed then blocks may have become newly unreachable.
95         * Re-run minification of the graph (incl reordering instruction ids)
96         */
97        func.body.blocks = get_reverse_postordered_blocks(&func.body, &func.instructions);
98        remove_unreachable_for_updates(&mut func.body);
99        remove_dead_do_while_statements(&mut func.body);
100        remove_unnecessary_try_catch(&mut func.body);
101        mark_instruction_ids(&mut func.body, &mut func.instructions);
102        mark_predecessors(&mut func.body);
103
104        // Now that predecessors are updated, prune phi operands that can never be reached
105        for (_block_id, block) in func.body.blocks.iter_mut() {
106            for phi in &mut block.phis {
107                phi.operands
108                    .retain(|pred, _operand| block.preds.contains(pred));
109            }
110        }
111
112        /*
113         * By removing some phi operands, there may be phis that were not previously
114         * redundant but now are
115         */
116        react_compiler_ssa::eliminate_redundant_phi(func, env);
117
118        /*
119         * Finally, merge together any blocks that are now guaranteed to execute
120         * consecutively
121         */
122        merge_consecutive_blocks(func, &mut env.functions);
123
124        // TODO: port assertConsistentIdentifiers(fn) and assertTerminalSuccessorsExist(fn)
125        // from TS HIR validation. These are debug assertions that verify structural
126        // invariants after the CFG cleanup helpers run.
127    }
128}
129
130fn apply_constant_propagation(
131    func: &mut HirFunction,
132    env: &mut Environment,
133    constants: &mut Constants,
134) -> bool {
135    let mut has_changes = false;
136
137    let block_ids: Vec<_> = func.body.blocks.keys().copied().collect();
138    for block_id in block_ids {
139        let block = &func.body.blocks[&block_id];
140
141        // Initialize phi values if all operands have the same known constant value
142        let phi_updates: Vec<(IdentifierId, Constant)> = block
143            .phis
144            .iter()
145            .filter_map(|phi| {
146                let value = evaluate_phi(phi, constants)?;
147                Some((phi.place.identifier, value))
148            })
149            .collect();
150        for (id, value) in phi_updates {
151            constants.insert(id, value);
152        }
153
154        let block = &func.body.blocks[&block_id];
155        let instr_ids = block.instructions.clone();
156        let block_kind = block.kind;
157        let instr_count = instr_ids.len();
158
159        for (i, instr_id) in instr_ids.iter().enumerate() {
160            if block_kind == BlockKind::Sequence && i == instr_count - 1 {
161                /*
162                 * evaluating the last value of a value block can break order of evaluation,
163                 * skip these instructions
164                 */
165                continue;
166            }
167            let result = evaluate_instruction(constants, func, env, *instr_id);
168            if let Some(value) = result {
169                let lvalue_id = func.instructions[instr_id.0 as usize].lvalue.identifier;
170                constants.insert(lvalue_id, value);
171            }
172        }
173
174        let block = &func.body.blocks[&block_id];
175        match &block.terminal {
176            Terminal::If {
177                test,
178                consequent,
179                alternate,
180                id,
181                loc,
182                ..
183            } => {
184                let test_value = read(constants, test);
185                if let Some(Constant::Primitive { value: ref prim, .. }) = test_value {
186                    has_changes = true;
187                    let target_block_id = if is_truthy(prim) {
188                        *consequent
189                    } else {
190                        *alternate
191                    };
192                    let terminal = Terminal::Goto {
193                        variant: GotoVariant::Break,
194                        block: target_block_id,
195                        id: *id,
196                        loc: *loc,
197                    };
198                    func.body.blocks.get_mut(&block_id).unwrap().terminal = terminal;
199                }
200            }
201            Terminal::Unsupported { .. }
202            | Terminal::Unreachable { .. }
203            | Terminal::Throw { .. }
204            | Terminal::Return { .. }
205            | Terminal::Goto { .. }
206            | Terminal::Branch { .. }
207            | Terminal::Switch { .. }
208            | Terminal::DoWhile { .. }
209            | Terminal::While { .. }
210            | Terminal::For { .. }
211            | Terminal::ForOf { .. }
212            | Terminal::ForIn { .. }
213            | Terminal::Logical { .. }
214            | Terminal::Ternary { .. }
215            | Terminal::Optional { .. }
216            | Terminal::Label { .. }
217            | Terminal::Sequence { .. }
218            | Terminal::MaybeThrow { .. }
219            | Terminal::Try { .. }
220            | Terminal::Scope { .. }
221            | Terminal::PrunedScope { .. } => {
222                // no-op
223            }
224        }
225    }
226
227    has_changes
228}
229
230// =============================================================================
231// Phi evaluation
232// =============================================================================
233
234fn evaluate_phi(phi: &Phi, constants: &Constants) -> Option<Constant> {
235    let mut value: Option<Constant> = None;
236    for (_pred, operand) in &phi.operands {
237        let operand_value = constants.get(&operand.identifier)?;
238
239        match &value {
240            None => {
241                // first iteration of the loop
242                value = Some(operand_value.clone());
243                continue;
244            }
245            Some(current) => match (current, operand_value) {
246                (
247                    Constant::Primitive { value: a, .. },
248                    Constant::Primitive { value: b, .. },
249                ) => {
250                    // Use JS strict equality semantics: NaN !== NaN
251                    if !js_strict_equal(a, b) {
252                        return None;
253                    }
254                }
255                (
256                    Constant::LoadGlobal { binding: a, .. },
257                    Constant::LoadGlobal { binding: b, .. },
258                ) => {
259                    // different global values, can't constant propagate
260                    if a.name() != b.name() {
261                        return None;
262                    }
263                }
264                // found different kinds of constants, can't constant propagate
265                (Constant::Primitive { .. }, Constant::LoadGlobal { .. })
266                | (Constant::LoadGlobal { .. }, Constant::Primitive { .. }) => {
267                    return None;
268                }
269            },
270        }
271    }
272    value
273}
274
275// =============================================================================
276// Instruction evaluation
277// =============================================================================
278
279fn evaluate_instruction(
280    constants: &mut Constants,
281    func: &mut HirFunction,
282    env: &mut Environment,
283    instr_id: react_compiler_hir::InstructionId,
284) -> Option<Constant> {
285    let instr = &func.instructions[instr_id.0 as usize];
286    match &instr.value {
287        InstructionValue::Primitive { value, loc } => Some(Constant::Primitive {
288            value: value.clone(),
289            loc: *loc,
290        }),
291        InstructionValue::LoadGlobal { binding, loc } => Some(Constant::LoadGlobal {
292            binding: binding.clone(),
293            loc: *loc,
294        }),
295        InstructionValue::ComputedLoad {
296            object,
297            property,
298            loc,
299        } => {
300            let prop_value = read(constants, property);
301            if let Some(Constant::Primitive {
302                value: ref prim, ..
303            }) = prop_value
304            {
305                match prim {
306                    PrimitiveValue::String(s) if is_valid_identifier(s) => {
307                        let object = object.clone();
308                        let loc = *loc;
309                        let new_property = PropertyLiteral::String(s.clone());
310                        func.instructions[instr_id.0 as usize].value =
311                            InstructionValue::PropertyLoad {
312                                object,
313                                property: new_property,
314                                loc,
315                            };
316                    }
317                    PrimitiveValue::Number(n) => {
318                        let object = object.clone();
319                        let loc = *loc;
320                        let new_property = PropertyLiteral::Number(*n);
321                        func.instructions[instr_id.0 as usize].value =
322                            InstructionValue::PropertyLoad {
323                                object,
324                                property: new_property,
325                                loc,
326                            };
327                    }
328                    PrimitiveValue::Null
329                    | PrimitiveValue::Undefined
330                    | PrimitiveValue::Boolean(_)
331                    | PrimitiveValue::String(_) => {}
332                }
333            }
334            None
335        }
336        InstructionValue::ComputedStore {
337            object,
338            property,
339            value,
340            loc,
341        } => {
342            let prop_value = read(constants, property);
343            if let Some(Constant::Primitive {
344                value: ref prim, ..
345            }) = prop_value
346            {
347                match prim {
348                    PrimitiveValue::String(s) if is_valid_identifier(s) => {
349                        let object = object.clone();
350                        let store_value = value.clone();
351                        let loc = *loc;
352                        let new_property = PropertyLiteral::String(s.clone());
353                        func.instructions[instr_id.0 as usize].value =
354                            InstructionValue::PropertyStore {
355                                object,
356                                property: new_property,
357                                value: store_value,
358                                loc,
359                            };
360                    }
361                    PrimitiveValue::Number(n) => {
362                        let object = object.clone();
363                        let store_value = value.clone();
364                        let loc = *loc;
365                        let new_property = PropertyLiteral::Number(*n);
366                        func.instructions[instr_id.0 as usize].value =
367                            InstructionValue::PropertyStore {
368                                object,
369                                property: new_property,
370                                value: store_value,
371                                loc,
372                            };
373                    }
374                    PrimitiveValue::Null
375                    | PrimitiveValue::Undefined
376                    | PrimitiveValue::Boolean(_)
377                    | PrimitiveValue::String(_) => {}
378                }
379            }
380            None
381        }
382        InstructionValue::PostfixUpdate {
383            lvalue,
384            operation,
385            value,
386            loc,
387        } => {
388            let previous = read(constants, value);
389            if let Some(Constant::Primitive {
390                value: PrimitiveValue::Number(n),
391                loc: prev_loc,
392            }) = previous
393            {
394                let prev_val = n.value();
395                let next_val = match operation {
396                    UpdateOperator::Increment => prev_val + 1.0,
397                    UpdateOperator::Decrement => prev_val - 1.0,
398                };
399                // Store the updated value for the lvalue
400                let lvalue_id = lvalue.identifier;
401                constants.insert(
402                    lvalue_id,
403                    Constant::Primitive {
404                        value: PrimitiveValue::Number(FloatValue::new(next_val)),
405                        loc: *loc,
406                    },
407                );
408                // But return the value prior to the update (preserving its original loc)
409                return Some(Constant::Primitive {
410                    value: PrimitiveValue::Number(n),
411                    loc: prev_loc,
412                });
413            }
414            None
415        }
416        InstructionValue::PrefixUpdate {
417            lvalue,
418            operation,
419            value,
420            loc,
421        } => {
422            let previous = read(constants, value);
423            if let Some(Constant::Primitive {
424                value: PrimitiveValue::Number(n),
425                ..
426            }) = previous
427            {
428                let prev_val = n.value();
429                let next_val = match operation {
430                    UpdateOperator::Increment => prev_val + 1.0,
431                    UpdateOperator::Decrement => prev_val - 1.0,
432                };
433                let result = Constant::Primitive {
434                    value: PrimitiveValue::Number(FloatValue::new(next_val)),
435                    loc: *loc,
436                };
437                // Store and return the updated value
438                let lvalue_id = lvalue.identifier;
439                constants.insert(lvalue_id, result.clone());
440                return Some(result);
441            }
442            None
443        }
444        InstructionValue::UnaryExpression {
445            operator,
446            value,
447            loc,
448        } => match operator {
449            UnaryOperator::Not => {
450                let operand = read(constants, value);
451                if let Some(Constant::Primitive {
452                    value: ref prim, ..
453                }) = operand
454                {
455                    let negated = !is_truthy(prim);
456                    let loc = *loc;
457                    let result = Constant::Primitive {
458                        value: PrimitiveValue::Boolean(negated),
459                        loc,
460                    };
461                    func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {
462                        value: PrimitiveValue::Boolean(negated),
463                        loc,
464                    };
465                    return Some(result);
466                }
467                None
468            }
469            UnaryOperator::Minus => {
470                let operand = read(constants, value);
471                if let Some(Constant::Primitive {
472                    value: PrimitiveValue::Number(n),
473                    ..
474                }) = operand
475                {
476                    let negated = n.value() * -1.0;
477                    let loc = *loc;
478                    let result = Constant::Primitive {
479                        value: PrimitiveValue::Number(FloatValue::new(negated)),
480                        loc,
481                    };
482                    func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {
483                        value: PrimitiveValue::Number(FloatValue::new(negated)),
484                        loc,
485                    };
486                    return Some(result);
487                }
488                None
489            }
490            UnaryOperator::Plus
491            | UnaryOperator::BitwiseNot
492            | UnaryOperator::TypeOf
493            | UnaryOperator::Void => None,
494        },
495        InstructionValue::BinaryExpression {
496            operator,
497            left,
498            right,
499            loc,
500        } => {
501            let lhs_value = read(constants, left);
502            let rhs_value = read(constants, right);
503            if let (
504                Some(Constant::Primitive { value: lhs, .. }),
505                Some(Constant::Primitive { value: rhs, .. }),
506            ) = (&lhs_value, &rhs_value)
507            {
508                let result = evaluate_binary_op(*operator, lhs, rhs);
509                if let Some(ref prim) = result {
510                    let loc = *loc;
511                    func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {
512                        value: prim.clone(),
513                        loc,
514                    };
515                    return Some(Constant::Primitive {
516                        value: prim.clone(),
517                        loc,
518                    });
519                }
520            }
521            None
522        }
523        InstructionValue::PropertyLoad {
524            object,
525            property,
526            loc,
527        } => {
528            let object_value = read(constants, object);
529            if let Some(Constant::Primitive {
530                value: PrimitiveValue::String(ref s),
531                ..
532            }) = object_value
533            {
534                if let PropertyLiteral::String(prop_name) = property {
535                    if prop_name == "length" {
536                        // Use UTF-16 code unit count to match JS .length semantics
537                        let len = s.encode_utf16().count() as f64;
538                        let loc = *loc;
539                        let result = Constant::Primitive {
540                            value: PrimitiveValue::Number(FloatValue::new(len)),
541                            loc,
542                        };
543                        func.instructions[instr_id.0 as usize].value =
544                            InstructionValue::Primitive {
545                                value: PrimitiveValue::Number(FloatValue::new(len)),
546                                loc,
547                            };
548                        return Some(result);
549                    }
550                }
551            }
552            None
553        }
554        InstructionValue::TemplateLiteral {
555            subexprs,
556            quasis,
557            loc,
558        } => {
559            if subexprs.is_empty() {
560                // No subexpressions: join all cooked quasis
561                let mut result_string = String::new();
562                for q in quasis {
563                    match &q.cooked {
564                        Some(cooked) => result_string.push_str(cooked),
565                        None => return None,
566                    }
567                }
568                let loc = *loc;
569                let result = Constant::Primitive {
570                    value: PrimitiveValue::String(result_string.clone()),
571                    loc,
572                };
573                func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {
574                    value: PrimitiveValue::String(result_string),
575                    loc,
576                };
577                return Some(result);
578            }
579
580            if subexprs.len() != quasis.len() - 1 {
581                return None;
582            }
583
584            if quasis.iter().any(|q| q.cooked.is_none()) {
585                return None;
586            }
587
588            let mut quasi_index = 0usize;
589            let mut result_string = quasis[quasi_index].cooked.as_ref().unwrap().clone();
590            quasi_index += 1;
591
592            for sub_expr in subexprs {
593                let sub_expr_value = read(constants, sub_expr);
594                let sub_prim = match sub_expr_value {
595                    Some(Constant::Primitive { ref value, .. }) => value,
596                    _ => return None,
597                };
598
599                let expression_str = match sub_prim {
600                    PrimitiveValue::Null => "null".to_string(),
601                    PrimitiveValue::Boolean(b) => b.to_string(),
602                    PrimitiveValue::Number(n) => format_js_number(n.value()),
603                    PrimitiveValue::String(s) => s.clone(),
604                    // TS rejects undefined subexpression values
605                    PrimitiveValue::Undefined => return None,
606                };
607
608                let suffix = match &quasis[quasi_index].cooked {
609                    Some(s) => s.clone(),
610                    None => return None,
611                };
612                quasi_index += 1;
613
614                result_string.push_str(&expression_str);
615                result_string.push_str(&suffix);
616            }
617
618            let loc = *loc;
619            let result = Constant::Primitive {
620                value: PrimitiveValue::String(result_string.clone()),
621                loc,
622            };
623            func.instructions[instr_id.0 as usize].value = InstructionValue::Primitive {
624                value: PrimitiveValue::String(result_string),
625                loc,
626            };
627            Some(result)
628        }
629        InstructionValue::LoadLocal { place, .. } => {
630            let place_value = read(constants, place);
631            if let Some(ref constant) = place_value {
632                // Replace the LoadLocal with the constant value (including the constant's original loc)
633                func.instructions[instr_id.0 as usize].value =
634                    constant.clone().into_instruction_value();
635            }
636            place_value
637        }
638        InstructionValue::StoreLocal { lvalue, value, .. } => {
639            let place_value = read(constants, value);
640            if let Some(ref constant) = place_value {
641                let lvalue_id = lvalue.place.identifier;
642                constants.insert(lvalue_id, constant.clone());
643            }
644            place_value
645        }
646        InstructionValue::FunctionExpression {
647            lowered_func, ..
648        } => {
649            let func_id = lowered_func.func;
650            process_inner_function(func_id, env, constants);
651            None
652        }
653        InstructionValue::ObjectMethod {
654            lowered_func, ..
655        } => {
656            let func_id = lowered_func.func;
657            process_inner_function(func_id, env, constants);
658            None
659        }
660        InstructionValue::StartMemoize { deps, .. } => {
661            if let Some(deps) = deps {
662                // Two-phase: collect which deps are constant, then mutate
663                let const_dep_indices: Vec<usize> = deps
664                    .iter()
665                    .enumerate()
666                    .filter_map(|(i, dep)| {
667                        if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal {
668                            value,
669                            ..
670                        } = &dep.root
671                        {
672                            let pv = read(constants, value);
673                            if matches!(pv, Some(Constant::Primitive { .. })) {
674                                return Some(i);
675                            }
676                        }
677                        None
678                    })
679                    .collect();
680                for idx in const_dep_indices {
681                    if let InstructionValue::StartMemoize {
682                        deps: Some(ref mut deps),
683                        ..
684                    } = func.instructions[instr_id.0 as usize].value
685                    {
686                        if let react_compiler_hir::ManualMemoDependencyRoot::NamedLocal {
687                            constant,
688                            ..
689                        } = &mut deps[idx].root
690                        {
691                            *constant = true;
692                        }
693                    }
694                }
695            }
696            None
697        }
698        // All other instruction kinds: no constant folding
699        InstructionValue::LoadContext { .. }
700        | InstructionValue::DeclareLocal { .. }
701        | InstructionValue::DeclareContext { .. }
702        | InstructionValue::StoreContext { .. }
703        | InstructionValue::Destructure { .. }
704        | InstructionValue::JSXText { .. }
705        | InstructionValue::NewExpression { .. }
706        | InstructionValue::CallExpression { .. }
707        | InstructionValue::MethodCall { .. }
708        | InstructionValue::TypeCastExpression { .. }
709        | InstructionValue::JsxExpression { .. }
710        | InstructionValue::ObjectExpression { .. }
711        | InstructionValue::ArrayExpression { .. }
712        | InstructionValue::JsxFragment { .. }
713        | InstructionValue::RegExpLiteral { .. }
714        | InstructionValue::MetaProperty { .. }
715        | InstructionValue::PropertyStore { .. }
716        | InstructionValue::PropertyDelete { .. }
717        | InstructionValue::ComputedDelete { .. }
718        | InstructionValue::StoreGlobal { .. }
719        | InstructionValue::TaggedTemplateExpression { .. }
720        | InstructionValue::Await { .. }
721        | InstructionValue::GetIterator { .. }
722        | InstructionValue::IteratorNext { .. }
723        | InstructionValue::NextPropertyOf { .. }
724        | InstructionValue::Debugger { .. }
725        | InstructionValue::FinishMemoize { .. }
726        | InstructionValue::UnsupportedNode { .. } => None,
727    }
728}
729
730// =============================================================================
731// Inner function processing
732// =============================================================================
733
734fn process_inner_function(func_id: FunctionId, env: &mut Environment, constants: &mut Constants) {
735    let mut inner = std::mem::replace(
736        &mut env.functions[func_id.0 as usize],
737        placeholder_function(),
738    );
739    constant_propagation_impl(&mut inner, env, constants);
740    env.functions[func_id.0 as usize] = inner;
741}
742
743// =============================================================================
744// Helper: read constant for a place
745// =============================================================================
746
747fn read(constants: &Constants, place: &Place) -> Option<Constant> {
748    constants.get(&place.identifier).cloned()
749}
750
751// =============================================================================
752// Helper: is_valid_identifier
753// =============================================================================
754
755/// Check if a string is a valid JavaScript identifier.
756/// Supports Unicode identifier characters per ECMAScript spec (ID_Start / ID_Continue).
757/// Rejects JS reserved words (matching Babel's `isValidIdentifier` default behavior).
758fn is_valid_identifier(s: &str) -> bool {
759    if s.is_empty() {
760        return false;
761    }
762    let mut chars = s.chars();
763    match chars.next() {
764        Some(c) if is_id_start(c) => {}
765        _ => return false,
766    }
767    if !chars.all(is_id_continue) {
768        return false;
769    }
770    !is_reserved_word(s)
771}
772
773/// JS reserved words that cannot be used as identifiers.
774/// Includes keywords, future reserved words, and strict mode reserved words.
775fn is_reserved_word(s: &str) -> bool {
776    matches!(
777        s,
778        "break"
779            | "case"
780            | "catch"
781            | "continue"
782            | "debugger"
783            | "default"
784            | "do"
785            | "else"
786            | "finally"
787            | "for"
788            | "function"
789            | "if"
790            | "in"
791            | "instanceof"
792            | "new"
793            | "return"
794            | "switch"
795            | "this"
796            | "throw"
797            | "try"
798            | "typeof"
799            | "var"
800            | "void"
801            | "while"
802            | "with"
803            | "class"
804            | "const"
805            | "enum"
806            | "export"
807            | "extends"
808            | "import"
809            | "super"
810            | "implements"
811            | "interface"
812            | "let"
813            | "package"
814            | "private"
815            | "protected"
816            | "public"
817            | "static"
818            | "yield"
819            | "await"
820            | "delete"
821            | "null"
822            | "true"
823            | "false"
824    )
825}
826
827/// Check if a character is valid as the start of a JS identifier (ID_Start + _ + $).
828fn is_id_start(c: char) -> bool {
829    c == '_' || c == '$' || c.is_alphabetic()
830}
831
832/// Check if a character is valid as a continuation of a JS identifier (ID_Continue + $ + \u200C + \u200D).
833fn is_id_continue(c: char) -> bool {
834    c == '$'
835        || c == '_'
836        || c.is_alphanumeric()
837        || c == '\u{200C}' // ZWNJ
838        || c == '\u{200D}' // ZWJ
839}
840
841// =============================================================================
842// Helper: is_truthy for PrimitiveValue
843// =============================================================================
844
845fn is_truthy(value: &PrimitiveValue) -> bool {
846    match value {
847        PrimitiveValue::Null => false,
848        PrimitiveValue::Undefined => false,
849        PrimitiveValue::Boolean(b) => *b,
850        PrimitiveValue::Number(n) => {
851            let v = n.value();
852            v != 0.0 && !v.is_nan()
853        }
854        PrimitiveValue::String(s) => !s.is_empty(),
855    }
856}
857
858// =============================================================================
859// Binary operation evaluation
860// =============================================================================
861
862fn evaluate_binary_op(
863    operator: BinaryOperator,
864    lhs: &PrimitiveValue,
865    rhs: &PrimitiveValue,
866) -> Option<PrimitiveValue> {
867    match operator {
868        BinaryOperator::Add => match (lhs, rhs) {
869            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
870                Some(PrimitiveValue::Number(FloatValue::new(l.value() + r.value())))
871            }
872            (PrimitiveValue::String(l), PrimitiveValue::String(r)) => {
873                let mut s = l.clone();
874                s.push_str(r);
875                Some(PrimitiveValue::String(s))
876            }
877            _ => None,
878        },
879        BinaryOperator::Subtract => match (lhs, rhs) {
880            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
881                Some(PrimitiveValue::Number(FloatValue::new(l.value() - r.value())))
882            }
883            _ => None,
884        },
885        BinaryOperator::Multiply => match (lhs, rhs) {
886            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
887                Some(PrimitiveValue::Number(FloatValue::new(l.value() * r.value())))
888            }
889            _ => None,
890        },
891        BinaryOperator::Divide => match (lhs, rhs) {
892            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
893                Some(PrimitiveValue::Number(FloatValue::new(l.value() / r.value())))
894            }
895            _ => None,
896        },
897        BinaryOperator::Modulo => match (lhs, rhs) {
898            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
899                Some(PrimitiveValue::Number(FloatValue::new(l.value() % r.value())))
900            }
901            _ => None,
902        },
903        BinaryOperator::Exponent => match (lhs, rhs) {
904            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => Some(
905                PrimitiveValue::Number(FloatValue::new(l.value().powf(r.value()))),
906            ),
907            _ => None,
908        },
909        BinaryOperator::BitwiseOr => match (lhs, rhs) {
910            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
911                let result = js_to_int32(l.value()) | js_to_int32(r.value());
912                Some(PrimitiveValue::Number(FloatValue::new(result as f64)))
913            }
914            _ => None,
915        },
916        BinaryOperator::BitwiseAnd => match (lhs, rhs) {
917            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
918                let result = js_to_int32(l.value()) & js_to_int32(r.value());
919                Some(PrimitiveValue::Number(FloatValue::new(result as f64)))
920            }
921            _ => None,
922        },
923        BinaryOperator::BitwiseXor => match (lhs, rhs) {
924            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
925                let result = js_to_int32(l.value()) ^ js_to_int32(r.value());
926                Some(PrimitiveValue::Number(FloatValue::new(result as f64)))
927            }
928            _ => None,
929        },
930        BinaryOperator::ShiftLeft => match (lhs, rhs) {
931            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
932                let result = js_to_int32(l.value()) << (js_to_uint32(r.value()) & 0x1f);
933                Some(PrimitiveValue::Number(FloatValue::new(result as f64)))
934            }
935            _ => None,
936        },
937        BinaryOperator::ShiftRight => match (lhs, rhs) {
938            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
939                let result = js_to_int32(l.value()) >> (js_to_uint32(r.value()) & 0x1f);
940                Some(PrimitiveValue::Number(FloatValue::new(result as f64)))
941            }
942            _ => None,
943        },
944        BinaryOperator::UnsignedShiftRight => match (lhs, rhs) {
945            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
946                let result = js_to_uint32(l.value()) >> (js_to_uint32(r.value()) & 0x1f);
947                Some(PrimitiveValue::Number(FloatValue::new(result as f64)))
948            }
949            _ => None,
950        },
951        BinaryOperator::LessThan => match (lhs, rhs) {
952            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
953                Some(PrimitiveValue::Boolean(l.value() < r.value()))
954            }
955            _ => None,
956        },
957        BinaryOperator::LessEqual => match (lhs, rhs) {
958            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
959                Some(PrimitiveValue::Boolean(l.value() <= r.value()))
960            }
961            _ => None,
962        },
963        BinaryOperator::GreaterThan => match (lhs, rhs) {
964            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
965                Some(PrimitiveValue::Boolean(l.value() > r.value()))
966            }
967            _ => None,
968        },
969        BinaryOperator::GreaterEqual => match (lhs, rhs) {
970            (PrimitiveValue::Number(l), PrimitiveValue::Number(r)) => {
971                Some(PrimitiveValue::Boolean(l.value() >= r.value()))
972            }
973            _ => None,
974        },
975        BinaryOperator::StrictEqual => Some(PrimitiveValue::Boolean(js_strict_equal(lhs, rhs))),
976        BinaryOperator::StrictNotEqual => {
977            Some(PrimitiveValue::Boolean(!js_strict_equal(lhs, rhs)))
978        }
979        BinaryOperator::Equal => Some(PrimitiveValue::Boolean(js_abstract_equal(lhs, rhs))),
980        BinaryOperator::NotEqual => Some(PrimitiveValue::Boolean(!js_abstract_equal(lhs, rhs))),
981        BinaryOperator::In | BinaryOperator::InstanceOf => None,
982    }
983}
984
985// =============================================================================
986// JavaScript equality semantics
987// =============================================================================
988
989fn js_strict_equal(lhs: &PrimitiveValue, rhs: &PrimitiveValue) -> bool {
990    match (lhs, rhs) {
991        (PrimitiveValue::Null, PrimitiveValue::Null) => true,
992        (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true,
993        (PrimitiveValue::Boolean(a), PrimitiveValue::Boolean(b)) => a == b,
994        (PrimitiveValue::Number(a), PrimitiveValue::Number(b)) => {
995            let av = a.value();
996            let bv = b.value();
997            // NaN !== NaN in JS
998            if av.is_nan() || bv.is_nan() {
999                return false;
1000            }
1001            av == bv
1002        }
1003        (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b,
1004        // Different types => false
1005        _ => false,
1006    }
1007}
1008
1009/// Convert a string to a number using JS `ToNumber` semantics.
1010/// In JS: `""` → 0, `" "` → 0, `" 42 "` → 42, `"0x1A"` → 26, `"Infinity"` → Infinity.
1011fn js_to_number(s: &str) -> f64 {
1012    let trimmed = s.trim();
1013    if trimmed.is_empty() {
1014        return 0.0;
1015    }
1016    if trimmed == "Infinity" || trimmed == "+Infinity" {
1017        return f64::INFINITY;
1018    }
1019    if trimmed == "-Infinity" {
1020        return f64::NEG_INFINITY;
1021    }
1022    // Handle hex literals (0x/0X)
1023    if trimmed.starts_with("0x") || trimmed.starts_with("0X") {
1024        return match u64::from_str_radix(&trimmed[2..], 16) {
1025            Ok(v) => v as f64,
1026            Err(_) => f64::NAN,
1027        };
1028    }
1029    // Handle octal literals (0o/0O)
1030    if trimmed.starts_with("0o") || trimmed.starts_with("0O") {
1031        return match u64::from_str_radix(&trimmed[2..], 8) {
1032            Ok(v) => v as f64,
1033            Err(_) => f64::NAN,
1034        };
1035    }
1036    // Handle binary literals (0b/0B)
1037    if trimmed.starts_with("0b") || trimmed.starts_with("0B") {
1038        return match u64::from_str_radix(&trimmed[2..], 2) {
1039            Ok(v) => v as f64,
1040            Err(_) => f64::NAN,
1041        };
1042    }
1043    trimmed.parse::<f64>().unwrap_or(f64::NAN)
1044}
1045
1046fn js_abstract_equal(lhs: &PrimitiveValue, rhs: &PrimitiveValue) -> bool {
1047    match (lhs, rhs) {
1048        (PrimitiveValue::Null, PrimitiveValue::Null) => true,
1049        (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true,
1050        (PrimitiveValue::Null, PrimitiveValue::Undefined)
1051        | (PrimitiveValue::Undefined, PrimitiveValue::Null) => true,
1052        (PrimitiveValue::Boolean(a), PrimitiveValue::Boolean(b)) => a == b,
1053        (PrimitiveValue::Number(a), PrimitiveValue::Number(b)) => {
1054            let av = a.value();
1055            let bv = b.value();
1056            if av.is_nan() || bv.is_nan() {
1057                return false;
1058            }
1059            av == bv
1060        }
1061        (PrimitiveValue::String(a), PrimitiveValue::String(b)) => a == b,
1062        // Cross-type coercions for primitives
1063        (PrimitiveValue::Number(n), PrimitiveValue::String(s))
1064        | (PrimitiveValue::String(s), PrimitiveValue::Number(n)) => {
1065            // String is coerced to number using JS ToNumber semantics
1066            let sv = js_to_number(s);
1067            let nv = n.value();
1068            if nv.is_nan() || sv.is_nan() {
1069                false
1070            } else {
1071                nv == sv
1072            }
1073        }
1074        (PrimitiveValue::Boolean(b), other) => {
1075            let num = if *b { 1.0 } else { 0.0 };
1076            js_abstract_equal(&PrimitiveValue::Number(FloatValue::new(num)), other)
1077        }
1078        (other, PrimitiveValue::Boolean(b)) => {
1079            let num = if *b { 1.0 } else { 0.0 };
1080            js_abstract_equal(other, &PrimitiveValue::Number(FloatValue::new(num)))
1081        }
1082        // null/undefined vs number/string => false
1083        _ => false,
1084    }
1085}
1086
1087// =============================================================================
1088// JavaScript Number.toString() approximation
1089// =============================================================================
1090
1091/// ECMAScript ToInt32: convert f64 to i32 with modular (wrapping) semantics.
1092fn js_to_int32(n: f64) -> i32 {
1093    if n.is_nan() || n.is_infinite() || n == 0.0 {
1094        return 0;
1095    }
1096    // Truncate, then wrap to 32 bits
1097    let int64 = (n.trunc() as i64) & 0xFFFFFFFF;
1098    // Reinterpret as signed i32
1099    if int64 >= 0x80000000 {
1100        (int64 as u32) as i32
1101    } else {
1102        int64 as i32
1103    }
1104}
1105
1106/// ECMAScript ToUint32: convert f64 to u32 with modular (wrapping) semantics.
1107fn js_to_uint32(n: f64) -> u32 {
1108    js_to_int32(n) as u32
1109}