Browse Source

Feature/number object (#182)

* Feature(Number): Tests for the Test Driven Development!

refs #34

* Feature: Skeleton code for Number

refs #34

* Feature: Most of Number() is complete

Includes:
- make_number()
- call_number()
- Number().toExponential()
- Number().toFixed()
- Number().toLocaleString() (ish)
- Number().toString()
- Number().valueOf()
- create_constructor()
- init()

Missing:
- Number().toPrecision()

refs #34

* Feature(Number): test compatability with const -> var

I don't have all the context on _why_ but all of the tests for `Number` started failing.

Upon investigation it was becuase `const` stopped acting the way I expected.
Switching my test cases from using `const` to using `var` variable declarations
made everything work again.  I don't know enough about JS to know if this is a
bug or expected behavior.  Based on changes to other tests, it is know
behavior.

Refs #34

* Changelog: Number() object.

Includes some clippy fixes.

Fixes #34
pull/212/head
Elijah Caine M. Voigt 5 years ago committed by Jason Williams
parent
commit
d725b0c769
  1. 6
      .gitignore
  2. 2
      CHANGELOG.md
  3. 2
      src/lib/builtins/mod.rs
  4. 382
      src/lib/builtins/number.rs
  5. 5
      src/lib/builtins/value.rs
  6. 27
      src/lib/exec.rs
  7. 9
      src/lib/realm.rs
  8. 8
      tests/js/test.js

6
.gitignore vendored

@ -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

2
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:

2
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

382
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::<f64>() {
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));
}
}

5
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 {

27
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::<f64>().unwrap(),
ValueData::Object(_) => {
let prim_value = self.to_primitive(value, Some("number"));
self.to_string(&prim_value)
.to_string()
.parse::<f64>()
.unwrap()
}
_ => {
// TODO: Make undefined?
f64::from(0)
}
}
}
}
#[cfg(test)]

9
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));
}
}

8
tests/js/test.js

@ -1,8 +0,0 @@
let a = {
a: true,
b() {
return 2;
}
};
console.log(a["b"]());
Loading…
Cancel
Save