feature(display): Implement pretty printing of arguments inside of console.log (closes #1437)#4853
feature(display): Implement pretty printing of arguments inside of console.log (closes #1437)#4853gluzandii wants to merge 4 commits intoboa-dev:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses #1437 by improving how console.log formats and displays certain JavaScript values—especially arguments objects—while also refactoring the core::boa_engine::value::display module into smaller, type-focused files.
Changes:
- Refactors
core/engine/src/value/displayfrom a single large file into multiple modules (array/map/set/object/value/etc.). - Adds specialized formatting for
Arguments,RegExp(/pattern/flags), andDate(ISO 8601 /Invalid Date). - Introduces “compact” formatting used when printing nested values within
Arguments, plus new console tests covering the new output.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| core/runtime/src/console/tests.rs | Adds regression tests for logging arguments, RegExp, and Date. |
| core/engine/src/value/mod.rs | Removes now-unneeded HashSet import after display refactor. |
| core/engine/src/value/display/mod.rs | New display module entry point wiring submodules and ValueDisplay. |
| core/engine/src/value/display/value.rs | Main dispatch for value formatting; adds compact formatting + Date/RegExp handling. |
| core/engine/src/value/display/arguments.rs | Implements [Arguments] { ... } formatting and compact formatting of individual entries. |
| core/engine/src/value/display/array.rs | Array formatting + compact array formatting for nested contexts. |
| core/engine/src/value/display/map.rs | Map formatting + compact map formatting. |
| core/engine/src/value/display/set.rs | Set formatting + compact set formatting. |
| core/engine/src/value/display/object.rs | Object formatting helpers + compact plain-object formatting. |
| core/engine/src/value/display/typed_array.rs | Adds TypedArray formatting. |
| core/engine/src/value/display/primitives.rs | Extracts float formatting helper (-0 handling). |
| core/engine/src/value/display.rs | Removes the old monolithic display implementation (replaced by the new module layout). |
| core/engine/src/builtins/regexp/mod.rs | Exposes original source/flags for display formatting. |
| core/engine/src/builtins/date/mod.rs | Adds ISO 8601 display helper for Date. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if let Some(value) = x | ||
| .borrow() | ||
| .properties() | ||
| .get(&i.into()) | ||
| .and_then(|x| x.value().cloned()) | ||
| { | ||
| super::value::log_value_compact(f, &value, depth + 1, print_internals, encounters)?; |
There was a problem hiding this comment.
In log_array_compact, elements backed by accessor descriptors are currently treated as missing because it only reads descriptor.value(). That makes accessor elements display as <empty> even when present. Consider matching on DescriptorKind (like log_array_to) and printing a [Getter]/[Setter] marker (or similar) for accessors, reserving <empty> only for actual holes (no property at that index).
| if let Some(value) = x | |
| .borrow() | |
| .properties() | |
| .get(&i.into()) | |
| .and_then(|x| x.value().cloned()) | |
| { | |
| super::value::log_value_compact(f, &value, depth + 1, print_internals, encounters)?; | |
| if let Some(desc) = x.borrow().properties().get(&i.into()) { | |
| match desc.kind() { | |
| DescriptorKind::Data { value, .. } => { | |
| if let Some(value) = value { | |
| super::value::log_value_compact( | |
| f, | |
| value, | |
| depth + 1, | |
| print_internals, | |
| encounters, | |
| )?; | |
| } else { | |
| // Data descriptor with no value is treated as `undefined`, matching `log_array_to`. | |
| super::value::log_value_compact( | |
| f, | |
| &JsValue::undefined(), | |
| depth + 1, | |
| print_internals, | |
| encounters, | |
| )?; | |
| } | |
| } | |
| DescriptorKind::Accessor { get, set } => { | |
| let display = match (get.is_some(), set.is_some()) { | |
| (true, true) => "[Getter/Setter]", | |
| (true, false) => "[Getter]", | |
| (false, true) => "[Setter]", | |
| _ => "<empty>", | |
| }; | |
| f.write_str(display)?; | |
| } | |
| DescriptorKind::Generic => { | |
| unreachable!("found generic descriptor in array") | |
| } | |
| } |
| let len = x | ||
| .borrow() | ||
| .properties() | ||
| .get(&js_string!("length").into()) | ||
| .expect("array object must have 'length' property") | ||
| .value() | ||
| .and_then(JsValue::as_number) | ||
| .map(|n| n as i32) | ||
| .unwrap_or_default(); |
There was a problem hiding this comment.
Array length is read as a JS number and cast to i32. This can overflow/turn negative for large arrays (length can be up to 2^32-1), leading to incorrect output and skipping elements because the 0..len loop becomes empty. Prefer parsing the length as u32/usize (or clamping) to keep iteration and Array({len}) accurate for large lengths.
| .get(&js_string!("length").into()) | ||
| .and_then(|d| d.value().cloned()) | ||
| .and_then(|v| v.as_number()) | ||
| .map(|n| n as i32) |
There was a problem hiding this comment.
Arguments length is cast to i32 and then iterated with for i in 0..len. While typical argument counts are small, length is conceptually a non-negative integer; casting from a JS number to i32 can overflow/turn negative and make the loop skip printing. Prefer using u32/usize (or clamping) for len to ensure correct behavior for large lengths.
| .map(|n| n as i32) | |
| .map(|n| n.max(0.0).min(i32::MAX as f64) as i32) |
| write!( | ||
| f, | ||
| "/{}/{}", | ||
| regexp.original_source().to_std_string_escaped(), |
There was a problem hiding this comment.
RegExp display uses original_source() verbatim inside /.../flags. For regexes created via new RegExp("a/b"), original_source will contain an unescaped /, so the output becomes /a/b/ (ambiguous / not a valid literal form). Consider escaping / (and other characters required by RegExp.prototype.toString, e.g. line terminators) when formatting so the printed form matches JS /.*/ syntax reliably.
| write!( | |
| f, | |
| "/{}/{}", | |
| regexp.original_source().to_std_string_escaped(), | |
| let source = regexp | |
| .original_source() | |
| .to_std_string_escaped() | |
| .replace('/', r"\/"); | |
| write!( | |
| f, | |
| "/{}/{}", | |
| source, |
| (false, true) => "Getter", | ||
| _ => "No Getter/Setter", | ||
| }; | ||
| write!(f, "{key}: {display}")?; |
There was a problem hiding this comment.
When formatting accessor properties, the key gets printed twice. print_obj_value_props already wrote the indented {key}: prefix, but the accessor branch writes {key}: {display} again, producing output like foo: foo: Getter. Write only the accessor marker after the prefix (e.g. just {display}) to avoid duplication.
| write!(f, "{key}: {display}")?; | |
| write!(f, "{display}")?; |
| // FIXME: handle accessor descriptors | ||
| let val = x | ||
| .borrow() | ||
| .properties() | ||
| .get(&i.into()) | ||
| .and_then(|d| d.value().cloned()); | ||
|
|
||
| match val { | ||
| Some(v) => { | ||
| write!( | ||
| f, | ||
| " {i}: {}", | ||
| CompactValue { | ||
| value: &v, | ||
| print_internals | ||
| } | ||
| )?; | ||
| } | ||
| None => { |
There was a problem hiding this comment.
log_arguments_to reads indexed values directly from the object’s stored property descriptors (properties().get(...).value()), which can diverge from the actual value for MappedArguments (values are sourced from the environment via the parameter map). This also treats accessor descriptors as <empty> even when the property exists. Prefer detecting MappedArguments and using its get(index) for mapped entries (falling back to the stored value when unmapped), and for non-data descriptors print a [Getter]/[Setter] marker similar to array/object formatting instead of <empty>.
| // FIXME: handle accessor descriptors | |
| let val = x | |
| .borrow() | |
| .properties() | |
| .get(&i.into()) | |
| .and_then(|d| d.value().cloned()); | |
| match val { | |
| Some(v) => { | |
| write!( | |
| f, | |
| " {i}: {}", | |
| CompactValue { | |
| value: &v, | |
| print_internals | |
| } | |
| )?; | |
| } | |
| None => { | |
| // Retrieve the property descriptor once so we can distinguish between | |
| // data and accessor descriptors and, for mapped arguments, prefer the | |
| // live mapped value over the stored descriptor value. | |
| let desc = x | |
| .borrow() | |
| .properties() | |
| .get(&i.into()) | |
| .cloned(); | |
| match desc { | |
| Some(desc) => { | |
| if !desc.is_data_descriptor() { | |
| // Accessor descriptor: print a marker instead of `<empty>`. | |
| let has_get = desc.get().is_some(); | |
| let has_set = desc.set().is_some(); | |
| let marker = match (has_get, has_set) { | |
| (true, true) => "[Getter/Setter]", | |
| (true, false) => "[Getter]", | |
| (false, true) => "[Setter]", | |
| // An accessor descriptor with neither getter nor setter | |
| // is effectively empty. | |
| (false, false) => "<empty>", | |
| }; | |
| write!(f, " {i}: {marker}")?; | |
| } else { | |
| // Data descriptor: start from the stored value. | |
| // For mapped arguments, prefer the live mapped value when present. | |
| let mut value = desc.value().cloned(); | |
| // If this is a `MappedArguments` object, consult its mapping for `i`. | |
| if let Some(mapped) = x.borrow().as_mapped_arguments() { | |
| if let Some(mapped_value) = mapped.get(i as u32) { | |
| value = Some(mapped_value); | |
| } | |
| } | |
| match value { | |
| Some(v) => { | |
| write!( | |
| f, | |
| " {i}: {}", | |
| CompactValue { | |
| value: &v, | |
| print_internals | |
| } | |
| )?; | |
| } | |
| None => { | |
| write!(f, " {i}: <empty>")?; | |
| } | |
| } | |
| } | |
| } | |
| None => { | |
| // No property descriptor at all for this index. |
Test262 conformance changes
Tested main commit: |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4853 +/- ##
==========================================
+ Coverage 47.24% 57.05% +9.81%
==========================================
Files 476 562 +86
Lines 46892 60721 +13829
==========================================
+ Hits 22154 34645 +12491
- Misses 24738 26076 +1338 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
070c167 to
c10f628
Compare
Modularize the display module, so that each formatting method for each type (array, set etc) have own file, which makes it easier to read and maintain. Argument object logging via console.log, is formatted in a compact manner (Similar to Node).
When printing arguments of function, it now prints the date in ISO 8601 format (similar to Node) And regex is now printed with the pattern and its flags, similar to how node does it. Add support for pretty printing TypedArray objects
- Break up monolithic value::display into submodules for arguments, arrays, objects, primitives, and typed arrays - Delegate formatting in value.rs to the new specialized formatters - Update existing display call sites to match the new module structure
Adds support for output of WeakMap and WeakSet types. Map and Set logging also displays number of entires now. fix(dispay tests): Gone back to old formatting for some cases for test conformance. Map uses unicode arrow instead of =>, and Map and Set now dont display how many items the have. Fix clippy CI/CD errors.
c10f628 to
e7d8227
Compare
|
I force pushed since my branch was very behind the main branch, hence updated its base commit to the latest. |
This pull request closes: #1437.
Firstly, it restructures the
core::boa_engine::value::displaymodule into different files. Initially the display module was a single file with everything in it, this made it hard to read and maintain, I have split it up into different modules inside of display, each pertaining to a certain data type for formatting (array, map, set etc).Added compact formatting of certain Js types, so that it can be used in console logging the arguments object:
Along with the compact formatting methods that are used for logging arguments, there are also methods for regular logging (not compact) for the same data types as well, present in the same file.
This PR also added ability to log:
The above 2 changes have been added for formatting in general (not only restricted to how arguments object is displayed)
Added method in value.rs called
log_value_compactwhich is used for compact formatting of objects. This is what is internally used when theconsole.log(arguments)is called.Compact formatting is the same as regular formatting but smartly compacts deeply nested types (like objects, arrays to just mentioning what type they are). It does this by keeping track of the depth. The current max depth is 2.
This can be changed by modifying the constant
COMPACT_DEPTH_LIMITconstant inside of value.rs.