console.table: match Node's column model (symbol keys, properties dedup, Set, depth, key order)

Closed
RO
2mo ago
+249-26

What

console.table's column model diverged from Node.js in five ways that change which data a table shows, not just how it looks.

console.table([{ a: 1, [Symbol("s")]: 2, [Symbol("s")]: 3 }]);
// (a) node: column a only.  bun: columns a,s (description collision, s=3 last-wins)

console.table([{ a: 1, b: 2 }], ["b", "b", "a"]);
// (b) node: columns b,a (deduped).  bun: b,b,a

console.table(new Set([1, { a: 2 }]));
// (c) node: one Values column.  bun: columns a,Values (objects exploded)

console.table([{ a: { x: { y: { z: 9 } } } }]);
// (d) node cell: "{ x: [Object] }".  bun: "{ x: { y: { z: 9 } } }"

console.table([{ b: 1 }, { a: 2, "7": 3 }]);
// (e) node columns: 7,b,a (integer-like keys first).  bun: b,7,a (first-seen)

Cause

Node builds the column union by assigning into a null-proto object keyed by ObjectKeys(row), then reading it back via ObjectKeys(map). That gives it three properties for free: symbol keys are excluded, duplicate keys dedupe, and the final order is OrdinaryOwnPropertyKeys (array-index names sorted numerically first, then string names in insertion order). Set/SetIterator go through a dedicated setlike branch that never explodes per-property columns, and cell values are inspected at depth 0.

Bun's TablePrinter (src/jsc/ConsoleObject.rs) instead kept columns in a first-seen Vec keyed by the stringified property name from a JSPropertyIterator that yields symbols, appended the properties filter verbatim, let Set rows fall through to the generic object-explode path, and formatted cells at max_depth = 5.

Fix

  • (a) Add an include_symbols flag to JSPropertyIterator (default true so every existing caller keeps its behaviour) and set it false for both the per-row column scan and the outer row scan in TablePrinter.
  • (b) Dedupe the properties filter when pre-populating columns.
  • (c) Add an is_set() branch in collect_row that routes every Set entry to the Values column.
  • (d) Set the cell formatter's max_depth to 0. Bun.inspect.table is updated to apply its depth option to max_depth (it was writing to the formatter's current depth, which happened to give the same one-level output via a different accident) with a matching default of 0.
  • (e) After collecting rows, stably partition the data columns so array-index names (0 to 2^32-2, canonical form) sort numerically first and string names stay in first-seen order, then remap each row's cells accordingly.

Verification

New column model (node compat) describe block in test/js/bun/console/console-table.test.ts covering all five cases plus the integer-index edge cases (4294967295, 01, -1, numeric sort 1/2/10). 8 of the 9 new tests fail on USE_SYSTEM_BUN=1; all 9 pass under bun bd. All pre-existing console-table.test.ts and bun-inspect-table.test.ts snapshots continue to match.

Not addressed here (cell-rendering cosmetics outside the column model): Node additionally renders cells whose value is a non-array object with more than two own keys as a bare [Object] (depth -1), and clamps arrays at maxArrayLength: 3. Those are formatter-level refinements and would need separate work in Formatter.


[review] gate passed · iteration 2 · 8 files touched

fails on main (without fix)
ASAN without fix: 10 failed, 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/console/console-table.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (be529eb73)

test/js/bun/console/console-table.test.ts:
┌───┐
│   │
├───┤
└───┘
┌───┐
│   │
├───┤
└───┘
(pass) console.table > throws when second arg is invalid [11.97ms]
(pass) console.table > expected output for: not object (number) [15.37ms]
(pass) console.table > expected output for: not object (string) [3.16ms]
(pass) console.table > expected output for: object - empty [2.28ms]
(pass) console.table > expected output for: object [3.36ms]
(pass) console.table > expected output for: array - empty [1.95ms]
(pass) console.table > expected output for: array - plain [3.80ms]
(pass) console.table > expected output for: array - object [2.67ms]
(pass) console.tabl
... (truncated)

release without fix: 1 skipped
bun test v1.4.0-canary.1 (be529eb73)

test/js/bun/console/console-table.test.ts:
┌───┐
│   │
├───┤
└───┘
┌───┐
│   │
├───┤
└───┘
(pass) console.table > throws when second arg is invalid [0.12ms]
(pass) console.table > expected output for: not object (number) [0.42ms]
(pass) console.table > expected output for: not object (string) [0.03ms]
(pass) console.table > expected output for: object - empty [0.02ms]
(pass) console.table > expected output for: object [0.03ms]
(pass) console.table > expected output for: array - empty [0.01ms]
(pass) console.table > expected output for: array - plain [0.02ms]
(pass) console.table > expected output for: array - object [0.01ms]
(pass) console.table > expected output for: array - objects with diff props [0.01ms]
(pass) console.table > expected output for: array - mixed [0.02ms]
(pass) console.table > expected output for: set [0.02ms]
(pass) console.table > expected output for: map [0.03ms]
(pass) console.table > expected output for: properties [0.02ms]
(pass) console.table > expected output for: properties - empty [0.01ms]
(pass) console.table > expected output for:
... (truncated)
passes on PR (with fix)
ASAN with fix: 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/console/console-table.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (be529eb73)

test/js/bun/console/console-table.test.ts:
┌───┐
│   │
├───┤
└───┘
┌───┐
│   │
├───┤
└───┘
(pass) console.table > throws when second arg is invalid [6.82ms]
(pass) console.table > expected output for: not object (number) [8.64ms]
(pass) console.table > expected output for: not object (string) [1.75ms]
(pass) console.table > expected output for: object - empty [1.33ms]
(pass) console.table > expected output for: object [2.12ms]
(pass) console.table > expected output for: array - empty [1.13ms]
(pass) console.table > expected output for: array - plain [2.01ms]
(pass) console.table > expected output for: array - object [1.51ms]
(pass) console.table 
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 744ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/7] cxx obj/unified/UnifiedSource-src_jsc_bindings-2.cpp.o
[2/7] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[3/7] gen cpp.rs (cppbind)
[3/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

info: checking for self-update (current version: 1.29.0)
  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

   Compiling bun_jsc v0.0.0 (/workspace/bun/sr
... (truncated)
diff hotspot
src/jsc/ConsoleObject.rs                  | 107 +++++++++++++++++++++--
 src/jsc/JSPropertyIterator.rs             |  17 ++--
 src/jsc/VirtualMachine.rs                 |   1 +
 src/jsc/bindings/JSPropertyIterator.cpp   |   4 +-
 src/runtime/api/BunObject.rs              |   4 +-
 src/runtime/api/JSTranspiler.rs           |   1 +
 src/runtime/test_runner/expect.rs         |   1 +
 test/js/bun/console/console-table.test.ts | 140 ++++++++++++++++++++++++++++--
 8 files changed, 249 insertions(+), 26 deletions(-)

gate history · 4 passed · 1 rejected · iteration 2

evidence per changed file
file                                       reads  edits  tests
src/jsc/ConsoleObject.rs                       6     13      0
src/jsc/JSPropertyIterator.rs                  3      5      0
src/jsc/VirtualMachine.rs                      1      1      0
src/jsc/bindings/JSPropertyIterator.cpp        1      1      0
src/runtime/api/BunObject.rs                   1      2      0
src/runtime/api/JSTranspiler.rs                1      1      0
src/runtime/test_runner/expect.rs              1      1      0
test/js/bun/console/console-table.test.ts      5      8      0