From 6425540950cb974f32e71a87c53e5be1cc900234 Mon Sep 17 00:00:00 2001 From: Redfire Date: Tue, 13 Feb 2024 11:37:44 +0800 Subject: [PATCH] Added Format Specifier Support to Console --- ion/src/error.rs | 2 +- ion/src/exception.rs | 2 +- ion/src/function/mod.rs | 17 +- ion/src/lib.rs | 1 + modules/src/assert/assert.rs | 2 +- runtime/src/event_loop/future.rs | 2 +- runtime/src/globals/console/format.rs | 195 ++++++++++++++++++ .../globals/{console.rs => console/mod.rs} | 162 +++++++++------ runtime/src/globals/fetch/request/options.rs | 2 +- runtime/src/globals/url/mod.rs | 8 +- 10 files changed, 310 insertions(+), 83 deletions(-) create mode 100644 runtime/src/globals/console/format.rs rename runtime/src/globals/{console.rs => console/mod.rs} (77%) diff --git a/ion/src/error.rs b/ion/src/error.rs index 3a627f8c..6796ce1b 100644 --- a/ion/src/error.rs +++ b/ion/src/error.rs @@ -222,7 +222,7 @@ impl From for ErrorReport { impl From for Error { fn from(error: E) -> Error { - Error::new(&error.to_string(), None) + Error::new(error.to_string(), None) } } diff --git a/ion/src/exception.rs b/ion/src/exception.rs index 05984aa0..3fcd85c1 100644 --- a/ion/src/exception.rs +++ b/ion/src/exception.rs @@ -76,7 +76,7 @@ impl Exception { let kind = ErrorKind::from_proto_key(IdentifyStandardInstance(handle.get())); let error = Error { kind, - message, + message: message.into(), location: Some(location), object: Some(handle.get()), }; diff --git a/ion/src/function/mod.rs b/ion/src/function/mod.rs index eecb6b09..3d93e0d5 100644 --- a/ion/src/function/mod.rs +++ b/ion/src/function/mod.rs @@ -51,13 +51,16 @@ pub fn __handle_native_constructor_result( } fn handle_unwind_error(cx: &Context, unwind_error: Box) -> bool { - if let Some(unwind) = unwind_error.downcast_ref::() { - Error::new(unwind, None).throw(cx); - } else if let Some(unwind) = unwind_error.downcast_ref::<&str>() { - Error::new(unwind, None).throw(cx); - } else { - Error::new("Unknown Panic Occurred", None).throw(cx); - forget(unwind_error); + match unwind_error.downcast::() { + Ok(unwind) => Error::new(*unwind, None).throw(cx), + Err(unwind_error) => { + if let Some(unwind) = unwind_error.downcast_ref::<&'static str>() { + Error::new(*unwind, None).throw(cx); + } else { + Error::new("Unknown Panic Occurred", None).throw(cx); + forget(unwind_error); + } + } } false } diff --git a/ion/src/lib.rs b/ion/src/lib.rs index 0be6df22..b482c0dd 100644 --- a/ion/src/lib.rs +++ b/ion/src/lib.rs @@ -11,6 +11,7 @@ extern crate mozjs; use std::result; +pub use bigint::BigInt; pub use class::ClassDefinition; pub use context::{Context, ContextInner}; pub use error::{Error, ErrorKind}; diff --git a/modules/src/assert/assert.rs b/modules/src/assert/assert.rs index 20438663..26662ff3 100644 --- a/modules/src/assert/assert.rs +++ b/modules/src/assert/assert.rs @@ -15,7 +15,7 @@ fn assert_internal(message: Option) -> Result<()> { Some(msg) => format!("Assertion Failed: {}", msg), None => String::from("Assertion Failed"), }; - Err(Error::new(&error, None)) + Err(Error::new(error, None)) } #[js_fn] diff --git a/runtime/src/event_loop/future.rs b/runtime/src/event_loop/future.rs index e62c9127..59d8debb 100644 --- a/runtime/src/event_loop/future.rs +++ b/runtime/src/event_loop/future.rs @@ -30,7 +30,7 @@ impl FutureQueue { match item { Ok(item) => results.push(item), Err(error) => { - Error::new(&error.to_string(), ErrorKind::Normal).throw(cx); + Error::new(error.to_string(), ErrorKind::Normal).throw(cx); return Err(None); } } diff --git a/runtime/src/globals/console/format.rs b/runtime/src/globals/console/format.rs new file mode 100644 index 00000000..38c72274 --- /dev/null +++ b/runtime/src/globals/console/format.rs @@ -0,0 +1,195 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +use std::fmt; +use std::fmt::{Display, Formatter, Write}; + +use mozjs::conversions::ConversionBehavior; + +use ion::{BigInt, Context, Local, Result, Value}; +use ion::conversions::FromValue; +use ion::format::{format_value, ValueDisplay}; +use ion::format::Config as FormatConfig; + +use crate::config::{Config, LogLevel}; +use crate::globals::console::INDENTS; + +pub(crate) enum FormatArg<'cx> { + String(String), + Value { value: ValueDisplay<'cx>, spaced: bool }, +} + +impl FormatArg<'_> { + pub(crate) fn spaced(&self) -> bool { + match self { + FormatArg::String(_) => false, + FormatArg::Value { spaced, .. } => *spaced, + } + } +} + +impl Display for FormatArg<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + FormatArg::String(string) => string.fmt(f), + FormatArg::Value { value, .. } => value.fmt(f), + } + } +} + +pub(crate) fn format_args<'cx>(cx: &'cx Context, args: &'cx [Value<'cx>]) -> Vec> { + fn inner<'cx>(cx: &'cx Context, args: &'cx [Value<'cx>]) -> Result>> { + if args.len() <= 1 || !args[0].get().is_string() { + return Ok(format_value_args(cx, args.iter()).collect()); + } + + let format = String::from_value(cx, &args[0], true, ())?; + + if format.is_empty() { + return Ok(format_value_args(cx, args.iter()).collect()); + } + + let mut outputs = Vec::new(); + let mut output = String::with_capacity(format.len()); + + let mut args = args.iter().skip(1).peekable(); + let mut index = 0; + + for (base, _) in format.match_indices('%') { + if base < index { + continue; + } + + output.push_str(&format[index..base]); + index = base + 1; + + match get_ascii_at(&format, index) { + next @ (Some(b'%') | None) => { + if next.is_some() || index == format.len() { + output.push('%'); + } + index += 1; + } + Some(b'0'..=b'9') | Some(b'.') | Some(b'd') | Some(b'i') | Some(b'f') => { + let arg = args.next().unwrap(); + + let (w_len, width) = parse_maximum(&format[index..]).unzip(); + index += w_len.unwrap_or(0); + let (p_len, precision) = get_ascii_at(&format, index) + .filter(|b| *b == b'.') + .and_then(|_| parse_maximum(&format[index + 1..])) + .unzip(); + index += p_len.map(|len| len + 1).unwrap_or(0); + + match get_ascii_at(&format, index) { + Some(b'd') | Some(b'i') => { + if arg.get().is_symbol() { + output.push_str("NaN"); + } else if arg.get().is_bigint() { + let bigint = BigInt::from(unsafe { Local::from_marked(&arg.get().to_bigint()) }); + output.push_str(&bigint.to_string(cx, 10).unwrap().to_owned(cx).unwrap()); + } else { + write_printf( + &mut output, + width, + precision, + i32::from_value(cx, arg, false, ConversionBehavior::Default)?, + ) + .unwrap(); + } + index += 1; + } + Some(b'f') => { + if arg.get().is_symbol() { + output.push_str("NaN"); + } else { + write_printf(&mut output, width, precision, f64::from_value(cx, arg, false, ())?) + .unwrap(); + } + index += 1; + } + _ => output.push_str(&format[base..index]), + } + } + Some(next @ (b's' | b'o' | b'O')) => { + let arg = args.next().unwrap(); + index += 1; + + match next { + b's' => output.push_str(&String::from_value(cx, arg, false, ())?), + b'o' | b'O' => { + outputs.push(FormatArg::String(output)); + output = String::with_capacity(format.len() - index); + + outputs.push(FormatArg::Value { + value: format_value(cx, FormatConfig::default().indentation(INDENTS.get()), arg), + spaced: false, + }); + } + _ => unreachable!(), + } + } + Some(b'c') => { + index += 1; + } + Some(b) => { + output.push('%'); + output.push(char::from(b)); + index += 1; + } + }; + + if args.peek().is_none() { + output.push_str(&format[index..]); + break; + } + } + + outputs.push(FormatArg::String(output)); + outputs.extend(format_value_args(cx, args)); + Ok(outputs) + } + + inner(cx, args).unwrap_or_else(|error| { + if Config::global().log_level >= LogLevel::Warn { + eprintln!("{}", error.format()); + } + Vec::new() + }) +} + +pub(crate) fn format_value_args<'cx>( + cx: &'cx Context, args: impl Iterator>, +) -> impl Iterator> { + args.map(|arg| FormatArg::Value { + value: format_value(cx, FormatConfig::default().indentation(INDENTS.get()), arg), + spaced: true, + }) +} + +fn get_ascii_at(str: &str, index: usize) -> Option { + str.as_bytes().get(index).copied().filter(|b| b.is_ascii()) +} + +fn parse_maximum(str: &str) -> Option<(usize, usize)> { + if str.is_empty() || !str.as_bytes()[0].is_ascii_digit() { + return None; + } + + let end = str.bytes().position(|b| !b.is_ascii_digit()).unwrap_or(str.len()); + Some((end, str[..end].parse().unwrap())) +} + +fn write_printf( + output: &mut String, width: Option, precision: Option, display: D, +) -> fmt::Result { + match (width, precision) { + (Some(width), Some(precision)) => write!(output, "{:1$.2$}", display, width, precision), + (Some(width), None) => write!(output, "{:1$}", display, width), + (None, Some(precision)) => write!(output, "{:.1$}", display, precision), + (None, None) => write!(output, "{}", display), + } +} diff --git a/runtime/src/globals/console.rs b/runtime/src/globals/console/mod.rs similarity index 77% rename from runtime/src/globals/console.rs rename to runtime/src/globals/console/mod.rs index 21ee68d7..2b8f4bdb 100644 --- a/runtime/src/globals/console.rs +++ b/runtime/src/globals/console/mod.rs @@ -4,6 +4,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +mod format; + use std::cell::{Cell, RefCell}; use std::collections::hash_map::{Entry, HashMap}; @@ -26,6 +28,7 @@ use ion::function::{Opt, Rest}; use crate::cache::map::find_sourcemap; use crate::config::{Config, LogLevel}; +use crate::globals::console::format::{format_args, format_value_args, FormatArg}; const ANSI_CLEAR: &str = "\x1b[1;1H"; const ANSI_CLEAR_SCREEN_DOWN: &str = "\x1b[0J"; @@ -39,23 +42,55 @@ thread_local! { static INDENTS: Cell = const { Cell::new(0) }; } -fn print_indent(is_stderr: bool) { - let indentation = INDENTS.get() as usize; - if !is_stderr { - print!("{}", indent_str(indentation)); +fn log_args(cx: &Context, args: &[Value], log_level: LogLevel) { + if log_level == LogLevel::None || args.is_empty() { + return; + } + + if args.len() == 1 { + print_args(format_value_args(cx, args.iter()), log_level); } else { - eprint!("{}", indent_str(indentation)); + print_args(format_args(cx, args).into_iter(), log_level); } } -fn print_args(cx: &Context, args: &[Value], stderr: bool) { - for value in args.iter() { - let string = format_value(cx, FormatConfig::default().indentation(INDENTS.get()), value); - if !stderr { - print!("{} ", string); - } else { - eprint!("{} ", string); - } +fn print_args<'cx>(args: impl Iterator>, log_level: LogLevel) { + if log_level == LogLevel::None { + return; + } + + let mut first = true; + + let mut prev_spaced = false; + for arg in args { + let spaced = arg.spaced(); + let to_space = !first && (prev_spaced || spaced); + match log_level { + LogLevel::Info | LogLevel::Debug => { + if to_space { + print!(" "); + } + print!("{}", arg); + } + LogLevel::Warn | LogLevel::Error => { + if to_space { + eprint!(" "); + } + eprint!("{}", arg); + } + LogLevel::None => unreachable!(), + }; + first = false; + prev_spaced = spaced; + } +} + +fn print_indent(log_level: LogLevel) { + let indentation = usize::from(INDENTS.get()); + match log_level { + LogLevel::Info | LogLevel::Debug => print!("{}", indent_str(indentation)), + LogLevel::Warn | LogLevel::Error => eprint!("{}", indent_str(indentation)), + LogLevel::None => {} } } @@ -71,8 +106,8 @@ fn get_label(label: Option) -> String { #[js_fn] fn log(cx: &Context, Rest(values): Rest) { if Config::global().log_level >= LogLevel::Info { - print_indent(false); - print_args(cx, &values, false); + print_indent(LogLevel::Info); + log_args(cx, &values, LogLevel::Info); println!(); } } @@ -80,8 +115,8 @@ fn log(cx: &Context, Rest(values): Rest) { #[js_fn] fn warn(cx: &Context, Rest(values): Rest) { if Config::global().log_level >= LogLevel::Warn { - print_indent(true); - print_args(cx, &values, true); + print_indent(LogLevel::Warn); + log_args(cx, &values, LogLevel::Warn); println!(); } } @@ -89,8 +124,8 @@ fn warn(cx: &Context, Rest(values): Rest) { #[js_fn] fn error(cx: &Context, Rest(values): Rest) { if Config::global().log_level >= LogLevel::Error { - print_indent(true); - print_args(cx, &values, true); + print_indent(LogLevel::Error); + log_args(cx, &values, LogLevel::Error); println!(); } } @@ -98,8 +133,8 @@ fn error(cx: &Context, Rest(values): Rest) { #[js_fn] fn debug(cx: &Context, Rest(values): Rest) { if Config::global().log_level == LogLevel::Debug { - print_indent(false); - print_args(cx, &values, false); + print_indent(LogLevel::Debug); + log_args(cx, &values, LogLevel::Debug); println!(); } } @@ -113,25 +148,25 @@ fn assert(cx: &Context, Opt(assertion): Opt, Rest(values): Rest) { } if values.is_empty() { - print_indent(true); + print_indent(LogLevel::Error); eprintln!("Assertion Failed"); return; } if values[0].handle().is_string() { - print_indent(true); + print_indent(LogLevel::Error); eprint!( "Assertion Failed: {} ", format_primitive(cx, FormatConfig::default(), &values[0]) ); - print_args(cx, &values[2..], true); + log_args(cx, &values[2..], LogLevel::Error); eprintln!(); return; } - print_indent(true); + print_indent(LogLevel::Error); eprint!("Assertion Failed: "); - print_args(cx, &values, true); + log_args(cx, &values, LogLevel::Error); println!(); } else { eprintln!("Assertion Failed:"); @@ -150,9 +185,9 @@ fn clear() { #[js_fn] fn trace(cx: &Context, Rest(values): Rest) { if Config::global().log_level == LogLevel::Debug { - print_indent(false); + print_indent(LogLevel::Debug); print!("Trace: "); - print_args(cx, &values, false); + log_args(cx, &values, LogLevel::Debug); println!(); let mut stack = Stack::from_capture(cx); @@ -177,7 +212,7 @@ fn group(cx: &Context, Rest(values): Rest) { INDENTS.set(INDENTS.get().min(u16::MAX - 1) + 1); if Config::global().log_level >= LogLevel::Info { - print_args(cx, &values, false); + log_args(cx, &values, LogLevel::Info); println!(); } } @@ -190,20 +225,14 @@ fn groupEnd() { #[js_fn] fn count(Opt(label): Opt) { let label = get_label(label); - COUNT_MAP.with_borrow_mut(|counts| match counts.entry(label.clone()) { - Entry::Vacant(v) => { - let val = v.insert(1); - if Config::global().log_level >= LogLevel::Info { - print_indent(false); - println!("{}: {}", label, val); - } - } - Entry::Occupied(mut o) => { - let val = o.insert(o.get() + 1); - if Config::global().log_level >= LogLevel::Info { - print_indent(false); - println!("{}: {}", label, val); - } + COUNT_MAP.with_borrow_mut(|counts| { + let count = match counts.entry(label.clone()) { + Entry::Vacant(v) => *v.insert(1), + Entry::Occupied(mut o) => o.insert(o.get() + 1), + }; + if Config::global().log_level >= LogLevel::Info { + print_indent(LogLevel::Info); + println!("{}: {}", label, count); } }); } @@ -211,16 +240,16 @@ fn count(Opt(label): Opt) { #[js_fn] fn countReset(Opt(label): Opt) { let label = get_label(label); - COUNT_MAP.with_borrow_mut(|counts| match counts.entry(label.clone()) { - Entry::Vacant(_) => { - if Config::global().log_level >= LogLevel::Error { - print_indent(true); + COUNT_MAP.with_borrow_mut(|counts| match counts.get_mut(&label) { + Some(count) => { + *count = 0; + } + None => { + if Config::global().log_level >= LogLevel::Warn { + print_indent(LogLevel::Warn); eprintln!("Count for {} does not exist", label); } } - Entry::Occupied(mut o) => { - o.insert(0); - } }); } @@ -232,8 +261,8 @@ fn time(Opt(label): Opt) { v.insert(Utc::now()); } Entry::Occupied(_) => { - if Config::global().log_level >= LogLevel::Error { - print_indent(true); + if Config::global().log_level >= LogLevel::Warn { + print_indent(LogLevel::Warn); eprintln!("Timer {} already exists", label); } } @@ -247,15 +276,15 @@ fn timeLog(cx: &Context, Opt(label): Opt, Rest(values): Rest) { Some(start) => { if Config::global().log_level >= LogLevel::Info { let duration = Utc::now().timestamp_millis() - start.timestamp_millis(); - print_indent(false); + print_indent(LogLevel::Info); print!("{}: {}ms ", label, duration); - print_args(cx, &values, false); + log_args(cx, &values, LogLevel::Info); println!(); } } None => { - if Config::global().log_level >= LogLevel::Error { - print_indent(true); + if Config::global().log_level >= LogLevel::Warn { + print_indent(LogLevel::Warn); eprintln!("Timer {} does not exist", label); } } @@ -265,22 +294,21 @@ fn timeLog(cx: &Context, Opt(label): Opt, Rest(values): Rest) { #[js_fn] fn timeEnd(Opt(label): Opt) { let label = get_label(label); - TIMER_MAP.with_borrow_mut(|timers| match timers.entry(label.clone()) { - Entry::Vacant(_) => { - if Config::global().log_level >= LogLevel::Error { - print_indent(true); - eprintln!("Timer {} does not exist", label); - } - } - Entry::Occupied(o) => { + TIMER_MAP.with_borrow_mut(|timers| match timers.remove(&label) { + Some(start_time) => { if Config::global().log_level >= LogLevel::Info { - let (_, start_time) = o.remove_entry(); let duration = Utc::now().timestamp_millis() - start_time.timestamp_millis(); - print_indent(false); + print_indent(LogLevel::Info); print!("{}: {}ms - Timer Ended", label, duration); println!(); } } + None => { + if Config::global().log_level >= LogLevel::Warn { + print_indent(LogLevel::Warn); + eprintln!("Timer {} does not exist", label); + } + } }); } @@ -401,7 +429,7 @@ fn table(cx: &Context, data: Value, Opt(columns): Opt>) -> Result<() println!("{}", indent_all_by((indents * 2) as usize, table.render())) } else if Config::global().log_level >= LogLevel::Info { - print_indent(true); + print_indent(LogLevel::Info); println!( "{}", format_value(cx, FormatConfig::default().indentation(indents), &data) diff --git a/runtime/src/globals/fetch/request/options.rs b/runtime/src/globals/fetch/request/options.rs index 5b75ff84..6d13a2fc 100644 --- a/runtime/src/globals/fetch/request/options.rs +++ b/runtime/src/globals/fetch/request/options.rs @@ -34,7 +34,7 @@ impl FromStr for Referrer { if referrer.is_empty() { Ok(Referrer::NoReferrer) } else { - let url = Url::parse(referrer).map_err(|e| Error::new(&e.to_string(), ErrorKind::Type))?; + let url = Url::parse(referrer).map_err(|e| Error::new(e.to_string(), ErrorKind::Type))?; if url.scheme() == "about" && url.path() == "client" { Ok(Referrer::Client) diff --git a/runtime/src/globals/url/mod.rs b/runtime/src/globals/url/mod.rs index b17d231e..83328409 100644 --- a/runtime/src/globals/url/mod.rs +++ b/runtime/src/globals/url/mod.rs @@ -42,7 +42,7 @@ impl URL { let url = Url::options() .base_url(base.as_ref()) .parse(&input) - .map_err(|error| Error::new(&error.to_string(), None))?; + .map_err(|error| Error::new(error.to_string(), None))?; let search_params = Box::new(URLSearchParams::new(url.query_pairs().into_owned().collect())); search_params.url.as_ref().unwrap().set(this.handle().get()); @@ -99,7 +99,7 @@ impl URL { self.url = url; Ok(()) } - Err(error) => Err(Error::new(&error.to_string(), None)), + Err(error) => Err(Error::new(error.to_string(), None)), } } @@ -136,7 +136,7 @@ impl URL { Ordering::Equal => { let port = match segments[1].parse::() { Ok(port) => Ok(port), - Err(error) => Err(Error::new(&error.to_string(), None)), + Err(error) => Err(Error::new(error.to_string(), None)), }?; Ok((segments[0], Some(port))) } @@ -153,7 +153,7 @@ impl URL { #[ion(set)] pub fn set_hostname(&mut self, hostname: String) -> Result<()> { - self.url.set_host(Some(&hostname)).map_err(|error| Error::new(&error.to_string(), None)) + self.url.set_host(Some(&hostname)).map_err(|error| Error::new(error.to_string(), None)) } #[ion(get)]