Skip to content

feature(display): Implement pretty printing of arguments inside of console.log (closes #1437)#4853

Open
gluzandii wants to merge 4 commits intoboa-dev:mainfrom
gluzandii:feature/prettyprintarguments
Open

feature(display): Implement pretty printing of arguments inside of console.log (closes #1437)#4853
gluzandii wants to merge 4 commits intoboa-dev:mainfrom
gluzandii:feature/prettyprintarguments

Conversation

@gluzandii
Copy link

This pull request closes: #1437.

Firstly, it restructures the core::boa_engine::value::display module 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:

  • Regex -> In the form "/{regex pattern}/{flags}"
  • Date -> In the ISO 8601 format

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_compact which is used for compact formatting of objects. This is what is internally used when the console.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_LIMIT constant inside of value.rs.

@gluzandii gluzandii requested a review from a team as a code owner March 4, 2026 06:48
Copilot AI review requested due to automatic review settings March 4, 2026 06:48
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/display from a single large file into multiple modules (array/map/set/object/value/etc.).
  • Adds specialized formatting for Arguments, RegExp (/pattern/flags), and Date (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.

Comment on lines +97 to +103
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)?;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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")
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +20
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();
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
.get(&js_string!("length").into())
.and_then(|d| d.value().cloned())
.and_then(|v| v.as_number())
.map(|n| n as i32)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.map(|n| n as i32)
.map(|n| n.max(0.0).min(i32::MAX as f64) as i32)

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +122
write!(
f,
"/{}/{}",
regexp.original_source().to_std_string_escaped(),
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
write!(
f,
"/{}/{}",
regexp.original_source().to_std_string_escaped(),
let source = regexp
.original_source()
.to_std_string_escaped()
.replace('/', r"\/");
write!(
f,
"/{}/{}",
source,

Copilot uses AI. Check for mistakes.
(false, true) => "Getter",
_ => "No Getter/Setter",
};
write!(f, "{key}: {display}")?;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
write!(f, "{key}: {display}")?;
write!(f, "{display}")?;

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +58
// 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 => {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Mar 4, 2026

Test262 conformance changes

Test result main count PR count difference
Total 52,963 52,963 0
Passed 49,687 49,687 0
Ignored 2,262 2,262 0
Failed 1,014 1,014 0
Panics 0 0 0
Conformance 93.81% 93.81% 0.00%

Tested main commit: 34ca7f1f404c2712d6897c3e39e5f5e981ce9ed6
Tested PR commit: e7d82273a2600713ae1db74dce49e541afb26a82
Compare commits: 34ca7f1...e7d8227

@codecov
Copy link

codecov bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 41.12554% with 272 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.05%. Comparing base (6ddc2b4) to head (c10f628).
⚠️ Report is 742 commits behind head on main.

Files with missing lines Patch % Lines
core/engine/src/value/display/value.rs 30.55% 100 Missing ⚠️
core/engine/src/value/display/object.rs 37.27% 69 Missing ⚠️
core/engine/src/value/display/typed_array.rs 0.00% 33 Missing ⚠️
core/engine/src/value/display/array.rs 40.81% 29 Missing ⚠️
core/engine/src/value/display/map.rs 47.05% 18 Missing ⚠️
core/engine/src/value/display/set.rs 50.00% 15 Missing ⚠️
core/engine/src/value/display/arguments.rs 89.65% 3 Missing ⚠️
core/engine/src/value/display/mod.rs 40.00% 3 Missing ⚠️
core/engine/src/builtins/date/mod.rs 89.47% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gluzandii gluzandii force-pushed the feature/prettyprintarguments branch 3 times, most recently from 070c167 to c10f628 Compare March 4, 2026 14:37
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.
@gluzandii gluzandii force-pushed the feature/prettyprintarguments branch from c10f628 to e7d8227 Compare March 8, 2026 05:47
@gluzandii
Copy link
Author

I force pushed since my branch was very behind the main branch, hence updated its base commit to the latest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pretty print Arguments objects

2 participants