console.table: match Node's column model (symbol keys, properties dedup, Set, depth, key order)
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_symbolsflag toJSPropertyIterator(defaulttrueso every existing caller keeps its behaviour) and set itfalsefor both the per-row column scan and the outer row scan inTablePrinter. - (b) Dedupe the
propertiesfilter when pre-populating columns. - (c) Add an
is_set()branch incollect_rowthat routes every Set entry to the Values column. - (d) Set the cell formatter's
max_depthto 0.Bun.inspect.tableis updated to apply itsdepthoption tomax_depth(it was writing to the formatter's currentdepth, 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)
[1m[92m Compiling[0m 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