Skip to content

util.inspect, Console#table: read Map and Set iterators from storage - #42359

Open
robobun wants to merge 5 commits into
mainfrom
robobun/c4e079fe/preview-entries-native
Open

robobun wants to merge 5 commits into
mainfrom
robobun/c4e079fe/preview-entries-native

Conversation

@robobun

@robobun robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • On bun 1.4.2, util.inspect of an advanced Map or Set iterator also prints the entries it already yielded. new Console(stream).table(map.entries()) prints pairs in one Values column and leaves the iterator done. Node.js prints what is left, in Key and Values columns, and does not move the iterator.
  • Both never return when %MapIteratorPrototype%.next is replaced by a function that never reports done. No user reported this. A code read found it (console: do not hang on an iterator that never ends #42264).
  • The cause is previewEntries (src/js/internal/util/inspect.js:2786): it copies the collection with Array.from. Console#table (src/js/builtins/ConsoleObject.ts:697) ran a for-of over the iterator.

Fix

  • previewEntries is now native (src/jsc/bindings/UtilInspect.cpp). It reads what the iterator has left from the storage of the collection. It calls no user code, does not move the iterator, copies at most maxArrayLength entries and counts the rest.
  • Console#table calls it for iterators, with Node's five lines. Each key and value is now a cell, so this PR carries the isBuffer import line of node:console: fix Console#table throwing on object cells, Maps and Sets #33452. Without it an object cell throws.
  • Scope: util.inspect and new Console(). The native formatter (console.log, Bun.inspect) still advances an iterator it prints.
  • Verified: internal-inspect.test.js, console-table-iterators.test.ts. bun 1.4.2 fails 8 of 9 new tests (1 control). Self-reviewed: 14 concerns raised, 6 addressed, the rest in Notes.

Background

  • A Map or Set iterator holds the collection, a pointer to its storage table and an entry index. A rehash or clear() links the old table to the new one.
  • JSMap::Helper::transitAndNext is the read-only step that next() uses: follow those links, skip deleted entries, return the next entry. The helper loops over it.
  • A Worker uses the JS Console class as its global console.
Notes

Repro of the hang

const util = require("node:util");
const m = new Map([["a", 1]]);
let nexts = 0;
Object.getPrototypeOf(m.entries()).next = () => {
  if (++nexts > 5000) { console.log("RUNAWAY nexts=" + nexts); process.exit(2); }
  return { value: ["k", "v"], done: false };
};
console.log(util.inspect(m.entries()));
console.log("nexts=" + nexts);
// bun 1.4.2:  RUNAWAY nexts=5001        (without the guard: spins, memory grows)
// node 26.3:  [Map Entries] { [ 'a', 1 ] }   nexts=0
// this PR:    [Map Entries] { [ 'a', 1 ] }   nexts=0

Without tampering

const it = new Map([["a", 1], ["b", 2], ["c", 3]]).entries();
it.next();
util.inspect(it);
// bun 1.4.2:         [Map Entries] { [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] }
// node and this PR:  [Map Entries] { [ 'b', 2 ], [ 'c', 3 ] }
new Console(process.stdout).table(it);
// bun 1.4.2:         one Values column with [ 'b', 2 ] and [ 'c', 3 ], then it.next() is { done: true }
// node and this PR:  Key and Values columns, then it.next() is { value: [ 'b', 2 ], done: false }

Comparison with Node.js v26.3.0

A script of about 50 shapes prints byte-identical util.inspect output on this branch and on Node: all four Map and all four Set iterator kinds, advanced and exhausted iterators, delete(), clear() and a rehash while the iterator is open, an iterator made before the Map had any entry, an iterator from a node:vm context, maxArrayLength of 0, 1, 2, -1, Infinity and null, showHidden, sorted, compact: false, nested values and extra own keys. Console#table matches Node on the same shapes except for cell alignment, which #32619 covers. Node's own test-console-table.js expects the Key and Values columns for map.entries().

isBuffer

Console#table formats an object cell with isArray(v) = $isJSArray(v) || $isTypedArrayView(v) || isBuffer(v), and isBuffer came from require("node:buffer").isBuffer, which is undefined. So an object cell that is not an array throws TypeError: isBuffer is not a function. #33452 fixes that import. Before this PR table(map.entries()) did not reach it, because each row was the [key, value] pair, an array. With Node's code path each key and value is a cell of its own, so this PR needs the same line (const { Buffer: { isBuffer } } = require("node:buffer")). The line is identical to #33452, so the PR that lands second rebases without a conflict.

The self-review asked for #33452 to land first, by itself, because the bug is worse than its title says. A Worker uses this JS Console as its global console. console.table([{ createdAt: new Date(0) }]) inside a node:worker_threads Worker throws undefined is not a function on 1.4.2 and the worker exits with code 1. I agree with that order. I kept the line here because this PR is not correct without it, and I do not set the merge order.

Console#table(map) is unchanged

The first revision of this PR also stopped the for-of over a Map or a Set after size steps. The self-review falsified that with quick-lru 7.3.0. It caps size at maxSize, and its iterator also yields the older generation: size is 3 and the iterator yields 5. Node.js and bun 1.4.2 print 5 rows. The bound printed 3. The bound is gone, and those two loops are Node's again. The test prints every entry that a Map subclass yields, also past its size pins the two-generation shape.

So new Console().table(map) with an endless Map.prototype[Symbol.iterator] still never returns, on bun and on Node alike. If a bound is wanted there, one rule has to cover this code and forEachLimited in #42264.

Same class, not fixed here

  • util.inspect(Object.setPrototypeOf(new Map([[1, 2]]), null)) with a replaced %MapIteratorPrototype%.next never returns on bun and on Node. For a null-prototype Map formatRaw hands formatMap the result of Map.prototype.entries(), whose size is undefined, so there is no bound.
  • console.log(it), console.table(it) and Bun.inspect(it) go through the native formatter, which advances it. Node does not. The helper in this PR could serve the native formatter too. That is a follow-up.
  • util.inspect(weakMap, { showHidden: true }) still prints no entries (node:util does not implement WeakSet correctly #28044).

Overlap with open PRs

Self-review

14 concerns survived the review. Removed the size bound and its test (2 concerns). Rewrote this description around the differences that need no tampering, the missing user report and the scope (4 concerns). Not taken: move the isBuffer line out and stack this PR on #33452 and on the alignment PR (4 concerns), for the reason above. 4 concerns say to keep the design: the native read that does not advance is Node's own, and the Console#table lines are the ones #35391 has. 1 concern (WeakMap and WeakSet contents) is a follow-up.

Other checks

  • The new tests pass with BUN_JSC_validateExceptionChecks=1.
  • A stress script (60 rounds: 300-entry Map and Set, five iterators at different positions, deletes, a rehash, clear(), Bun.gc(true) between two util.inspect calls of the same iterator) gives stable output under the ASAN debug build, and what util.inspect shows equals what the iterator then yields.
  • Ran test/js/node/util/, test/js/node/console/, test/js/bun/console/ and test/js/bun/util/inspect.test.js on the debug build. util-inspect-long-running and the parseArgs stress test hit the 5 s timeout under ASAN when the host is loaded. Neither reaches this code.
  • Also ran test-util-inspect-*.js, test-util.js, test-util-types.js, test-console-instance.js, test-console-methods.js, test-console-group.js, test-console-with-frozen-intrinsics.js and test-repl-inspect-defaults.js from test/js/node/test/parallel/.
  • util.inspect of an iterator over a large Map no longer copies the Map. It copies maxArrayLength entries and counts the rest in native code.

[human-review] gate passed · iteration 0 · 6 files touched

fails on main (without fix)
ASAN without fix: 8 failed, 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/console/console-table-iterators.test.ts test/js/node/util/node-inspect-tests/internal-inspect.test.js
bun test v1.4.3 (4ff919377)

test/js/node/console/console-table-iterators.test.ts:
25 | test.concurrent("console.Console#table renders Map and Set iterators", async () => {
26 |   const { stdout, exitCode } = await run(/* js */ `
27 |     c.table(new Map([["a", 1], ["b", 2]]).entries());
28 |     c.table(new Set([7, 8]).values());
29 |   `);
30 |   expect(stdout).toMatchInlineSnapshot(`
                      ^
error: expect(received).toMatchInlineSnapshot(expected)

  
- "┌───────────────────┬─────┬────────┐
- │ (iteration index) │ Key │ Values │
- ├───────────────────┼─────┼────────┤
- │         0         │ 'a' │   1    │
- │         1         │ 'b' │   2    │
- └───────────────────┴─────┴─────
... (truncated)

release without fix: 1 skipped
bun test v1.4.3-canary.1 (29be5f2e4)

test/js/node/console/console-table-iterators.test.ts:
(pass) console.Console#table shows what a Map or Set iterator has left and does not advance it [40.58ms]
(pass) console.Console#table renders Map and Set iterators [50.23ms]
(pass) console.Console#table formats object keys and values of a Map or Set iterator [42.52ms]
(pass) console.Console#table prints every entry that a Map subclass yields, also past its size [40.69ms]
(pass) console.Console#table does not run a replaced next() of a Map or Set iterator [52.23ms]

test/js/node/util/node-inspect-tests/internal-inspect.test.js:
(pass) no assertion failures [5.02ms]
(pass) boxed BigInt/Symbol with no prototype are still formatted as boxed primitives [0.29ms]
(pass) util.inspect reports the non-index keys of an array [1.59ms]
(pass) util.inspect reports the non-index keys of a typed array [0.53ms]
(pass) util.inspect of a large Array does not visit every element [12.24ms]
(pass) util.inspect of a large Uint8Array does not visit every element [4.56ms]
(pass) util.inspect shows what a Map or Set iterator has left [5.44ms]
(pass) util.inspect of a Map or Set iterator follows delete
... (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/pr_gate.xml" test/js/node/console/console-table-iterators.test.ts test/js/node/util/node-inspect-tests/internal-inspect.test.js
bun test v1.4.3 (4ff919377)

test/js/node/console/console-table-iterators.test.ts:
(pass) console.Console#table shows what a Map or Set iterator has left and does not advance it [2222.42ms]
(pass) console.Console#table renders Map and Set iterators [2364.55ms]
(pass) console.Console#table formats object keys and values of a Map or Set iterator [2290.43ms]
(pass) console.Console#table does not run a replaced next() of a Map or Set iterator [2370.29ms]
(pass) console.Console#table prints every entry that a Map subclass yields, also past its size [2366.96ms]

test/js/node/util/node-inspect-tests/internal-inspect.test.js:
(pass) no assertion failures [235.63ms]
(pass) boxed BigInt/Symbol with no prototype are still formatted as boxed primitives [15.86ms]
(pass) util.inspect reports the non-index keys of an array [90.00ms]
(pass) util.inspect reports the non-index keys of a typed array [32.32ms]
(pass) util.inspect of a large Ar
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1016ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/138] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/release/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[2/138] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[3/138] gen cpp.rs (cppbind)
[4/138] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 243 extern-C blocks audited
[5/138] gen JS modules (bundle-modules)
Preprocess modules (13626ms)
Bundle modules (97ms)
Postprocesss modules (172ms)
Bundle Functions (671ms)
Generate Code (61ms)

[14.64s] Bundled "src/js" for production
  2598 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[5/137] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_sys v0.0.0 (/workspace/bun/src/sys)
�[1m�[92m   Compiling�[0m bun_uws_sys v0.0.0 (/workspace/bun/src/uws_sys)
�[1m�[92m   Compiling�[0m bun_perf v0.0.0 (/works
... (truncated)
diff hotspot
src/js/builtins/ConsoleObject.ts                   |  13 +-
 src/js/internal/util/inspect.js                    |  49 ++---
 src/jsc/bindings/UtilInspect.cpp                   |  82 +++++++++
 src/jsc/bindings/UtilInspect.h                     |   3 +
 .../node/console/console-table-iterators.test.ts   | 200 ++++++++++++++++++---
 .../node-inspect-tests/internal-inspect.test.js    | 190 +++++++++++++++++++-
 6 files changed, 478 insertions(+), 59 deletions(-)

gate history · 3 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
src/js/builtins/ConsoleObject.ts                              4      5     33
src/js/internal/util/inspect.js                               5      6     33
src/jsc/bindings/UtilInspect.cpp                              3      3     34
src/jsc/bindings/UtilInspect.h                                1      1     27
test/js/node/console/console-table-iterators.test.ts          4      4     23
…s/node/util/node-inspect-tests/internal-inspect.test.js      1      2     16

util.inspect(map.entries()) and new Console().table(map.entries()) walked
the iterated collection through the iterator protocol with no bound
(Array.from in previewEntries, a for-of in table). A replaced
%MapIteratorPrototype%.next or Map.prototype[Symbol.iterator] that never
reports done made them spin forever.

previewEntries is now native (UtilInspect.cpp), like
internalBinding('util').previewEntries in Node.js. It reads what the
iterator has left from the storage of the collection. It runs no user
code, does not advance the iterator, and copies out at most
maxArrayLength entries. As in Node.js, the output starts at the position
of the iterator and Console#table shows Key and Values columns for
map.entries().

Console#table still reads a Map or a Set through its iterator, so that a
subclass can keep its entries anywhere, but stops after size steps, like
formatMap in util.inspect.

Each key and value of an iterator is now a table cell of its own, so an
object cell reaches isBuffer(). Import it from Buffer, where it lives:
require('node:buffer').isBuffer is undefined.
@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: e764a95c-2669-4b30-80e3-e219dbbcc795

📥 Commits

Reviewing files that changed from the base of the PR and between 158ff6c and 204b594.

📒 Files selected for processing (6)
  • src/js/builtins/ConsoleObject.ts
  • src/js/internal/util/inspect.js
  • src/jsc/bindings/UtilInspect.cpp
  • src/jsc/bindings/UtilInspect.h
  • test/js/node/console/console-table-iterators.test.ts
  • test/js/node/util/node-inspect-tests/internal-inspect.test.js

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Map and Set iterator inspection now uses a native, non-advancing preview helper. util.inspect and console.table consume previewed entries and counts. Tests cover mutations, truncation, cross-realm iterators, overridden methods, and Map subclasses.

Changes

Iterator previewing and formatting

Layer / File(s) Summary
Native iterator preview implementation
src/jsc/bindings/UtilInspect.cpp, src/jsc/bindings/UtilInspect.h
Adds previewEntries to inspect remaining Map and Set iterator storage without advancing iterators. It handles limits, mutations, iterator kinds, invalid inputs, and allocation failures.
Inspect formatting integration
src/js/internal/util/inspect.js, test/js/node/util/node-inspect-tests/internal-inspect.test.js
Routes iterator inspection through the native preview helper and uses total remaining counts for truncation. Tests cover state preservation, mutations, rehashing, cross-realm iterators, options, and overridden methods.
Console table iterator materialization
src/js/builtins/ConsoleObject.ts, test/js/node/console/console-table-iterators.test.ts
Materializes Map and Set iterators before table processing. Tests cover remaining entries, object cells, overridden methods, and Map subclasses with additional entries.

Suggested reviewers: dylan-conway

Priority: ⚪ Not assessed

Merge Risk: ⚪ Minimal · up to 204b5

The iterator preview integration and its consumers are covered by focused tests, with no actionable merge-blocking issue identified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main changes to util.inspect and Console#table for Map and Set iterators.
Description check ✅ Passed The description explains the problem, implementation, scope, limitations, and verification results. It does not use the exact template headings, but it provides the required information and is complet…

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on bun 1.4.2 and on main with this script. The guard ends the process so that it does not hang.

const util = require("node:util");
const m = new Map([["a", 1]]);
let nexts = 0;
Object.getPrototypeOf(m.entries()).next = () => {
  if (++nexts > 5000) { console.log("RUNAWAY nexts=" + nexts); process.exit(2); }
  return { value: ["k", "v"], done: false };
};
console.log(util.inspect(m.entries()));
console.log("nexts=" + nexts);
  • bun 1.4.2: RUNAWAY nexts=5001
  • node v26.3.0 and this branch: [Map Entries] { [ 'a', 1 ] }, then nexts=0

The same guard with new Console(process.stdout).table(map.entries()) also prints RUNAWAY on 1.4.2 and ends on this branch.

Without any tampering, const it = map.entries(); it.next(); util.inspect(it) prints the entry that it already yielded on 1.4.2. Node and this branch print only what is left.

Tests: bun 1.4.2 fails 8 of the 9 new tests in test/js/node/util/node-inspect-tests/internal-inspect.test.js and test/js/node/console/console-table-iterators.test.ts (1 is a control). bun bd test passes all of them.

CI

The diff is green. Build #114384 on the first head passed on every lane. The two tests of this PR pass on every lane of every build.

Builds #114632 and #114654 are red because of one test that this PR does not touch: test/js/bun/http/serve-pending-promise-abort-leak.test.ts on the debian 13 x64-asan lane (expect(alive).toBe(0), received 1, a Bun.serve response stream that is not collected after a client abort). It also fails on main, and it is reported there. Every other failure in those builds passed on a retry: bun-lock, bun-install-registry, bun-patch, bake/deinitialization, next-pages dev-server-ssr-100, spawn-maxbuf, bun-inspector-protocol, and three tests that fail only in the parallel batch.

The review threads are all resolved. The PR is ready for a maintainer.

PR: #42359

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Because it adds a new native path that walks JSC's ordered-hash-table storage directly (Helper::transitAndNext, tryGetStorage, sentinel handling), a human familiar with those JSC internals should also take a look.

What was reviewed:

  • jsFunctionPreviewEntries: throw-scope discipline, MarkedArgumentBuffer + hasOverflowed() OOM, RETURN_IF_EXCEPTION/RELEASE_AND_RETURN placement, dynamicDowncast, and NaN/±Infinity/negative handling on the limit — all correct.
  • previewRemainingEntries: no allocation or user-JS entry during the walk, so no GC hazard between reading next.key/next.value and appending to the rooted buffer; the uncheckedDowncast is guarded by the sentinel check.
  • inspect.js: maxArrayLength: null is normalized to Infinity at line 548 before formatIterator runs, so the new MathMax(ctx.maxArrayLength, 0) limit is never accidentally 0; deleted primordials are all now unused.
  • Tests: runaway guard exits 2 (fails for the right reason instead of hanging), stdout asserted before exitCode, await using + concurrent drain, snapshots match Node's shape.
Extended reasoning...

Overview

This PR replaces the JS-level previewEntries in util.inspect (which materialized an entire Map/Set via Array.from, ran user-overridable iterator protocol, and ignored iterator position) with a native C++ implementation in src/jsc/bindings/UtilInspect.cpp that reads the iterator's remaining entries directly from JSC's ordered-hash-table storage via Helper::transitAndNext. It wires the same helper into console.Console#table, fixes a latent isBuffer destructuring bug, bounds the for...of over real Maps/Sets by .size, and threads a separate length through formatSetIterInner/formatMapIterInner so the "... N more items" count is accurate for advanced iterators under maxArrayLength. Roughly 90 lines of new C++, ~40 lines of JS delta, and ~400 lines of new tests across two files.

Security risks

None identified. The native function is only reachable from built-in JS ($newCppFunction), takes a Map/Set iterator (validated with dynamicDowncast, else throws a TypeError), and a numeric limit that is defensively clamped (NaN/negative → 0, ≥ UINT32_MAX → UINT32_MAX, non-number → default). It calls no user code, performs no allocation inside the storage walk, and roots every collected JSValue in a MarkedArgumentBuffer with an overflow → OOM check. No untrusted parsing, no filesystem or network surface.

Level of scrutiny

Medium-high. The JS and test changes are straightforward and follow repo conventions closely (primordial-safe calls, $newCppFunction, dead-primordial cleanup, await using + concurrent drain, stdout-before-exitCode, runaway guard so a regression fails rather than hangs). The C++ is small and hits every REVIEW.md checkbox for JSC bindings. What lifts scrutiny above "approve" is that previewRemainingEntries depends on JSC-internal invariants: that tryGetStorage() is null only for the never-stepped case, that after ruling out orderedHashTableSentinel() the cell is safe to uncheckedDowncast to Collection::Storage, and that transitAndNext from a stale entry() correctly chains through rehash/clear tables without touching freed memory. These are the same invariants the iterator's own next() relies on and the tests exercise them (delete/clear/rehash/growth, cross-realm, exhausted iterator), but a reviewer who knows the JSC ordered-hash-table lifecycle should confirm the contract.

Other factors

The PR description documents byte-identical output against Node v26.3.0 across ~50 shapes, a pass under BUN_JSC_validateExceptionChecks=1, and an ASAN stress loop with Bun.gc(true) between inspects. The bug hunt ran to dry_streak with no findings and no ruled-out candidates. I spot-checked the maxArrayLength: null path (normalized to Infinity at inspect.js:548 before formatIterator, so the new limit argument is Infinity, not 0), the default-parameter back-compat for formatWeakSet/formatWeakMap callers, and that every deleted primordial (ArrayFrom, ArrayPrototypeFlat, MapPrototypeValues, MapPrototypeKeys, SetPrototypeEntries) is genuinely unused after the change. No CODEOWNERS entry covers these paths. The one open design point — .size is read via a user-overridable getter, and a subclass reporting size = Infinity with an infinite iterator still hangs — is called out in the PR as intentionally Node-compatible and matches formatMap in util.inspect.

@robobun

robobun commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

For the reviewer who checks the JSC side: previewRemainingEntries is JSMapIterator::nextWithAdvance / JSSetIterator::nextWithAdvance (JSMapIterator.h, JSSetIterator.h) in a loop, without its three writes (setEntry, setStorage, markClosed). It relies on the same three things that function relies on.

  • tryGetStorage() returns null only for an iterator that was made while the collection had no storage. nextWithAdvance then reads iteratedObject()->storage(), and the helper does the same. The late case in the test follows delete(), clear() and growth of the collection covers it.
  • A closed iterator holds vm.orderedHashTableSentinel() in its storage field. The helper returns before the downcast in that case, as nextWithAdvance does.
  • Helper::transitAndNext(vm, storage, entry) only reads. It follows the nextTable links that a rehash or a clear() leaves in an obsolete table, and it adjusts the index for the entries that were deleted. An obsolete table stays alive because the storage field of the iterator points at it, and each obsolete table links to the next one. The walk allocates nothing, so no GC can run between two steps.

The helper uses these through the JSC headers of the pinned WebKit build. If a WebKit upgrade changes them, UtilInspect.cpp fails to compile. It cannot fail at run time.

The failed claude-find-issues check is not related to this diff: the action ended after 685 ms with is_error: true before it did any work.

@robobun

robobun commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:42 AM PT - Sep 12th, 2026

❌ @robobun, your commit 495e5be has 1 failures in Build #114654 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 42359

That installs a local version of the PR into your bun-42359 executable, so you can run:

bun-42359 --bun

A subclass can yield more entries than size reports. quick-lru caps size
at maxSize while its iterator also yields the older generation, so
table(lru) printed 3 of 5 rows. Node.js and bun 1.4.2 print all 5.

The loops over a Map or a Set are Node's again. The iterator path keeps
the native previewEntries. A test pins the two-generation shape.
Comment thread src/js/internal/util/inspect.js Outdated
Comment thread src/jsc/bindings/UtilInspect.cpp Outdated
Comment thread src/jsc/bindings/UtilInspect.cpp Outdated
@robobun

robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Two pushes after a self-review. The description above is rewritten to match.

204b594 removes the size bound on Console#table(map) and Console#table(set). The first revision stopped the for-of over a Map or a Set after size steps, so that a replaced Map.prototype[Symbol.iterator] could not run forever. The review checked that against quick-lru 7.3.0, the package I named as the reason to keep the iterator path. It caps size at maxSize, and its iterator also yields the older generation.

const lru = new QuickLRU({ maxSize: 3 });
for (let i = 0; i < 5; i++) lru.set("k" + i, i);
lru.size;        // 3
[...lru].length; // 5
new Console(process.stdout).table(lru);
// node 26.3 and bun 1.4.2: 5 rows.  first revision of this PR: 3 rows

The two loops are Node's again, and a test pins the two-generation shape. So table(map) with an endless Map.prototype[Symbol.iterator] still never returns, on bun and on Node alike. #42264 bounds the native console.table by size with a marker row. If a bound is wanted in this JS path too, one rule has to cover both.

2c8f81e cuts the three comment blocks that comment-cop flagged to one line each.

Not taken from the review: split the isBuffer import line out and land it first through #33452. I agree that #33452 deserves to land first. A Worker uses this JS Console as its global console, so console.table([{ createdAt: new Date(0) }]) inside a node:worker_threads Worker throws on 1.4.2 and the worker exits with code 1. This PR keeps the identical line because table(map.entries()) with an object value throws without it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread test/js/node/console/console-table-iterators.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant