diff --git a/.gitignore b/.gitignore index 6241b9fa41..42c96f2e8c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ *.iml # Vim -.*.swp -.*.swo +*.*.swp +*.*.swo # Build target @@ -16,4 +16,4 @@ yarn-error.log .vscode/settings.json # tests/js/test.js is used for testing changes locally -test/js/test.js +tests/js/test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index af1c6a33eb..769a745034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ Feature enhancements: Implement Array.prototype.fill() (@bojan88) - Array tests: Tests implemented for shift, unshift and reverse, pop and push (@muskuloes) - Demo page has been improved, new font plus change on input. Thanks @WofWca +- [FEATURE #182](https://github.com/jasonwilliams/boa/pull/182): + Implement some Number prototype methods (incl tests) (@pop) Bug fixes: diff --git a/src/lib/builtins/mod.rs b/src/lib/builtins/mod.rs index 1c59657f31..3d549fa445 100644 --- a/src/lib/builtins/mod.rs +++ b/src/lib/builtins/mod.rs @@ -12,6 +12,8 @@ pub mod function; pub mod json; /// The global `Math` object pub mod math; +/// The global `Number` object +pub mod number; /// The global `Object` object pub mod object; /// The global 'RegExp' object diff --git a/src/lib/builtins/number.rs b/src/lib/builtins/number.rs new file mode 100644 index 0000000000..a0762d9aa0 --- /dev/null +++ b/src/lib/builtins/number.rs @@ -0,0 +1,382 @@ +use crate::{ + builtins::{ + function::NativeFunctionData, + object::{Object, ObjectKind, PROTOTYPE}, + value::{to_value, ResultValue, Value, ValueData}, + }, + exec::Interpreter, +}; +use std::{borrow::Borrow, f64, ops::Deref}; + +/// Helper function: to_number(value: &Value) -> Value +/// +/// Converts a Value to a Number. +fn to_number(value: &Value) -> Value { + match *value.deref().borrow() { + ValueData::Boolean(b) => { + if b { + to_value(1) + } else { + to_value(0) + } + } + ValueData::Function(_) | ValueData::Undefined => to_value(f64::NAN), + ValueData::Integer(i) => to_value(f64::from(i)), + ValueData::Object(ref o) => (o).deref().borrow().get_internal_slot("NumberData"), + ValueData::Null => to_value(0), + ValueData::Number(n) => to_value(n), + ValueData::String(ref s) => match s.parse::() { + Ok(n) => to_value(n), + Err(_) => to_value(f64::NAN), + }, + } +} + +/// Helper function: num_to_exponential(n: f64) -> String +/// +/// Formats a float as a ES6-style exponential number string. +fn num_to_exponential(n: f64) -> String { + match n.abs() { + x if x > 1.0 => format!("{:e}", n).replace("e", "e+"), + x if x == 0.0 => format!("{:e}", n).replace("e", "e+"), + _ => format!("{:e}", n), + } +} + +/// Number(arg) +/// +/// Create a new number [[Construct]] +pub fn make_number(this: &Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + let data = match args.get(0) { + Some(ref value) => to_number(value), + None => to_number(&to_value(0)), + }; + this.set_internal_slot("NumberData", data); + Ok(this.clone()) +} + +/// Number() +/// +/// https://tc39.es/ecma262/#sec-number-constructor-number-value +pub fn call_number(_this: &Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + let data = match args.get(0) { + Some(ref value) => to_number(value), + None => to_number(&to_value(0)), + }; + Ok(data) +} + +/// Number().toExponential() +/// +/// https://tc39.es/ecma262/#sec-number.prototype.toexponential +pub fn to_exponential(this: &Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + let this_num = to_number(this).to_num(); + let this_str_num = num_to_exponential(this_num); + Ok(to_value(this_str_num)) +} + +/// https://tc39.es/ecma262/#sec-number.prototype.tofixed +pub fn to_fixed(this: &Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + let this_num = to_number(this).to_num(); + let precision = match args.get(0) { + Some(n) => match n.to_int() { + x if x > 0 => n.to_int() as usize, + _ => 0, + }, + None => 0, + }; + let this_fixed_num = format!("{:.*}", precision, this_num); + Ok(to_value(this_fixed_num)) +} + +/// Number().toLocaleString() +/// +/// https://tc39.es/ecma262/#sec-number.prototype.tolocalestring +/// +/// Note that while this technically conforms to the Ecma standard, it does no actual +/// internationalization logic. +pub fn to_locale_string(this: &Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + let this_num = to_number(this).to_num(); + let this_str_num = format!("{}", this_num); + Ok(to_value(this_str_num)) +} + +/// Number().toPrecision(p) +/// +/// https://tc39.es/ecma262/#sec-number.prototype.toprecision +pub fn to_precision(this: &Value, args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + println!("Number::to_precision()"); + let this_num = to_number(this); + let _num_str_len = format!("{}", this_num.to_num()).len(); + let _precision = match args.get(0) { + Some(n) => match n.to_int() { + x if x > 0 => n.to_int() as usize, + _ => 0, + }, + None => 0, + }; + // TODO: Implement toPrecision + unimplemented!(); +} + +/// Number().toString() +/// +/// https://tc39.es/ecma262/#sec-number.prototype.tostring +pub fn to_string(this: &Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + Ok(to_value(format!("{}", to_number(this).to_num()))) +} + +/// Number().valueOf() +/// +/// https://tc39.es/ecma262/#sec-number.prototype.valueof +pub fn value_of(this: &Value, _args: &[Value], _ctx: &mut Interpreter) -> ResultValue { + Ok(to_number(this)) +} + +/// Create a new `Number` object +pub fn create_constructor(global: &Value) -> Value { + let mut number_constructor = Object::default(); + number_constructor.kind = ObjectKind::Function; + + number_constructor.set_internal_method("construct", make_number); + number_constructor.set_internal_method("call", call_number); + + let number_prototype = ValueData::new_obj(Some(global)); + + number_prototype.set_internal_slot("NumberData", to_value(0)); + + number_prototype.set_field_slice( + "toExponential", + to_value(to_exponential as NativeFunctionData), + ); + number_prototype.set_field_slice("toFixed", to_value(to_fixed as NativeFunctionData)); + number_prototype.set_field_slice( + "toLocaleString", + to_value(to_locale_string as NativeFunctionData), + ); + number_prototype.set_field_slice("toPrecision", to_value(to_precision as NativeFunctionData)); + number_prototype.set_field_slice("toString", to_value(to_string as NativeFunctionData)); + number_prototype.set_field_slice("valueOf", to_value(value_of as NativeFunctionData)); + + let number = to_value(number_constructor); + number_prototype.set_field_slice("constructor", number.clone()); + number.set_field_slice(PROTOTYPE, number_prototype); + number +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{builtins::value::ValueData, exec::Executor, forward, forward_val, realm::Realm}; + use std::f64; + + #[test] + fn check_number_constructor_is_function() { + let global = ValueData::new_obj(None); + let number_constructor = create_constructor(&global); + assert_eq!(number_constructor.is_function(), true); + } + + #[test] + fn call_number() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + let init = r#" + var default_zero = Number(); + var int_one = Number(1); + var float_two = Number(2.1); + var str_three = Number('3.2'); + var bool_one = Number(true); + var bool_zero = Number(false); + var invalid_nan = Number("I am not a number"); + var from_exp = Number("2.34e+2"); + "#; + + forward(&mut engine, init); + let default_zero = forward_val(&mut engine, "default_zero").unwrap(); + let int_one = forward_val(&mut engine, "int_one").unwrap(); + let float_two = forward_val(&mut engine, "float_two").unwrap(); + let str_three = forward_val(&mut engine, "str_three").unwrap(); + let bool_one = forward_val(&mut engine, "bool_one").unwrap(); + let bool_zero = forward_val(&mut engine, "bool_zero").unwrap(); + let invalid_nan = forward_val(&mut engine, "invalid_nan").unwrap(); + let from_exp = forward_val(&mut engine, "from_exp").unwrap(); + + assert_eq!(default_zero.to_num(), f64::from(0)); + assert_eq!(int_one.to_num(), f64::from(1)); + assert_eq!(float_two.to_num(), f64::from(2.1)); + assert_eq!(str_three.to_num(), f64::from(3.2)); + assert_eq!(bool_one.to_num(), f64::from(1)); + assert!(invalid_nan.to_num().is_nan()); + assert_eq!(bool_zero.to_num(), f64::from(0)); + assert_eq!(from_exp.to_num(), f64::from(234)); + } + + #[test] + fn to_exponential() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + let init = r#" + var default_exp = Number().toExponential(); + var int_exp = Number(5).toExponential(); + var float_exp = Number(1.234).toExponential(); + var big_exp = Number(1234).toExponential(); + var nan_exp = Number("I am also not a number").toExponential(); + var noop_exp = Number("1.23e+2").toExponential(); + "#; + + forward(&mut engine, init); + let default_exp = forward(&mut engine, "default_exp"); + let int_exp = forward(&mut engine, "int_exp"); + let float_exp = forward(&mut engine, "float_exp"); + let big_exp = forward(&mut engine, "big_exp"); + let nan_exp = forward(&mut engine, "nan_exp"); + let noop_exp = forward(&mut engine, "noop_exp"); + + assert_eq!(default_exp, String::from("0e+0")); + assert_eq!(int_exp, String::from("5e+0")); + assert_eq!(float_exp, String::from("1.234e+0")); + assert_eq!(big_exp, String::from("1.234e+3")); + assert_eq!(nan_exp, String::from("NaN")); + assert_eq!(noop_exp, String::from("1.23e+2")); + } + + #[test] + fn to_fixed() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + let init = r#" + var default_fixed = Number().toFixed(); + var pos_fixed = Number("3.456e+4").toFixed(); + var neg_fixed = Number("3.456e-4").toFixed(); + var noop_fixed = Number(5).toFixed(); + var nan_fixed = Number("I am not a number").toFixed(); + "#; + + forward(&mut engine, init); + let default_fixed = forward(&mut engine, "default_fixed"); + let pos_fixed = forward(&mut engine, "pos_fixed"); + let neg_fixed = forward(&mut engine, "neg_fixed"); + let noop_fixed = forward(&mut engine, "noop_fixed"); + let nan_fixed = forward(&mut engine, "nan_fixed"); + + assert_eq!(default_fixed, String::from("0")); + assert_eq!(pos_fixed, String::from("34560")); + assert_eq!(neg_fixed, String::from("0")); + assert_eq!(noop_fixed, String::from("5")); + assert_eq!(nan_fixed, String::from("NaN")); + } + + #[test] + fn to_locale_string() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + let init = r#" + var default_locale = Number().toLocaleString(); + var small_locale = Number(5).toLocaleString(); + var big_locale = Number("345600").toLocaleString(); + var neg_locale = Number(-25).toLocaleString(); + "#; + + // TODO: We don't actually do any locale checking here + // To honor the spec we should print numbers according to user locale. + + forward(&mut engine, init); + let default_locale = forward(&mut engine, "default_locale"); + let small_locale = forward(&mut engine, "small_locale"); + let big_locale = forward(&mut engine, "big_locale"); + let neg_locale = forward(&mut engine, "neg_locale"); + + assert_eq!(default_locale, String::from("0")); + assert_eq!(small_locale, String::from("5")); + assert_eq!(big_locale, String::from("345600")); + assert_eq!(neg_locale, String::from("-25")); + } + + #[test] + #[ignore] + fn to_precision() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + let init = r#" + var default_precision = Number().toPrecision(); + var low_precision = Number(123456789).toPrecision(1); + var more_precision = Number(123456789).toPrecision(4); + var exact_precision = Number(123456789).toPrecision(9); + var over_precision = Number(123456789).toPrecision(50); + var neg_precision = Number(-123456789).toPrecision(4); + "#; + + forward(&mut engine, init); + let default_precision = forward(&mut engine, "default_precision"); + let low_precision = forward(&mut engine, "low_precision"); + let more_precision = forward(&mut engine, "more_precision"); + let exact_precision = forward(&mut engine, "exact_precision"); + let over_precision = forward(&mut engine, "over_precision"); + let neg_precision = forward(&mut engine, "neg_precision"); + + assert_eq!(default_precision, String::from("0")); + assert_eq!(low_precision, String::from("1e+8")); + assert_eq!(more_precision, String::from("1.235e+8")); + assert_eq!(exact_precision, String::from("123456789")); + assert_eq!( + over_precision, + String::from("123456789.00000000000000000000000000000000000000000") + ); + assert_eq!(neg_precision, String::from("-1.235e+8")); + } + + #[test] + fn to_string() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + let init = r#" + var default_string = Number().toString(); + var int_string = Number(123).toString(); + var float_string = Number(1.234).toString(); + var exp_string = Number("1.2e+4").toString(); + var neg_string = Number(-1.2).toString(); + "#; + + forward(&mut engine, init); + let default_string = forward(&mut engine, "default_string"); + let int_string = forward(&mut engine, "int_string"); + let float_string = forward(&mut engine, "float_string"); + let exp_string = forward(&mut engine, "exp_string"); + let neg_string = forward(&mut engine, "neg_string"); + + assert_eq!(default_string, String::from("0")); + assert_eq!(int_string, String::from("123")); + assert_eq!(float_string, String::from("1.234")); + assert_eq!(exp_string, String::from("12000")); + assert_eq!(neg_string, String::from("-1.2")); + } + + #[test] + fn value_of() { + let realm = Realm::create(); + let mut engine = Executor::new(realm); + // TODO: In addition to parsing numbers from strings, parse them bare As of October 2019 + // the parser does not understand scientific e.g., Xe+Y or -Xe-Y notation. + let init = r#" + var default_val = Number().valueOf(); + var int_val = Number("123").valueOf(); + var float_val = Number(1.234).valueOf(); + var exp_val = Number("1.2e+4").valueOf() + var neg_val = Number("-1.2e+4").valueOf() + "#; + + forward(&mut engine, init); + let default_val = forward_val(&mut engine, "default_val").unwrap(); + let int_val = forward_val(&mut engine, "int_val").unwrap(); + let float_val = forward_val(&mut engine, "float_val").unwrap(); + let exp_val = forward_val(&mut engine, "exp_val").unwrap(); + let neg_val = forward_val(&mut engine, "neg_val").unwrap(); + + assert_eq!(default_val.to_num(), f64::from(0)); + assert_eq!(int_val.to_num(), f64::from(123)); + assert_eq!(float_val.to_num(), f64::from(1.234)); + assert_eq!(exp_val.to_num(), f64::from(12000)); + assert_eq!(neg_val.to_num(), f64::from(-12000)); + } +} diff --git a/src/lib/builtins/value.rs b/src/lib/builtins/value.rs index 7221976372..1aeb5bbb1b 100644 --- a/src/lib/builtins/value.rs +++ b/src/lib/builtins/value.rs @@ -131,6 +131,11 @@ impl ValueData { } } + /// Returns true if the value is a number + pub fn is_num(&self) -> bool { + self.is_double() + } + /// Returns true if the value is a string pub fn is_string(&self) -> bool { match *self { diff --git a/src/lib/exec.rs b/src/lib/exec.rs index 7cca7d0d9c..d00a04097e 100644 --- a/src/lib/exec.rs +++ b/src/lib/exec.rs @@ -682,6 +682,33 @@ impl Interpreter { _ => String::from("undefined"), } } + + pub fn value_to_rust_number(&mut self, value: &Value) -> f64 { + match *value.deref().borrow() { + ValueData::Null => f64::from(0), + ValueData::Boolean(boolean) => { + if boolean { + f64::from(1) + } else { + f64::from(0) + } + } + ValueData::Number(num) => num, + ValueData::Integer(num) => f64::from(num), + ValueData::String(ref string) => string.parse::().unwrap(), + ValueData::Object(_) => { + let prim_value = self.to_primitive(value, Some("number")); + self.to_string(&prim_value) + .to_string() + .parse::() + .unwrap() + } + _ => { + // TODO: Make undefined? + f64::from(0) + } + } + } } #[cfg(test)] diff --git a/src/lib/realm.rs b/src/lib/realm.rs index b42c4d035f..372c7eac13 100644 --- a/src/lib/realm.rs +++ b/src/lib/realm.rs @@ -5,7 +5,7 @@ //!A realm is represented in this implementation as a Realm struct with the fields specified from the spec use crate::{ builtins::{ - array, boolean, console, function, json, math, object, regexp, string, + array, boolean, console, function, json, math, number, object, regexp, string, value::{Value, ValueData}, }, environment::{ @@ -54,14 +54,15 @@ impl Realm { // Create intrinsics, add global objects here function::init(global); - global.set_field_slice("String", string::create_constructor(global)); - global.set_field_slice("RegExp", regexp::create_constructor(global)); global.set_field_slice("Array", array::create_constructor(global)); global.set_field_slice("Boolean", boolean::create_constructor(global)); global.set_field_slice("JSON", json::create_constructor(global)); global.set_field_slice("Math", math::create_constructor(global)); - global.set_field_slice("console", console::create_constructor(global)); + global.set_field_slice("Number", number::create_constructor(global)); global.set_field_slice("Object", object::create_constructor(global)); + global.set_field_slice("RegExp", regexp::create_constructor(global)); + global.set_field_slice("String", string::create_constructor(global)); + global.set_field_slice("console", console::create_constructor(global)); } } diff --git a/tests/js/test.js b/tests/js/test.js index ac0f9d87ba..e69de29bb2 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -1,8 +0,0 @@ -let a = { - a: true, - b() { - return 2; - } -}; - -console.log(a["b"]());