Skip to content

Commit 33c0e9f

Browse files
authored
feat(ecmascript): add global isNaN, isFinite, parseFloat, parseInt functions support to constant evaluation (#12954)
1 parent e2bda36 commit 33c0e9f

File tree

4 files changed

+359
-33
lines changed

4 files changed

+359
-33
lines changed

crates/oxc_ecmascript/src/constant_evaluation/call_expr.rs

Lines changed: 264 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::{
2525

2626
use super::{ConstantEvaluation, ConstantEvaluationCtx, ConstantValue};
2727

28-
fn try_fold_url_related_function<'a>(
28+
fn try_fold_global_functions<'a>(
2929
ident: &IdentifierReference<'a>,
3030
arguments: &Vec<'a, Argument<'a>>,
3131
ctx: &impl ConstantEvaluationCtx<'a>,
@@ -39,6 +39,12 @@ fn try_fold_url_related_function<'a>(
3939
"decodeURIComponent" if ctx.is_global_reference(ident) => {
4040
try_fold_decode_uri_component(arguments, ctx)
4141
}
42+
"isNaN" if ctx.is_global_reference(ident) => try_fold_global_is_nan(arguments, ctx),
43+
"isFinite" if ctx.is_global_reference(ident) => try_fold_global_is_finite(arguments, ctx),
44+
"parseFloat" if ctx.is_global_reference(ident) => {
45+
try_fold_global_parse_float(arguments, ctx)
46+
}
47+
"parseInt" if ctx.is_global_reference(ident) => try_fold_global_parse_int(arguments, ctx),
4248
_ => None,
4349
}
4450
}
@@ -49,7 +55,7 @@ pub fn try_fold_known_global_methods<'a>(
4955
ctx: &impl ConstantEvaluationCtx<'a>,
5056
) -> Option<ConstantValue<'a>> {
5157
if let Expression::Identifier(ident) = callee {
52-
if let Some(result) = try_fold_url_related_function(ident, arguments, ctx) {
58+
if let Some(result) = try_fold_global_functions(ident, arguments, ctx) {
5359
return Some(result);
5460
}
5561
return None;
@@ -617,3 +623,259 @@ fn try_fold_decode_uri_component<'a>(
617623
)?;
618624
Some(ConstantValue::String(decoded))
619625
}
626+
627+
fn try_fold_global_is_nan<'a>(
628+
args: &Vec<'a, Argument<'a>>,
629+
ctx: &impl ConstantEvaluationCtx<'a>,
630+
) -> Option<ConstantValue<'a>> {
631+
if args.is_empty() {
632+
return Some(ConstantValue::Boolean(true));
633+
}
634+
if args.len() != 1 {
635+
return None;
636+
}
637+
let arg = args.first().unwrap();
638+
let expr = arg.as_expression()?;
639+
let num = expr.get_side_free_number_value(ctx)?;
640+
Some(ConstantValue::Boolean(num.is_nan()))
641+
}
642+
643+
fn try_fold_global_is_finite<'a>(
644+
args: &Vec<'a, Argument<'a>>,
645+
ctx: &impl ConstantEvaluationCtx<'a>,
646+
) -> Option<ConstantValue<'a>> {
647+
if args.is_empty() {
648+
return Some(ConstantValue::Boolean(false));
649+
}
650+
if args.len() != 1 {
651+
return None;
652+
}
653+
let arg = args.first().unwrap();
654+
let expr = arg.as_expression()?;
655+
let num = expr.get_side_free_number_value(ctx)?;
656+
Some(ConstantValue::Boolean(num.is_finite()))
657+
}
658+
659+
fn try_fold_global_parse_float<'a>(
660+
args: &Vec<'a, Argument<'a>>,
661+
ctx: &impl ConstantEvaluationCtx<'a>,
662+
) -> Option<ConstantValue<'a>> {
663+
if args.is_empty() {
664+
return Some(ConstantValue::Number(f64::NAN));
665+
}
666+
if args.len() != 1 {
667+
return None;
668+
}
669+
let arg = args.first().unwrap();
670+
let expr = arg.as_expression()?;
671+
let input_string = expr.get_side_free_string_value(ctx)?;
672+
let trimmed = input_string.trim_start();
673+
let Some(trimmed_prefix) = find_str_decimal_literal_prefix(trimmed) else {
674+
return Some(ConstantValue::Number(f64::NAN));
675+
};
676+
677+
let parsed = trimmed_prefix.cow_replace('_', "").parse::<f64>().unwrap_or_else(|_| {
678+
unreachable!(
679+
"StrDecimalLiteral should be parse-able with Rust FromStr for f64: {trimmed_prefix}"
680+
)
681+
});
682+
Some(ConstantValue::Number(parsed))
683+
}
684+
685+
/// Find the longest prefix of a string that satisfies the syntax of a `StrDecimalLiteral`.
686+
/// Returns None when not found.
687+
///
688+
/// This function implements step 4 of `parseFloat`.
689+
/// <https://tc39.es/ecma262/2025/multipage/global-object.html#sec-parsefloat-string>
690+
fn find_str_decimal_literal_prefix(input: &str) -> Option<&str> {
691+
fn match_decimal_digits(s: &str) -> Option<usize> {
692+
let bytes = s.as_bytes();
693+
if bytes.first().is_none_or(|b| !b.is_ascii_digit()) {
694+
// must have at least one digit
695+
return None;
696+
}
697+
let mut iter = bytes.iter().enumerate().skip(1);
698+
while let Some((i, &b)) = iter.next() {
699+
match b {
700+
b'0'..=b'9' => {}
701+
b'_' => {
702+
let Some((i, &b)) = iter.next() else {
703+
// must have at least one digit after _
704+
return Some(i); // without _
705+
};
706+
if !b.is_ascii_digit() {
707+
// must have at least one digit after _
708+
return Some(i); // without _
709+
}
710+
}
711+
_ => return Some(i),
712+
}
713+
}
714+
Some(s.len())
715+
}
716+
fn match_exponent_part(mut s: &str) -> Option<usize> {
717+
if !s.starts_with(['e', 'E']) {
718+
return None;
719+
}
720+
let mut last_index = 1;
721+
s = &s[1..];
722+
if s.starts_with(['+', '-']) {
723+
last_index += 1;
724+
s = &s[1..];
725+
}
726+
let end_of_decimal_digits = match_decimal_digits(s)?;
727+
last_index += end_of_decimal_digits;
728+
Some(last_index)
729+
}
730+
731+
let mut s = input;
732+
let mut last_index: usize = 0;
733+
if s.starts_with(['+', '-']) {
734+
s = &s[1..];
735+
last_index += 1;
736+
}
737+
if s.starts_with("Infinity") {
738+
last_index += "Infinity".len();
739+
return Some(&input[..last_index]);
740+
}
741+
// . DecimalDigits ExponentPart
742+
if s.starts_with('.') {
743+
last_index += 1;
744+
s = &s[1..];
745+
let end_of_decimal_digits = match_decimal_digits(s)?;
746+
last_index += end_of_decimal_digits;
747+
s = &s[end_of_decimal_digits..];
748+
let Some(end_of_exponent_part) = match_exponent_part(s) else {
749+
return Some(&input[..last_index]);
750+
};
751+
last_index += end_of_exponent_part;
752+
return Some(&input[..last_index]);
753+
}
754+
755+
let end_of_decimal_digits = match_decimal_digits(s)?;
756+
last_index += end_of_decimal_digits;
757+
s = &s[end_of_decimal_digits..];
758+
759+
// DecimalDigits . DecimalDigits ExponentPart
760+
if s.starts_with('.') {
761+
last_index += 1;
762+
s = &s[1..];
763+
let Some(end_of_decimal_digits) = match_decimal_digits(s) else {
764+
return Some(&input[..last_index - 1]); // without .
765+
};
766+
last_index += end_of_decimal_digits;
767+
s = &s[end_of_decimal_digits..];
768+
let Some(end_of_exponent_part) = match_exponent_part(s) else {
769+
return Some(&input[..last_index]);
770+
};
771+
last_index += end_of_exponent_part;
772+
return Some(&input[..last_index]);
773+
}
774+
775+
// DecimalDigits ExponentPart
776+
let Some(end_of_exponent_part) = match_exponent_part(s) else {
777+
return Some(&input[..last_index]);
778+
};
779+
last_index += end_of_exponent_part;
780+
Some(&input[..last_index])
781+
}
782+
783+
fn try_fold_global_parse_int<'a>(
784+
args: &Vec<'a, Argument<'a>>,
785+
ctx: &impl ConstantEvaluationCtx<'a>,
786+
) -> Option<ConstantValue<'a>> {
787+
if args.is_empty() {
788+
return Some(ConstantValue::Number(f64::NAN));
789+
}
790+
if args.len() > 2
791+
|| args
792+
.iter()
793+
.any(|arg| arg.as_expression().is_none_or(|arg| arg.may_have_side_effects(ctx)))
794+
{
795+
return None;
796+
}
797+
let string_arg = args.first().unwrap();
798+
let string_expr = string_arg.as_expression()?;
799+
let string_value = string_expr.evaluate_value_to_string(ctx)?;
800+
let mut string_value = string_value.trim_start();
801+
802+
let mut sign = 1;
803+
if string_value.starts_with('-') {
804+
sign = -1;
805+
}
806+
if string_value.starts_with(['+', '-']) {
807+
string_value = &string_value[1..];
808+
}
809+
810+
let mut strip_prefix = true;
811+
let mut radix = if let Some(arg) = args.get(1) {
812+
let expr = arg.as_expression()?;
813+
let mut radix = expr.evaluate_value_to_number(ctx)?.to_int_32();
814+
if radix == 0 {
815+
radix = 10;
816+
} else if !(2..=36).contains(&radix) {
817+
return Some(ConstantValue::Number(f64::NAN));
818+
} else if radix != 16 {
819+
strip_prefix = false;
820+
}
821+
radix as u32
822+
} else {
823+
10
824+
};
825+
826+
if !matches!(radix, 2 | 4 | 8 | 10 | 16 | 32) {
827+
// implementation can approximate the values. bail out to be safe
828+
return None;
829+
}
830+
831+
if strip_prefix && (string_value.starts_with("0x") || string_value.starts_with("0X")) {
832+
string_value = &string_value[2..];
833+
radix = 16;
834+
}
835+
836+
if let Some(non_radix_digit_pos) = string_value.chars().position(|c| !c.is_digit(radix)) {
837+
string_value = &string_value[..non_radix_digit_pos];
838+
}
839+
840+
if string_value.is_empty() {
841+
return Some(ConstantValue::Number(f64::NAN));
842+
}
843+
844+
if radix == 10 && string_value.len() > 20 {
845+
// implementation can approximate the values. bail out to be safe
846+
return None;
847+
}
848+
849+
let Ok(math_int) = i32::from_str_radix(string_value, radix) else {
850+
// ignore values that cannot be represented as i32 to avoid precision issues
851+
return None;
852+
};
853+
if math_int == 0 {
854+
return Some(ConstantValue::Number(if sign == -1 { -0.0 } else { 0.0 }));
855+
}
856+
Some(ConstantValue::Number((math_int as f64) * sign as f64))
857+
}
858+
859+
#[test]
860+
fn test_find_str_decimal_literal_prefix() {
861+
assert_eq!(find_str_decimal_literal_prefix("Infinitya"), Some("Infinity"));
862+
assert_eq!(find_str_decimal_literal_prefix("+Infinitya"), Some("+Infinity"));
863+
assert_eq!(find_str_decimal_literal_prefix("-Infinitya"), Some("-Infinity"));
864+
assert_eq!(find_str_decimal_literal_prefix("0a"), Some("0"));
865+
assert_eq!(find_str_decimal_literal_prefix("+0a"), Some("+0"));
866+
assert_eq!(find_str_decimal_literal_prefix("-0a"), Some("-0"));
867+
assert_eq!(find_str_decimal_literal_prefix("0."), Some("0"));
868+
assert_eq!(find_str_decimal_literal_prefix("0.e"), Some("0"));
869+
assert_eq!(find_str_decimal_literal_prefix("0.e1"), Some("0"));
870+
assert_eq!(find_str_decimal_literal_prefix("0.1"), Some("0.1"));
871+
assert_eq!(find_str_decimal_literal_prefix("0.1."), Some("0.1"));
872+
assert_eq!(find_str_decimal_literal_prefix("0.1e"), Some("0.1"));
873+
assert_eq!(find_str_decimal_literal_prefix("0.1e1"), Some("0.1e1"));
874+
assert_eq!(find_str_decimal_literal_prefix(".1"), Some(".1"));
875+
assert_eq!(find_str_decimal_literal_prefix(".1."), Some(".1"));
876+
assert_eq!(find_str_decimal_literal_prefix(".1e"), Some(".1"));
877+
assert_eq!(find_str_decimal_literal_prefix(".1e1"), Some(".1e1"));
878+
assert_eq!(find_str_decimal_literal_prefix("1_"), Some("1"));
879+
assert_eq!(find_str_decimal_literal_prefix("1_1"), Some("1_1"));
880+
assert_eq!(find_str_decimal_literal_prefix("1_1_"), Some("1_1"));
881+
}

crates/oxc_ecmascript/src/string_to_number.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use oxc_syntax::identifier::{is_line_terminator, is_white_space};
2+
13
pub trait StringToNumber {
24
fn string_to_number(&self) -> f64;
35
}
@@ -7,7 +9,7 @@ pub trait StringToNumber {
79
/// <https://tc39.es/ecma262/#sec-stringtonumber>
810
impl StringToNumber for &str {
911
fn string_to_number(&self) -> f64 {
10-
let s = *self;
12+
let s = self.trim_start_matches(is_str_white_space_char);
1113
match s {
1214
"" => return 0.0,
1315
"-Infinity" => return f64::NEG_INFINITY,
@@ -66,3 +68,9 @@ impl StringToNumber for &str {
6668
s.parse::<f64>().unwrap_or(f64::NAN)
6769
}
6870
}
71+
72+
/// whether the char is a StrWhiteSpaceChar
73+
/// <https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type>
74+
fn is_str_white_space_char(c: char) -> bool {
75+
is_white_space(c) || is_line_terminator(c)
76+
}

0 commit comments

Comments
 (0)