diff --git a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs index 5c59693612838..18c1e9fc5e648 100644 --- a/crates/oxc_ecmascript/src/constant_evaluation/mod.rs +++ b/crates/oxc_ecmascript/src/constant_evaluation/mod.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, cmp::Ordering}; use num_bigint::BigInt; -use num_traits::{ToPrimitive, Zero}; +use num_traits::{FromPrimitive, ToPrimitive, Zero}; use equality_comparison::{abstract_equality_comparison, strict_equality_comparison}; use oxc_ast::{ast::*, AstBuilder}; @@ -539,40 +539,144 @@ pub trait ConstantEvaluation<'a>: MayHaveSideEffects { } } - /// + /// fn is_less_than(&self, x: &Expression<'a>, y: &Expression<'a>) -> Option> { - // a. Let px be ? ToPrimitive(x, number). - // b. Let py be ? ToPrimitive(y, number). - let px = ValueType::from(x); - - // `.toString()` method is called and compared. - // TODO: bigint is handled very differently in the spec - if px.is_object() || px.is_undetermined() || px.is_bigint() { + if self.expression_may_have_side_effects(x) || self.expression_may_have_side_effects(y) { return None; } + // a. Let px be ? ToPrimitive(x, NUMBER). + // b. Let py be ? ToPrimitive(y, NUMBER). + let px = ValueType::from(x); let py = ValueType::from(y); - if py.is_object() || py.is_undetermined() || py.is_bigint() { + // If the operands are not primitives, `ToPrimitive` is *not* a noop. + if px.is_undetermined() || px.is_object() || py.is_undetermined() || py.is_object() { return None; } + // 3. If px is a String and py is a String, then if px.is_string() && py.is_string() { - let left_string = self.get_side_free_string_value(x)?; - let right_string = self.get_side_free_string_value(y)?; + let left_string = x.to_js_string()?; + let right_string = y.to_js_string()?; return Some(ConstantValue::Boolean( left_string.encode_utf16().cmp(right_string.encode_utf16()) == Ordering::Less, )); } - let left_num = self.get_side_free_number_value(x)?; - if left_num.is_nan() { - return Some(ConstantValue::Undefined); + // a. If px is a BigInt and py is a String, then + if px.is_bigint() && py.is_string() { + use crate::StringToBigInt; + let ny = y.to_js_string()?.as_ref().string_to_big_int(); + let Some(ny) = ny else { return Some(ConstantValue::Undefined) }; + return Some(ConstantValue::Boolean(x.to_big_int()? < ny)); + } + // b. If px is a String and py is a BigInt, then + if px.is_string() && py.is_bigint() { + use crate::StringToBigInt; + let nx = x.to_js_string()?.as_ref().string_to_big_int(); + let Some(nx) = nx else { return Some(ConstantValue::Undefined) }; + return Some(ConstantValue::Boolean(nx < y.to_big_int()?)); + } + + // Both operands are primitives here. + // ToNumeric returns a BigInt if the operand is a BigInt. Otherwise, it returns a Number. + let nx_is_number = !px.is_bigint(); + let ny_is_number = !py.is_bigint(); + + // f. If SameType(nx, ny) is true, then + // i. If nx is a Number, then + if nx_is_number && ny_is_number { + let left_num = self.eval_to_number(x)?; + if left_num.is_nan() { + return Some(ConstantValue::Undefined); + } + let right_num = self.eval_to_number(y)?; + if right_num.is_nan() { + return Some(ConstantValue::Undefined); + } + return Some(ConstantValue::Boolean(left_num < right_num)); } - let right_num = self.get_side_free_number_value(y)?; - if right_num.is_nan() { + // ii. Else, + if px.is_bigint() && py.is_bigint() { + return Some(ConstantValue::Boolean(x.to_big_int()? < y.to_big_int()?)); + } + + let nx = self.eval_to_number(x); + let ny = self.eval_to_number(y); + + // h. If nx or ny is NaN, return undefined. + if nx_is_number && nx.is_some_and(f64::is_nan) + || ny_is_number && ny.is_some_and(f64::is_nan) + { return Some(ConstantValue::Undefined); } - Some(ConstantValue::Boolean(left_num < right_num)) + + // i. If nx is -∞𝔽 or ny is +∞𝔽, return true. + if nx_is_number && nx.is_some_and(|n| n == f64::NEG_INFINITY) + || ny_is_number && ny.is_some_and(|n| n == f64::INFINITY) + { + return Some(ConstantValue::Boolean(true)); + } + // j. If nx is +∞𝔽 or ny is -∞𝔽, return false. + if nx_is_number && nx.is_some_and(|n| n == f64::INFINITY) + || ny_is_number && ny.is_some_and(|n| n == f64::NEG_INFINITY) + { + return Some(ConstantValue::Boolean(false)); + } + + // k. If ℝ(nx) < ℝ(ny), return true; otherwise return false. + if px.is_bigint() { + let nx = x.to_big_int()?; + let ny = self.eval_to_number(y)?; + return compare_bigint_and_f64(&nx, ny) + .map(|ord| ConstantValue::Boolean(ord == Ordering::Less)); + } + if py.is_bigint() { + let ny = y.to_big_int()?; + let nx = self.eval_to_number(x)?; + return compare_bigint_and_f64(&ny, nx) + .map(|ord| ConstantValue::Boolean(ord.reverse() == Ordering::Less)); + } + + None + } +} + +fn compare_bigint_and_f64(x: &BigInt, y: f64) -> Option { + let ny = BigInt::from_f64(y)?; + + let raw_ord = x.cmp(&ny); + if raw_ord == Ordering::Equal { + let fract_ord = 0.0f64.partial_cmp(&y.fract()).expect("both should be finite"); + Some(fract_ord) + } else { + Some(raw_ord) + } +} + +#[cfg(test)] +mod test { + use super::compare_bigint_and_f64; + use num_bigint::BigInt; + use std::cmp::Ordering; + + #[test] + fn test_compare_bigint_and_f64() { + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), f64::NAN), None); + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), f64::INFINITY), None); + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), f64::NEG_INFINITY), None); + + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), 0.0), Some(Ordering::Greater)); + assert_eq!(compare_bigint_and_f64(&BigInt::from(0), 0.0), Some(Ordering::Equal)); + assert_eq!(compare_bigint_and_f64(&BigInt::from(-1), 0.0), Some(Ordering::Less)); + + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), 0.9), Some(Ordering::Greater)); + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), 1.0), Some(Ordering::Equal)); + assert_eq!(compare_bigint_and_f64(&BigInt::from(1), 1.1), Some(Ordering::Less)); + + assert_eq!(compare_bigint_and_f64(&BigInt::from(-1), -1.1), Some(Ordering::Greater)); + assert_eq!(compare_bigint_and_f64(&BigInt::from(-1), -1.0), Some(Ordering::Equal)); + assert_eq!(compare_bigint_and_f64(&BigInt::from(-1), -0.9), Some(Ordering::Less)); } } diff --git a/crates/oxc_minifier/src/peephole/fold_constants.rs b/crates/oxc_minifier/src/peephole/fold_constants.rs index 8750020538b42..c91a651eeeac1 100644 --- a/crates/oxc_minifier/src/peephole/fold_constants.rs +++ b/crates/oxc_minifier/src/peephole/fold_constants.rs @@ -675,8 +675,8 @@ mod test { fold("null == 0", "!1"); fold("null == 1", "!1"); - // test("null == 0n", "!1"); - // test("null == 1n", "!1"); + fold("null == 0n", "!1"); + fold("null == 1n", "!1"); fold("null == 'hi'", "!1"); fold("null == true", "!1"); fold("null == false", "!1"); @@ -695,8 +695,8 @@ mod test { fold("null != 0", "!0"); fold("null != 1", "!0"); - // test("null != 0n", "!0"); - // test("null != 1n", "!0"); + fold("null != 0n", "!0"); + fold("null != 1n", "!0"); fold("null != 'hi'", "!0"); fold("null != true", "!0"); fold("null != false", "!0"); @@ -716,16 +716,16 @@ mod test { fold("0 < null", "!1"); fold("0 > null", "!1"); fold("0 >= null", "!0"); - // test("0n < null", "!1"); - // test("0n > null", "!1"); - // test("0n >= null", "!0"); + fold("0n < null", "!1"); + fold("0n > null", "!1"); + fold("0n >= null", "!0"); fold("true > null", "!0"); fold("'hi' < null", "!1"); fold("'hi' >= null", "!1"); fold("null <= null", "!0"); fold("null < 0", "!1"); - // test("null < 0n", "!1"); + fold("null < 0n", "!1"); fold("null > true", "!1"); fold("null < 'hi'", "!1"); fold("null >= 'hi'", "!1"); @@ -897,10 +897,10 @@ mod test { fold("NaN <= 1", "!1"); fold("NaN > 1", "!1"); fold("NaN >= 1", "!1"); - // test("NaN < 1n", "!1"); - // test("NaN <= 1n", "!1"); - // test("NaN > 1n", "!1"); - // test("NaN >= 1n", "!1"); + fold("NaN < 1n", "!1"); + fold("NaN <= 1n", "!1"); + fold("NaN > 1n", "!1"); + fold("NaN >= 1n", "!1"); fold("NaN < NaN", "!1"); fold("NaN >= NaN", "!1"); @@ -1713,7 +1713,6 @@ mod test { } #[test] - #[ignore] fn test_bigint_number_comparison() { fold("1n < 2", "!0"); fold("1n > 2", "!1"); @@ -1754,7 +1753,6 @@ mod test { } #[test] - #[ignore] fn test_bigint_string_comparison() { fold("1n < '2'", "!0"); fold("2n > '1'", "!0"); @@ -1767,7 +1765,6 @@ mod test { } #[test] - #[ignore] fn test_string_bigint_comparison() { fold("'1' < 2n", "!0"); fold("'2' > 1n", "!0");