Browse Source

console functions (#315)

* Exec do..while tests.

* Parser do..while test.

* Do..while loop parser and exec implementation

* rustfmt fixes

* Update boa/src/exec/mod.rs

Co-Authored-By: HalidOdat <halidodat@gmail.com>

* Use expect(), make expect() skip newlines

* rustmf fixes

* Revert "Use expect(), make expect() skip newlines"

This reverts commit 517c4d0e06.

* Cargo Test Build task and Debug Test (Windows) launcher

* First attempt at console.assert implementation. Tests are just for
debugging. Run `cargo test console_assert -- --nocapture` to see stderror
messages.

* Refactoring - remove unnecessary map, variable rename.

* Update boa/src/builtins/console.rs

changes from HalidOdat

Co-Authored-By: HalidOdat <halidodat@gmail.com>

* Documentation fixes

* Remove space from documentation comment

* Update boa/src/builtins/console.rs

Simplify message printing.

Co-Authored-By: Iban Eguia <razican@protonmail.ch>

* Update boa/src/builtins/console.rs

Improve docs.

Co-Authored-By: Iban Eguia <razican@protonmail.ch>

* Update boa/src/builtins/console.rs

Improve getting of assertion result.

Co-Authored-By: Iban Eguia <razican@protonmail.ch>

* rustfmt

* console.count() and console.countReset() implementation

* Console state as native rust type, temporarily placed in Realm.

* console.time[,Log,End] methods implementation

* ConsoleState as internal state in console object.

* Fix merge mess

* Formatter function, get_arg_at_index moved out to function

* Fix merge mess, pt. 2

* console.group* functions

* Moved console code to its own subdirectory, formatter tests, fixed utf-8
handling.

* Most functions implemented.

* Basic logger and logLevel implementation

* console.group uses logger

* console.dir (and at the same time dirxml) implementation

* Make builtins::value::display_obj(...) public

* Update boa/src/builtins/console/mod.rs

Co-Authored-By: HalidOdat <halidodat@gmail.com>

* Update boa/src/builtins/console/mod.rs

Co-Authored-By: HalidOdat <halidodat@gmail.com>

* Update boa/src/builtins/value/mod.rs

Co-Authored-By: Iban Eguia <razican@protonmail.ch>

Co-authored-by: HalidOdat <halidodat@gmail.com>
Co-authored-by: Iban Eguia <razican@protonmail.ch>
pull/355/head
Radek Krahl 4 years ago committed by GitHub
parent
commit
7abb94abf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      .vscode/launch.json
  2. 55
      .vscode/tasks.json
  3. 42
      boa/src/builtins/console.rs
  4. 428
      boa/src/builtins/console/mod.rs
  5. 66
      boa/src/builtins/console/tests.rs
  6. 2
      boa/src/builtins/value/mod.rs

18
.vscode/launch.json vendored

@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
@ -23,10 +24,23 @@
"program": "${workspaceFolder}/target/debug/boa_cli.exe",
"cwd": "${workspaceFolder}",
"sourceFileMap": {
"/rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8": "${env:USERPROFILE}/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust"
"/rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8": "${env:USERPROFILE}/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust",
"/rustc/b8cedc00407a4c56a3bda1ed605c6fc166655447": "${env:USERPROFILE}/.rustup/toolchains/stable-x86_64-pc-windows-msvc/lib/rustlib/src/rust"
},
"stopAtEntry": false,
"symbolSearchPath": "https://msdl.microsoft.com/download/symbols"
}
},
{
"name": "(Windows) Run Test Debugger",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/target/debug/boa-ea5ed1ef3ee0cbe1.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"preLaunchTask": "Cargo Test Build",
}
]
}

55
.vscode/tasks.json vendored

@ -7,8 +7,13 @@
"type": "process",
"label": "Cargo Run",
"command": "cargo",
"args": ["run", "./tests/js/test.js"],
"problemMatcher": ["$rustc"],
"args": [
"run",
"./tests/js/test.js"
],
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
@ -21,8 +26,15 @@
"type": "process",
"label": "Get Tokens",
"command": "cargo",
"args": ["run", "--", "-t=Debug", "./tests/js/test.js"],
"problemMatcher": ["$rustc"],
"args": [
"run",
"--",
"-t=Debug",
"./tests/js/test.js"
],
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
@ -35,8 +47,15 @@
"type": "process",
"label": "Get AST",
"command": "cargo",
"args": ["run", "--", "-a=Debug", "./tests/js/test.js"],
"problemMatcher": ["$rustc"],
"args": [
"run",
"--",
"-a=Debug",
"./tests/js/test.js"
],
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
@ -49,8 +68,12 @@
"type": "process",
"label": "Cargo Test",
"command": "cargo",
"args": ["test"],
"problemMatcher": ["$rustc"],
"args": [
"test"
],
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "test",
"isDefault": true
@ -58,6 +81,22 @@
"presentation": {
"clear": true
}
},
{
"type": "process",
"label": "Cargo Test Build",
"command": "cargo",
"args": [
"test",
"--no-run"
],
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

42
boa/src/builtins/console.rs

@ -1,42 +0,0 @@
#![allow(clippy::print_stdout)]
use crate::{
builtins::{
function::NativeFunctionData,
value::{from_value, log_string_from, to_value, ResultValue, Value, ValueData},
},
exec::Interpreter,
};
use gc::Gc;
use std::{iter::FromIterator, ops::Deref};
/// Print a javascript value to the standard output stream
/// <https://console.spec.whatwg.org/#logger>
pub fn log(_: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
// Welcome to console.log! The output here is what the developer sees, so its best matching through value types and stringifying to the correct output
// The input is a vector of Values, we generate a vector of strings then
// pass them to println!
let args: Vec<String> =
FromIterator::from_iter(args.iter().map(|x| log_string_from(x.deref(), false)));
println!("{}", args.join(" "));
Ok(Gc::new(ValueData::Undefined))
}
/// Print a javascript value to the standard error stream
pub fn error(_: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let args: Vec<String> = FromIterator::from_iter(
args.iter()
.map(|x| from_value::<String>(x.clone()).expect("Could not convert value to String")),
);
eprintln!("{}", args.join(" "));
Ok(Gc::new(ValueData::Undefined))
}
/// Create a new `console` object
pub fn create_constructor(global: &Value) -> Value {
let console = ValueData::new_obj(Some(global));
console.set_field_slice("log", to_value(log as NativeFunctionData));
console.set_field_slice("error", to_value(error as NativeFunctionData));
console.set_field_slice("exception", to_value(error as NativeFunctionData));
console
}

428
boa/src/builtins/console/mod.rs

@ -0,0 +1,428 @@
#![allow(clippy::print_stdout)]
#[cfg(test)]
mod tests;
use crate::{
builtins::{
function::NativeFunctionData,
object::InternalState,
value::{display_obj, from_value, to_value, FromValue, ResultValue, Value, ValueData},
},
exec::Interpreter,
};
use gc::Gc;
use std::{collections::HashMap, time::SystemTime};
#[derive(Debug, Default)]
pub struct ConsoleState {
count_map: HashMap<String, u32>,
timer_map: HashMap<String, u128>,
groups: Vec<String>,
}
impl ConsoleState {
fn new() -> Self {
ConsoleState {
count_map: HashMap::new(),
timer_map: HashMap::new(),
groups: vec![],
}
}
}
impl InternalState for ConsoleState {}
#[derive(Debug)]
pub enum LogMessage {
Log(String),
Info(String),
Warn(String),
Error(String),
}
fn get_arg_at_index<T: FromValue + Default>(args: &[Value], index: usize) -> Option<T> {
args.get(index)
.cloned()
.map(|s| from_value::<T>(s).expect("Convert error"))
}
pub fn logger(msg: LogMessage, console_state: &ConsoleState) {
let indent = 2 * console_state.groups.len();
match msg {
LogMessage::Error(msg) => {
eprintln!("{:>width$}", msg, width = indent);
}
LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => {
println!("{:>width$}", msg, width = indent);
}
}
}
pub fn formatter(data: &[Value]) -> String {
let target = get_arg_at_index::<String>(data, 0).unwrap_or_default();
match data.len() {
0 => String::new(),
1 => target,
_ => {
let mut formatted = String::new();
let mut arg_index = 1;
let mut chars = target.chars();
while let Some(c) = chars.next() {
if c == '%' {
let fmt = chars.next().unwrap_or('%');
match fmt {
/* integer */
'd' | 'i' => {
let arg = get_arg_at_index::<i32>(data, arg_index).unwrap_or_default();
formatted.push_str(&format!("{}", arg));
arg_index += 1;
}
/* float */
'f' => {
let arg = get_arg_at_index::<f64>(data, arg_index).unwrap_or_default();
formatted.push_str(&format!("{number:.prec$}", number = arg, prec = 6));
arg_index += 1
}
/* object, FIXME: how to render this properly? */
'o' | 'O' => {
let arg = data.get(arg_index).cloned().unwrap_or_default();
formatted.push_str(&format!("{}", arg));
arg_index += 1
}
/* string */
's' => {
let arg =
get_arg_at_index::<String>(data, arg_index).unwrap_or_default();
formatted.push_str(&arg);
arg_index += 1
}
'%' => formatted.push('%'),
/* TODO: %c is not implemented */
c => {
formatted.push('%');
formatted.push(c);
}
}
} else {
formatted.push(c);
};
}
/* unformatted data */
for rest in data.iter().skip(arg_index) {
formatted.push_str(&format!(" {}", rest))
}
formatted
}
}
}
/// `console.assert(condition, ...data)`
///
/// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there
/// were no arguments.
///
/// More information: <https://console.spec.whatwg.org/#assert>
pub fn assert(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let assertion = get_arg_at_index::<bool>(args, 0).unwrap_or_default();
if !assertion {
let mut args: Vec<Value> = args.iter().skip(1).cloned().collect();
let message = "Assertion failed".to_string();
if args.is_empty() {
args.push(to_value::<String>(message));
} else if !args[0].is_string() {
args.insert(0, to_value::<String>(message));
} else {
let concat = format!("{}: {}", message, args[0]);
args[0] = to_value::<String>(concat);
}
this.with_internal_state_ref(|state| {
logger(LogMessage::Error(formatter(&args[..])), state)
});
}
Ok(Gc::new(ValueData::Undefined))
}
/// `console.clear()`
///
/// Removes all groups and clears console if possible.
///
/// More information: <https://console.spec.whatwg.org/#clear>
pub fn clear(this: &Value, _: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_mut(|state: &mut ConsoleState| {
state.groups.clear();
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.debug(...data)`
///
/// Prints a JavaScript values with "debug" logLevel.
///
/// More information: <https://console.spec.whatwg.org/#debug>
pub fn debug(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref(|state| logger(LogMessage::Log(formatter(&args[..])), state));
Ok(Gc::new(ValueData::Undefined))
}
/// `console.error(...data)`
///
/// Prints a JavaScript values with "error" logLevel.
///
/// More information: <https://console.spec.whatwg.org/#error>
pub fn error(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref(|state| logger(LogMessage::Error(formatter(&args[..])), state));
Ok(Gc::new(ValueData::Undefined))
}
/// `console.info(...data)`
///
/// Prints a JavaScript values with "info" logLevel.
///
/// More information: <https://console.spec.whatwg.org/#info>
pub fn info(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref(|state| logger(LogMessage::Info(formatter(&args[..])), state));
Ok(Gc::new(ValueData::Undefined))
}
/// `console.log(...data)`
///
/// Prints a JavaScript values with "log" logLevel.
///
/// More information: <https://console.spec.whatwg.org/#log>
pub fn log(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref(|state| logger(LogMessage::Log(formatter(&args[..])), state));
Ok(Gc::new(ValueData::Undefined))
}
/// `console.trace(...data)`
///
/// Prints a stack trace with "trace" logLevel, optionally labelled by data.
///
/// More information: <https://console.spec.whatwg.org/#trace>
pub fn trace(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
if !args.is_empty() {
this.with_internal_state_ref(|state| logger(LogMessage::Log(formatter(&args[..])), state));
/* TODO: get and print stack trace */
this.with_internal_state_ref(|state| {
logger(
LogMessage::Log("Not implemented: <stack trace>".to_string()),
state,
)
});
}
Ok(Gc::new(ValueData::Undefined))
}
/// `console.warn(...data)`
///
/// Prints a JavaScript values with "warn" logLevel.
///
/// More information: <https://console.spec.whatwg.org/#warn>
pub fn warn(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_ref(|state| logger(LogMessage::Warn(formatter(&args[..])), state));
Ok(Gc::new(ValueData::Undefined))
}
/// `console.count(label)`
///
/// Prints number of times the function was called with that particular label.
///
/// More information: <https://console.spec.whatwg.org/#count>
pub fn count(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let label = get_arg_at_index::<String>(args, 0).unwrap_or_else(|| "default".to_string());
this.with_internal_state_mut(|state: &mut ConsoleState| {
let msg = format!("count {}:", &label);
let c = state.count_map.entry(label).or_insert(0);
*c += 1;
logger(LogMessage::Info(format!("{} {}", msg, c)), state);
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.countReset(label)`
///
/// Resets the counter for label.
///
/// More information: <https://console.spec.whatwg.org/#countreset>
pub fn count_reset(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let label = get_arg_at_index::<String>(args, 0).unwrap_or_else(|| "default".to_string());
this.with_internal_state_mut(|state: &mut ConsoleState| {
state.count_map.remove(&label);
logger(LogMessage::Warn(format!("countReset {}", label)), state);
});
Ok(Gc::new(ValueData::Undefined))
}
/// Returns current system time in ms.
fn system_time_in_ms() -> u128 {
let now = SystemTime::now();
now.duration_since(SystemTime::UNIX_EPOCH)
.expect("negative duration")
.as_millis()
}
/// `console.time(label)`
///
/// Starts the timer for given label.
///
/// More information: <https://console.spec.whatwg.org/#time>
pub fn time(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let label = get_arg_at_index::<String>(args, 0).unwrap_or_else(|| "default".to_string());
this.with_internal_state_mut(|state: &mut ConsoleState| {
if state.timer_map.get(&label).is_some() {
logger(
LogMessage::Warn(format!("Timer '{}' already exist", label)),
state,
);
} else {
let time = system_time_in_ms();
state.timer_map.insert(label, time);
}
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.timeLog(label, ...data)`
///
/// Prints elapsed time for timer with given label.
///
/// More information: <https://console.spec.whatwg.org/#timelog>
pub fn time_log(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let label = get_arg_at_index::<String>(args, 0).unwrap_or_else(|| "default".to_string());
this.with_internal_state_mut(|state: &mut ConsoleState| {
if let Some(t) = state.timer_map.get(&label) {
let time = system_time_in_ms();
let mut concat = format!("{}: {} ms", label, time - t);
for msg in args.iter().skip(1) {
concat = concat + " " + &msg.to_string();
}
logger(LogMessage::Log(concat), state);
} else {
logger(
LogMessage::Warn(format!("Timer '{}' doesn't exist", label)),
state,
);
}
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.timeEnd(label)`
///
/// Removes the timer with given label.
///
/// More information: <https://console.spec.whatwg.org/#timeend>
pub fn time_end(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let label = get_arg_at_index::<String>(args, 0).unwrap_or_else(|| "default".to_string());
this.with_internal_state_mut(|state: &mut ConsoleState| {
if let Some(t) = state.timer_map.remove(&label) {
let time = system_time_in_ms();
logger(
LogMessage::Info(format!("{}: {} ms - timer removed", label, time - t)),
state,
);
} else {
logger(
LogMessage::Warn(format!("Timer '{}' doesn't exist", label)),
state,
);
}
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.group(...data)`
///
/// Adds new group with name from formatted data to stack.
///
/// More information: <https://console.spec.whatwg.org/#group>
pub fn group(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
let group_label = formatter(args);
this.with_internal_state_mut(|state: &mut ConsoleState| {
logger(LogMessage::Info(format!("group: {}", &group_label)), state);
state.groups.push(group_label);
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.groupEnd(label)`
///
/// Removes the last group from the stack.
///
/// More information: <https://console.spec.whatwg.org/#groupend>
pub fn group_end(this: &Value, _: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_mut(|state: &mut ConsoleState| {
state.groups.pop();
});
Ok(Gc::new(ValueData::Undefined))
}
/// `console.dir(item, options)`
///
/// Prints info about item
///
/// More information: <https://console.spec.whatwg.org/#dir>
pub fn dir(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue {
this.with_internal_state_mut(|state: &mut ConsoleState| {
logger(
LogMessage::Info(display_obj(
args.get(0).unwrap_or(&Gc::new(ValueData::Undefined)),
true,
)),
state,
);
});
Ok(Gc::new(ValueData::Undefined))
}
/// Create a new `console` object
pub fn create_constructor(global: &Value) -> Value {
let console = ValueData::new_obj(Some(global));
console.set_field_slice("assert", to_value(assert as NativeFunctionData));
console.set_field_slice("clear", to_value(clear as NativeFunctionData));
console.set_field_slice("debug", to_value(debug as NativeFunctionData));
console.set_field_slice("error", to_value(error as NativeFunctionData));
console.set_field_slice("info", to_value(info as NativeFunctionData));
console.set_field_slice("log", to_value(log as NativeFunctionData));
console.set_field_slice("trace", to_value(trace as NativeFunctionData));
console.set_field_slice("warn", to_value(warn as NativeFunctionData));
console.set_field_slice("exception", to_value(error as NativeFunctionData));
console.set_field_slice("count", to_value(count as NativeFunctionData));
console.set_field_slice("countReset", to_value(count_reset as NativeFunctionData));
console.set_field_slice("group", to_value(group as NativeFunctionData));
console.set_field_slice("groupCollapsed", to_value(group as NativeFunctionData));
console.set_field_slice("groupEnd", to_value(group_end as NativeFunctionData));
console.set_field_slice("time", to_value(time as NativeFunctionData));
console.set_field_slice("timeLog", to_value(time_log as NativeFunctionData));
console.set_field_slice("timeEnd", to_value(time_end as NativeFunctionData));
console.set_field_slice("dir", to_value(dir as NativeFunctionData));
console.set_field_slice("dirxml", to_value(dir as NativeFunctionData));
console.set_internal_state(ConsoleState::new());
console
}

66
boa/src/builtins/console/tests.rs

@ -0,0 +1,66 @@
use crate::builtins::{console::formatter, value::ValueData};
use gc::Gc;
#[test]
fn formatter_no_args_is_empty_string() {
assert_eq!(formatter(&[]), "")
}
#[test]
fn formatter_empty_format_string_is_empty_string() {
let val = Gc::new(ValueData::String("".to_string()));
let res = formatter(&[val]);
assert_eq!(res, "");
}
#[test]
fn formatter_format_without_args_renders_verbatim() {
let val = [Gc::new(ValueData::String("%d %s %% %f".to_string()))];
let res = formatter(&val);
assert_eq!(res, "%d %s %% %f");
}
#[test]
fn formatter_empty_format_string_concatenates_rest_of_args() {
let val = [
Gc::new(ValueData::String("".to_string())),
Gc::new(ValueData::String("to powinno zostać".to_string())),
Gc::new(ValueData::String("połączone".to_string())),
];
let res = formatter(&val);
assert_eq!(res, " to powinno zostać połączone");
}
#[test]
fn formatter_utf_8_checks() {
let val = [
Gc::new(ValueData::String(
"Są takie chwile %dą %są tu%sów %привет%ź".to_string(),
)),
Gc::new(ValueData::Integer(123)),
Gc::new(ValueData::Number(1.23)),
Gc::new(ValueData::String("ł".to_string())),
];
let res = formatter(&val);
assert_eq!(res, "Są takie chwile 123ą 1.23ą tułów %привет%ź");
}
#[test]
fn formatter_trailing_format_leader_renders() {
let val = [
Gc::new(ValueData::String("%%%%%".to_string())),
Gc::new(ValueData::String("|".to_string())),
];
let res = formatter(&val);
assert_eq!(res, "%%% |")
}
#[test]
fn formatter_float_format_works() {
let val = [
Gc::new(ValueData::String("%f".to_string())),
Gc::new(ValueData::Number(3.1415)),
];
let res = formatter(&val);
assert_eq!(res, "3.141500")
}

2
boa/src/builtins/value/mod.rs

@ -767,7 +767,7 @@ pub(crate) fn log_string_from(x: &ValueData, print_internals: bool) -> String {
}
/// A helper function for specifically printing object values
fn display_obj(v: &ValueData, print_internals: bool) -> String {
pub(crate) fn display_obj(v: &ValueData, print_internals: bool) -> String {
// A simple helper for getting the address of a value
// TODO: Find a more general place for this, as it can be used in other situations as well
fn address_of<T>(t: &T) -> usize {

Loading…
Cancel
Save