Skip to content

Commit

Permalink
feat(ecmascript): Implement parseInt (#456)
Browse files Browse the repository at this point in the history
  • Loading branch information
eliassjogreen authored Nov 9, 2024
1 parent 840888e commit 25945bf
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 65 deletions.
178 changes: 173 additions & 5 deletions nova_vm/src/ecmascript/builtins/global_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use oxc_ast::ast::{BindingIdentifier, Program, VariableDeclarationKind};
use oxc_ecmascript::BoundNames;
use oxc_span::SourceType;

use crate::ecmascript::abstract_operations::type_conversion::{
is_trimmable_whitespace, to_int32, to_string,
};
use crate::engine::context::GcScope;
use crate::{
ecmascript::{
Expand Down Expand Up @@ -74,7 +77,7 @@ impl BuiltinIntrinsic for GlobalObjectParseFloat {
struct GlobalObjectParseInt;
impl Builtin for GlobalObjectParseInt {
const NAME: String = BUILTIN_STRING_MEMORY.parseInt;
const LENGTH: u8 = 1;
const LENGTH: u8 = 2;
const BEHAVIOUR: Behaviour = Behaviour::Regular(GlobalObject::parse_int);
}
impl BuiltinIntrinsic for GlobalObjectParseInt {
Expand Down Expand Up @@ -725,15 +728,180 @@ impl GlobalObject {
) -> JsResult<Value> {
todo!()
}

/// ### [19.2.5 parseInt ( string, radix )](https://tc39.es/ecma262/#sec-parseint-string-radix)
///
/// This function produces an integral Number dictated by interpretation of
/// the contents of string according to the specified radix. Leading white
/// space in string is ignored. If radix coerces to 0 (such as when it is
/// undefined), it is assumed to be 10 except when the number
/// representation begins with "0x" or "0X", in which case it is assumed to
/// be 16. If radix is 16, the number representation may optionally begin
/// with "0x" or "0X".
fn parse_int(
_agent: &mut Agent,
_gc: GcScope<'_, '_>,
agent: &mut Agent,
mut gc: GcScope<'_, '_>,

_this_value: Value,
_: ArgumentsList,
arguments: ArgumentsList,
) -> JsResult<Value> {
todo!()
let string = arguments.get(0);
let radix = arguments.get(1);

// OPTIMIZATION: If the string is empty, undefined, null or a boolean, return NaN.
if string.is_undefined()
|| string.is_null()
|| string.is_boolean()
|| string.is_empty_string()
{
return Ok(Value::nan());
}

// OPTIMIZATION: If the string is an integer and the radix is 10, return the number.
if let Value::Integer(radix) = radix {
let radix = radix.into_i64();
if radix == 10 && matches!(string, Value::Integer(_)) {
return Ok(string);
}
}

// 1. Let inputString be ? ToString(string).
let s = to_string(agent, gc.reborrow(), string)?;

// 6. Let R be ℝ(? ToInt32(radix)).
let r = to_int32(agent, gc.reborrow(), radix)?;

// 2. Let S be ! TrimString(inputString, start).
let s = s.as_str(agent).trim_start_matches(is_trimmable_whitespace);

// 3. Let sign be 1.
// 4. If S is not empty and the first code unit of S is the code unit 0x002D (HYPHEN-MINUS), set sign to -1.
// 5. If S is not empty and the first code unit of S is either the code unit 0x002B (PLUS SIGN) or the code unit 0x002D (HYPHEN-MINUS), set S to the substring of S from index 1.
let (sign, mut s) = if let Some(s) = s.strip_prefix('-') {
(-1, s)
} else if let Some(s) = s.strip_prefix('+') {
(1, s)
} else {
(1, s)
};

// 7. Let stripPrefix be true.
// 8. If R ≠ 0, then
let (mut r, strip_prefix) = if r != 0 {
// a. If R < 2 or R > 36, return NaN.
if !(2..=36).contains(&r) {
return Ok(Value::nan());
}
// b. If R ≠ 16, set stripPrefix to false.
(r as u32, r == 16)
} else {
// 9. Else,
// a. Set R to 10.
(10, true)
};

// 10. If stripPrefix is true, then
if strip_prefix {
// a. If the length of S is at least 2 and the first two code units of S are either "0x" or "0X", then
if s.starts_with("0x") || s.starts_with("0X") {
// i. Set S to the substring of S from index 2.
s = &s[2..];
// ii. Set R to 16.
r = 16;
}
};

// 11. If S contains a code unit that is not a radix-R digit, let end be the index within S of the first such code unit; otherwise, let end be the length of S.
let end = s.find(|c: char| !c.is_digit(r)).unwrap_or(s.len());

// 12. Let Z be the substring of S from 0 to end.
let z = &s[..end];

// 13. If Z is empty, return NaN.
if z.is_empty() {
return Ok(Value::nan());
}

/// OPTIMIZATION: Quick path for known safe radix and length combinations.
/// E.g. we know that a number in base 2 with less than 8 characters is
/// guaranteed to be safe to parse as an u8, and so on. To calculate the
/// known safe radix and length combinations, the following pseudocode
/// can be consulted:
/// ```ignore
/// u8.MAX .toString(radix).length
/// u16.MAX .toString(radix).length
/// u32.MAX .toString(radix).length
/// Number.MAX_SAFE_INTEGER .toString(radix).length
/// ```
macro_rules! parse_known_safe_radix_and_length {
($unsigned: ty, $signed: ty, $signed_large: ty) => {{
let math_int = <$unsigned>::from_str_radix(z, r).unwrap();

Ok(if sign == -1 {
if math_int <= (<$signed>::MAX as $unsigned) {
Value::try_from(-(math_int as $signed)).unwrap()
} else {
Value::try_from(-(math_int as $signed_large)).unwrap()
}
} else {
Value::try_from(math_int).unwrap()
})
}};
}

// 14. Let mathInt be the integer value that is represented by Z in
// radix-R notation, using the letters A through Z and a through z
// for digits with values 10 through 35. (However, if R = 10 and Z
// contains more than 20 significant digits, every significant
// digit after the 20th may be replaced by a 0 digit, at the option
// of the implementation; and if R is not one of 2, 4, 8, 10, 16,
// or 32, then mathInt may be an implementation-approximated
// integer representing the integer value denoted by Z in radix-R
// notation.)
match (r, z.len()) {
(2, 0..8) => parse_known_safe_radix_and_length!(u8, i8, i16),
(2, 8..16) => parse_known_safe_radix_and_length!(u16, i16, i32),
(2, 16..32) => parse_known_safe_radix_and_length!(u32, i32, i64),
(2, 32..53) => parse_known_safe_radix_and_length!(i64, i64, i64),

(8, 0..3) => parse_known_safe_radix_and_length!(u8, i8, i16),
(8, 3..6) => parse_known_safe_radix_and_length!(u16, i16, i32),
(8, 6..11) => parse_known_safe_radix_and_length!(u32, i32, i64),
(8, 11..18) => parse_known_safe_radix_and_length!(i64, i64, i64),

(10..=11, 0..3) => parse_known_safe_radix_and_length!(u8, i8, i16),
(10..=11, 3..5) => parse_known_safe_radix_and_length!(u16, i16, i32),
(10..=11, 5..10) => parse_known_safe_radix_and_length!(u32, i32, i64),
(10..=11, 10..16) => parse_known_safe_radix_and_length!(i64, i64, i64),

(16, 0..2) => parse_known_safe_radix_and_length!(u8, i8, i16),
(16, 2..4) => parse_known_safe_radix_and_length!(u16, i16, i32),
(16, 4..8) => parse_known_safe_radix_and_length!(u32, i32, i64),
(16, 8..14) => parse_known_safe_radix_and_length!(i64, i64, i64),

(_, z_len) => {
match z_len {
// OPTIMIZATION: These are the known safe upper bounds for any
// integer represented in a radix up to 36.
0..2 => parse_known_safe_radix_and_length!(u8, i8, i16),
2..4 => parse_known_safe_radix_and_length!(u16, i16, i32),
4..7 => parse_known_safe_radix_and_length!(u32, i32, i64),
7..11 => parse_known_safe_radix_and_length!(i64, i64, i64),

_ => {
let math_int = i128::from_str_radix(z, r).unwrap() as f64;

// 15. If mathInt = 0, then
// a. If sign = -1, return -0𝔽.
// b. Return +0𝔽.
// 16. Return 𝔽(sign × mathInt).
Ok(Value::from_f64(agent, sign as f64 * math_int))
}
}
}
}
}

fn decode_uri(
_agent: &mut Agent,
_gc: GcScope<'_, '_>,
Expand Down
56 changes: 0 additions & 56 deletions tests/expectations.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@
"built-ins/Array/prototype/every/15.4.4.16-1-7.js": "FAIL",
"built-ins/Array/prototype/every/15.4.4.16-1-8.js": "FAIL",
"built-ins/Array/prototype/every/15.4.4.16-1-9.js": "FAIL",
"built-ins/Array/prototype/every/15.4.4.16-2-18.js": "CRASH",
"built-ins/Array/prototype/every/15.4.4.16-5-15.js": "CRASH",
"built-ins/Array/prototype/every/15.4.4.16-5-16.js": "CRASH",
"built-ins/Array/prototype/every/15.4.4.16-7-b-16.js": "FAIL",
Expand Down Expand Up @@ -314,7 +313,6 @@
"built-ins/Array/prototype/lastIndexOf/resizable-buffer.js": "CRASH",
"built-ins/Array/prototype/map/15.4.4.19-1-11.js": "CRASH",
"built-ins/Array/prototype/map/15.4.4.19-1-12.js": "CRASH",
"built-ins/Array/prototype/map/15.4.4.19-2-18.js": "CRASH",
"built-ins/Array/prototype/map/15.4.4.19-5-15.js": "CRASH",
"built-ins/Array/prototype/map/15.4.4.19-5-16.js": "CRASH",
"built-ins/Array/prototype/map/15.4.4.19-8-b-16.js": "FAIL",
Expand Down Expand Up @@ -371,7 +369,6 @@
"built-ins/Array/prototype/slice/resizable-buffer.js": "CRASH",
"built-ins/Array/prototype/some/15.4.4.17-1-11.js": "CRASH",
"built-ins/Array/prototype/some/15.4.4.17-1-12.js": "CRASH",
"built-ins/Array/prototype/some/15.4.4.17-2-18.js": "CRASH",
"built-ins/Array/prototype/some/15.4.4.17-5-15.js": "CRASH",
"built-ins/Array/prototype/some/15.4.4.17-5-16.js": "CRASH",
"built-ins/Array/prototype/some/15.4.4.17-7-b-16.js": "FAIL",
Expand Down Expand Up @@ -12294,59 +12291,6 @@
"built-ins/parseFloat/tonumber-numeric-separator-literal-nzd-nsl-dd.js": "CRASH",
"built-ins/parseFloat/tonumber-numeric-separator-literal-nzd-nsl-dds.js": "CRASH",
"built-ins/parseFloat/tonumber-numeric-separator-literal-sign-plus-dds-nsl-dd.js": "CRASH",
"built-ins/parseInt/15.1.2.2-2-1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T4.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T5.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T6.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A1_T7.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T10.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T10_U180E.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T4.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T5.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T6.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T7.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T8.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A2_T9.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T4.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T5.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T6.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.1_T7.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.2_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.2_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A3.2_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A4.1_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A4.1_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A4.2_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A4.2_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A5.1_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A5.2_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A5.2_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A6.1_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A6.1_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A6.1_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A6.1_T4.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A6.1_T5.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A6.1_T6.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.1_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.1_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.2_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.2_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.2_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.3_T1.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.3_T2.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A7.3_T3.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A8.js": "CRASH",
"built-ins/parseInt/S15.1.2.2_A9.3.js": "FAIL",
"built-ins/parseInt/S15.1.2.2_A9.4.js": "FAIL",
"harness/assert-throws-same-realm.js": "FAIL",
"harness/assertRelativeDateMs.js": "CRASH",
"harness/asyncHelpers-throwsAsync-custom-typeerror.js": "CRASH",
Expand Down
8 changes: 4 additions & 4 deletions tests/metrics.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"results": {
"crash": 15846,
"fail": 8372,
"pass": 21030,
"skip": 40,
"crash": 15792,
"fail": 8370,
"pass": 21085,
"skip": 41,
"timeout": 3,
"unresolved": 0
},
Expand Down
1 change: 1 addition & 0 deletions tests/skip.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"language/identifiers/part-unicode-16.0.0.js",
"language/identifiers/start-unicode-16.0.0-escaped.js",
"language/identifiers/start-unicode-16.0.0.js",
"built-ins/parseInt/S15.1.2.2_A8.js",
"built-ins/Array/length/15.4.5.1-3.d-3.js",
"built-ins/Array/length/S15.4.2.2_A2.1_T1.js",
"built-ins/Array/length/S15.4.5.2_A3_T4.js",
Expand Down

0 comments on commit 25945bf

Please sign in to comment.