Skip to content

Commit

Permalink
Fixed a bunch of test262 panics (#817)
Browse files Browse the repository at this point in the history
This also implements a spec-compliant `parseInt()` function.
  • Loading branch information
Razican committed Jan 7, 2021
1 parent 1a7e832 commit b02a859
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 141 deletions.
16 changes: 8 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions boa/src/builtins/array/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1364,9 +1364,9 @@ fn get_relative_end() {

#[test]
fn array_length_is_not_enumerable() {
let mut context = Context::new();
let context = Context::new();

let array = Array::new_array(&mut context).unwrap();
let array = Array::new_array(&context).unwrap();
let desc = array.get_property("length").unwrap();
assert!(!desc.enumerable());
}
128 changes: 88 additions & 40 deletions boa/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
value::{AbstractRelation, IntegerOrInfinity, Value},
BoaProfiler, Context, Result,
};
use num_traits::float::FloatCore;
use num_traits::{float::FloatCore, Num};

mod conversions;

Expand Down Expand Up @@ -651,54 +651,102 @@ impl Number {
///
/// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt
pub(crate) fn parse_int(_: &Value, args: &[Value], _ctx: &mut Context) -> Result<Value> {
if let (Some(val), r) = (args.get(0), args.get(1)) {
let mut radix = if let Some(rx) = r {
if let Value::Integer(i) = rx {
*i as u32
} else {
// Handling a second argument that isn't an integer but was provided so cannot be defaulted.
return Ok(Value::from(f64::NAN));
}
pub(crate) fn parse_int(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
if let (Some(val), radix) = (args.get(0), args.get(1)) {
// 1. Let inputString be ? ToString(string).
let input_string = val.to_string(context)?;

// 2. Let S be ! TrimString(inputString, start).
let mut var_s = input_string.trim();

// 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.
let sign = if !var_s.is_empty() && var_s.starts_with('\u{002D}') {
-1
} else {
// No second argument provided therefore radix is unknown
0
1
};

match val {
Value::String(s) => {
// Attempt to infer radix from given string.

if radix == 0 {
if s.starts_with("0x") || s.starts_with("0X") {
return if let Ok(i) = i32::from_str_radix(&s[2..], 16) {
Ok(Value::integer(i))
} else {
// String can't be parsed.
Ok(Value::from(f64::NAN))
};
} else {
radix = 10
};
}
// 5. If S is not empty and the first code unit of S is the code unit 0x002B (PLUS SIGN) or the code unit 0x002D (HYPHEN-MINUS), remove the first code unit from S.
if !var_s.is_empty() {
var_s = var_s
.strip_prefix(&['\u{002B}', '\u{002D}'][..])
.unwrap_or(var_s);
}

if let Ok(i) = i32::from_str_radix(s, radix) {
Ok(Value::integer(i))
} else {
// String can't be parsed.
Ok(Value::from(f64::NAN))
}
// 6. Let R be ℝ(? ToInt32(radix)).
let mut var_r = radix.cloned().unwrap_or_default().to_i32(context)?;

// 7. Let stripPrefix be true.
let mut strip_prefix = true;

// 8. If R ≠ 0, then
if var_r != 0 {
// a. If R < 2 or R > 36, return NaN.
if !(2..=36).contains(&var_r) {
return Ok(Value::nan());
}
Value::Integer(i) => Ok(Value::integer(*i)),
Value::Rational(f) => Ok(Value::integer(*f as i32)),
_ => {
// Wrong argument type to parseInt.
Ok(Value::from(f64::NAN))

// b. If R ≠ 16, set stripPrefix to false.
if var_r != 16 {
strip_prefix = false
}
} else {
// 9. Else,
// a. Set R to 10.
var_r = 10;
}

// 10. If stripPrefix is true, then
// a. If the length of S is at least 2 and the first two code units of S are either "0x" or "0X", then
// i. Remove the first two code units from S.
// ii. Set R to 16.
if strip_prefix
&& var_s.len() >= 2
&& (var_s.starts_with("0x") || var_s.starts_with("0X"))
{
var_s = var_s.split_at(2).1;

var_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 = if let Some(index) = var_s.find(|c: char| !c.is_digit(var_r as u32)) {
index
} else {
var_s.len()
};

// 12. Let Z be the substring of S from 0 to end.
let var_z = var_s.split_at(end).0;

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

// 14. Let mathInt be the integer value that is represented by Z in radix-R notation, using the letters A-Z and a-z for digits with values 10 through 35. (However, if R is 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 2, 4, 8, 10, 16, or 32, then mathInt may be an implementation-approximated value representing the integer value that is represented by Z in radix-R notation.)
let math_int = u64::from_str_radix(var_z, var_r as u32).map_or_else(
|_| f64::from_str_radix(var_z, var_r as u32).expect("invalid_float_conversion"),
|i| i as f64,
);

// 15. If mathInt = 0, then
// a. If sign = -1, return -0𝔽.
// b. Return +0𝔽.
if math_int == 0_f64 {
if sign == -1 {
return Ok(Value::rational(-0_f64));
} else {
return Ok(Value::rational(0_f64));
}
}

// 16. Return 𝔽(sign × mathInt).
Ok(Value::rational(f64::from(sign) * math_int))
} else {
// Not enough arguments to parseInt.
Ok(Value::from(f64::NAN))
Ok(Value::nan())
}
}

Expand Down
2 changes: 1 addition & 1 deletion boa/src/builtins/number/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ fn parse_int_float() {
fn parse_int_float_str() {
let mut context = Context::new();

assert_eq!(&forward(&mut context, "parseInt(\"100.5\")"), "NaN");
assert_eq!(&forward(&mut context, "parseInt(\"100.5\")"), "100");
}

#[test]
Expand Down
7 changes: 6 additions & 1 deletion boa/src/builtins/regexp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,12 @@ impl RegExp {
pub(crate) fn to_string(this: &Value, _: &[Value], context: &mut Context) -> Result<Value> {
let (body, flags) = if let Some(object) = this.as_object() {
let object = object.borrow();
let regex = object.as_regexp().unwrap();
let regex = object.as_regexp().ok_or_else(|| {
context.construct_type_error(format!(
"Method RegExp.prototype.toString called on incompatible receiver {}",
this.display()
))
})?;
(regex.original_source.clone(), regex.flags.clone())
} else {
return context.throw_type_error(format!(
Expand Down
2 changes: 1 addition & 1 deletion boa/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ impl Context {
let key = field.to_property_key(self)?;
Ok(get_field.obj().run(self)?.set_field(key, value, self)?)
}
_ => panic!("TypeError: invalid assignment to {}", node),
_ => self.throw_type_error(format!("invalid assignment to {}", node)),
}
}

Expand Down
2 changes: 1 addition & 1 deletion boa/src/syntax/ast/node/operator/unary_op/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ impl Executable for UnaryOp {
| Node::New(_)
| Node::Object(_)
| Node::UnaryOp(_) => Value::boolean(true),
_ => panic!("SyntaxError: wrong delete argument {}", self),
_ => return context.throw_syntax_error(format!("wrong delete argument {}", self)),
},
op::UnaryOp::TypeOf => Value::from(x.get_type().as_str()),
})
Expand Down
15 changes: 11 additions & 4 deletions boa/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,16 @@ impl Value {
Self::Boolean(b) => Ok(JSONValue::Bool(b)),
Self::Object(ref obj) => obj.to_json(context),
Self::String(ref str) => Ok(JSONValue::String(str.to_string())),
Self::Rational(num) => Ok(JSONValue::Number(
JSONNumber::from_str(&Number::to_native_string(num)).unwrap(),
)),
Self::Rational(num) => {
if num.is_finite() {
Ok(JSONValue::Number(
JSONNumber::from_str(&Number::to_native_string(num))
.expect("invalid number found"),
))
} else {
Ok(JSONValue::Null)
}
}
Self::Integer(val) => Ok(JSONValue::Number(JSONNumber::from(val))),
Self::BigInt(_) => {
Err(context.construct_type_error("BigInt value can't be serialized in JSON"))
Expand Down Expand Up @@ -716,7 +723,7 @@ impl Value {
///
/// This function is equivalent to `value | 0` in JavaScript
///
/// See: <https://tc39.es/ecma262/#sec-toint32>
/// See: <https://tc39.es/ecma262/#sec-touint32>
pub fn to_u32(&self, context: &mut Context) -> Result<u32> {
// This is the fast path, if the value is Integer we can just return it.
if let Value::Integer(number) = *self {
Expand Down
10 changes: 6 additions & 4 deletions boa/src/value/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,15 @@ fn hash_rational() {
assert_eq!(value1, value2);
assert_eq!(hash_value(&value1), hash_value(&value2));

let nan = Value::nan();
assert_eq!(nan, nan);
assert_eq!(hash_value(&nan), hash_value(&nan));
assert_ne!(hash_value(&nan), hash_value(&Value::rational(1.0)));
let nan1 = Value::nan();
let nan2 = Value::nan();
assert_eq!(nan1, nan2);
assert_eq!(hash_value(&nan1), hash_value(&nan2));
assert_ne!(hash_value(&nan1), hash_value(&Value::rational(1.0)));
}

#[test]
#[allow(clippy::eq_op)]
fn hash_object() {
let object1 = Value::object(Object::default());
assert_eq!(object1, object1);
Expand Down
2 changes: 1 addition & 1 deletion boa_wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ edition = "2018"
[dependencies]
Boa = { path = "../boa", features = ["console"] }
wasm-bindgen = "0.2.69"
getrandom = { version = "0.2.1", features = ["js"]}
getrandom = { version = "0.2.1", features = ["js"] }

[lib]
crate-type = ["cdylib", "lib"]
Expand Down
Loading

0 comments on commit b02a859

Please sign in to comment.