diff --git a/.vscode/launch.json b/.vscode/launch.json index ca9e95998e..ac65200653 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", +} ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0f38bb8b77..6ffc51f9f4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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 + } } ] } diff --git a/boa/src/builtins/console.rs b/boa/src/builtins/console.rs deleted file mode 100644 index 623d709ba9..0000000000 --- a/boa/src/builtins/console.rs +++ /dev/null @@ -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 -/// -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 = - 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 = FromIterator::from_iter( - args.iter() - .map(|x| from_value::(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 -} diff --git a/boa/src/builtins/console/mod.rs b/boa/src/builtins/console/mod.rs new file mode 100644 index 0000000000..835a1755a3 --- /dev/null +++ b/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, + timer_map: HashMap, + groups: Vec, +} + +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(args: &[Value], index: usize) -> Option { + args.get(index) + .cloned() + .map(|s| from_value::(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::(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::(data, arg_index).unwrap_or_default(); + formatted.push_str(&format!("{}", arg)); + arg_index += 1; + } + /* float */ + 'f' => { + let arg = get_arg_at_index::(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::(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: +pub fn assert(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let assertion = get_arg_at_index::(args, 0).unwrap_or_default(); + + if !assertion { + let mut args: Vec = args.iter().skip(1).cloned().collect(); + let message = "Assertion failed".to_string(); + if args.is_empty() { + args.push(to_value::(message)); + } else if !args[0].is_string() { + args.insert(0, to_value::(message)); + } else { + let concat = format!("{}: {}", message, args[0]); + args[0] = to_value::(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: +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: +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: +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: +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: +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: +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: ".to_string()), + state, + ) + }); + } + + Ok(Gc::new(ValueData::Undefined)) +} + +/// `console.warn(...data)` +/// +/// Prints a JavaScript values with "warn" logLevel. +/// +/// More information: +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: +pub fn count(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let label = get_arg_at_index::(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: +pub fn count_reset(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let label = get_arg_at_index::(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: +pub fn time(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let label = get_arg_at_index::(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: +pub fn time_log(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let label = get_arg_at_index::(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: +pub fn time_end(this: &Value, args: &[Value], _: &mut Interpreter) -> ResultValue { + let label = get_arg_at_index::(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: +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: +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: +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 +} diff --git a/boa/src/builtins/console/tests.rs b/boa/src/builtins/console/tests.rs new file mode 100644 index 0000000000..613cc2e1b8 --- /dev/null +++ b/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") +} diff --git a/boa/src/builtins/value/mod.rs b/boa/src/builtins/value/mod.rs index 03d676c6d9..fc276f8143 100644 --- a/boa/src/builtins/value/mod.rs +++ b/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) -> usize {