Skip to content

console,util: route every %s through one formatPercentS (Node's rule) - #36141

Open
robobun wants to merge 12 commits into
mainfrom
farm/1ac0d408/unify-percent-s
Open

console,util: route every %s through one formatPercentS (Node's rule)#36141
robobun wants to merge 12 commits into
mainfrom
farm/1ac0d408/unify-percent-s

Conversation

@robobun

@robobun robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Reproduction

import util from 'node:util';
console.log(util.format('%s', Buffer.from('ab')));      // util.format path
console.log('%s', new Map([['k', 'v']]));               // native console path
try { console.log('%s', Symbol('q')); } catch (e) { console.log('THREW:' + e.name); }
console.log('%s', -0);
console.log('%s', 42n);
console.log('%s', Object.create(null));
Node 26 Bun before Bun after
util.format('%s', Buffer.from('ab')) ab <Buffer 61 62> ab
util.format('%s', new URLSearchParams('a=1')) a=1 URLSearchParams {} a=1
console.log('%s', new Map(...)) Map(1) { 'k' => 'v' } [object Map] Map(1) { 'k' => 'v' }
console.log('%s', Symbol('q')) Symbol(q) TypeError Symbol(q)
console.log('%s', -0) -0 0 -0
console.log('%s', 42n) 42n 42 42n
console.log('%s', Object.create(null)) [Object: null prototype] {} TypeError [Object: null prototype] {}

Cause

Bun shipped three %s behaviors:

  • util.format('%s', v) and new Console(stream).log('%s', v) both ran the JS formatWithOptionsInternal port of Node's decision tree.
  • The default global console.log('%s', v) ran the native formatter's PercentTag::S, which forced engine ToString(v) for every value: throws on Symbol, loses -0 and the BigInt n suffix, and prints [object Object] / [object Map].

Separately, hasBuiltInToString decided whether an object's inherited toString is "built-in" by checking the owner prototype's constructor name against builtInObjects, which was populated from Object.getOwnPropertyNames(globalThis). Node captures that set at bootstrap when only ECMAScript intrinsics are present; Bun captures it after every host global is already installed, so Bun's set was a superset containing Buffer, URL, URLSearchParams, Request, Blob, ... and util.format('%s', buf) took the inspect branch instead of String(buf).

Fix

One source of truth, Node's architecture:

  1. builtInObjects is the hardcoded list of ECMA-262 language-level constructor names (what Node's bootstrap scrape actually yields). Host prototypes (Buffer, URL, URLSearchParams, ...) now take the String() path through %s.
  2. The case-115 body is hoisted into an exported formatPercentS(inspectOptions, arg) in internal/util/inspect.js; formatWithOptionsInternal calls it.
  3. The native PercentTag::S keeps a primitive-string fast path and delegates every other value to that same JS routine via a lazily cached LazyPropertyOfGlobalObject<JSFunction> (Bun__callFormatPercentS in UtilInspect.cpp). The returned string is written with colors off, matching Node's colors: false.

util.format, Console instances, and the global console now cannot disagree on %s.

Verification

$ git stash push -- src/ && bun bd test test/js/web/console/console-log.test.ts test/js/node/util/node-inspect-tests/parallel/util-format.test.js
(fail) console.log %s matches util.format and Console instances (Node's rule)
  - "gc": "-0"          + "gc": "0"
  - "gc": "42n"         + "gc": "42"
  - "gc": "Symbol(q)"   + "gc": "THREW:TypeError"
  - "gc": "[ 1, 2 ]"    + "gc": "1,2"
  - "gc": "{ a: 1 }"    + "gc": "[object Object]"
  ... 16 mismatches
(fail) no assertion failures
  + '<Buffer 61 62>'  - 'ab'

$ git stash pop && bun bd test test/js/web/console/console-log.test.ts test/js/node/util/node-inspect-tests/parallel/util-format.test.js
 6 pass  0 fail

Also green: test/js/bun/util/inspect.test.js, test/js/node/util/bun-inspect.test.ts, test/js/node/util/util.test.js, test/js/node/util/custom-inspect.test.js, test/js/node/console/, test/js/bun/console/, test/js/web/console/.

Supersedes #36138 (the builtInObjects hardcoding, same list) and takes a different route than #34603 (which reimplements the decision tree in Rust; this calls the one JS routine instead so the three paths share code).

builtInObjects contents

The frozen set is the 47 names Node v26.3.0 observes on globalThis at bootstrap:

AggregateError Array ArrayBuffer Atomics BigInt BigInt64Array BigUint64Array
Boolean DataView Date Error EvalError FinalizationRegistry Float32Array
Float64Array Function Infinity Int16Array Int32Array Int8Array Intl Iterator
JSON Map Math NaN Number Object Promise Proxy RangeError ReferenceError Reflect
RegExp Set String Symbol SyntaxError TypeError URIError Uint16Array Uint32Array
Uint8Array Uint8ClampedArray WeakMap WeakRef WeakSet

SharedArrayBuffer, WebAssembly, Float16Array, DisposableStack, AsyncDisposableStack and SuppressedError are deliberately excluded: V8 installs them after Node computes the set, so they are absent from Node's builtInObjects, and the difference is observable (for example, util.inspect(new Float16Array(1), { showHidden: true }) walks into Float16Array.prototype in Node, while Float32Array does not).


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js

Bun shipped three distinct %s behaviors:

  util.format('%s', v)               Node's ported decision tree
  new Console(stream).log('%s', v)   same (JS formatWithOptions)
  console.log('%s', v)               native engine ToString(v)

The native global console threw on Symbol, lost -0 and the BigInt 'n'
suffix, and printed [object Object]/[object Map] for plain values. On
the util.format side, hasBuiltInToString classified Buffer, URL and
URLSearchParams as language built-ins because builtInObjects was scraped
from globalThis after Bun had already installed every host global.

Changes:

  inspect.js: builtInObjects is now the hardcoded ECMA-262 constructor
  names (what Node observes at bootstrap). The case-115 body is hoisted
  into formatPercentS(inspectOptions, arg) and exported.

  UtilInspect.cpp / ZigGlobalObject: a LazyProperty caches formatPercentS
  from internal/util/inspect; Bun__callFormatPercentS calls it.

  ConsoleObject.rs: PercentTag::S keeps the primitive-string fast path
  and delegates every other value to that one JS routine, writing the
  returned string (no ANSI). One source of truth for all three paths.
@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:01 PM PT - Jul 27th, 2026

@robobun, your commit cfdcf76 has 1 failures in Build #83618 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36141

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

bun-36141 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. util.inspect: freeze builtInObjects to Node's 47-name bootstrap set #36138 - Fixes %s formatting for Buffer (explicitly superseded by this PR, covers a subset of the same problem)
  2. console: use Node's %s format semantics instead of engine ToString #34603 - Implements Node-compatible %s format semantics for console (same goal, different approach)
  3. util: %s should inspect built-ins that inherit Symbol.toPrimitive #33462 - Fixes %s inspection of built-ins like Date and boxed Symbol (subset of this PR's %s formatting scope)

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7df25e3-9ae1-40af-a571-159666dcb0cc

📥 Commits

Reviewing files that changed from the base of the PR and between edf2672 and cfdcf76.

📒 Files selected for processing (1)
  • test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js

Walkthrough

Changes

Node-style %s formatting is centralized in formatPercentS, wired into native console formatting through a cached host bridge, and covered by util.format and console consistency tests.

Percent-s formatting

Layer / File(s) Summary
Shared JavaScript formatting rule
src/js/internal/util/inspect.js
Adds formatPercentS, updates %s handling, exports the helper, and uses fixed intrinsic names for built-in detection.
Native console bridge
src/jsc/ConsoleObject.rs, src/jsc/bindings/UtilInspect.cpp, src/jsc/bindings/ZigGlobalObject.*
Routes native %s formatting through the cached JavaScript helper and host binding.
Percent-s behavior validation
test/js/node/util/node-inspect-tests/parallel/util-format.test.js, test/js/web/console/console-log.test.ts, test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js
Adds coverage for host-global objects, collections, typed values, cross-console consistency, and typed-array inspection adjustments.

Suggested reviewers: jarred-sumner, cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: unifying %s formatting through formatPercentS across console and util paths.
Description check ✅ Passed It explains the change and verification clearly, even though it uses custom section headings instead of the template ones.
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.

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

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

On the duplicate check: #33462's hasBuiltInToString rewrite already landed on main via #34434 (the Date / boxed-Symbol inspect path is present in the code this PR builds on). This PR doesn't touch hasBuiltInToString; the only inspect.js changes are the builtInObjects literal set and hoisting %s into formatPercentS.

The cargo clippy check failed on the clone-lolhtml ninja step (vendor/lolhtml/Cargo.toml never materialized), not on any Rust in this diff. Other PR runs of the same workflow around the same time pass; will re-roll if it sticks.

Comment thread src/js/internal/util/inspect.js Outdated
Comment thread src/js/internal/util/inspect.js Outdated
Comment thread src/jsc/ConsoleObject.rs Outdated
Comment thread src/jsc/ConsoleObject.rs Outdated
Comment thread src/js/internal/util/inspect.js Outdated
Comment thread src/jsc/ConsoleObject.rs
robobun added 2 commits July 27, 2026 20:34
Drop the language globals Node itself does not see at bootstrap
(SharedArrayBuffer, Float16Array, DisposableStack, AsyncDisposableStack,
SuppressedError) and add back Infinity/NaN. Node's set is what shows up
on globalThis before V8's post-bootstrap globals and before Node/Web
globals are installed; freezing to a live scrape would observably
diverge (showHidden on Float16Array walks into its prototype in Node
but not for the other typed arrays).

Also:
- drop Float16Array from the typed-array showHidden loops in
  util-inspect.test.js (upstream test-util-inspect.js never had it) and
  give that test body a debug-build timeout: its surrogate-pair loop
  runs ~10k inspects and already exceeds 5s on bun bd with or without
  this change
- remove the stale print_string comment in ConsoleObject.rs now that
  %s no longer routes through it
- add the remaining throw-face cases to the %s unification test:
  [Symbol], {toString:null}, revoked Proxy
Comment thread src/js/internal/util/inspect.js Outdated

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
test/js/web/console/console-log.test.ts (1)

220-230: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the spawned process exit code.

proc.exited is awaited, but exitCode is never checked. The fixture could write matching output and still exit non-zero, allowing the test to pass despite a failure. Assert expect(exitCode).toBe(0) after the output, file, and behavior assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/web/console/console-log.test.ts` around lines 220 - 230, Add an
assertion for the awaited exitCode in the spawned-process test, requiring it to
equal 0 after the existing output, file, and behavior assertions. Keep the
current proc.stdout, proc.stderr, and proc.exited collection unchanged.

Source: Coding guidelines

src/jsc/ConsoleObject.rs (1)

3891-3912: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve line-length accounting for delegated %s output.

print_percent_s writes directly to writer_ without updating self.estimated_line_length. A following %o/object value can therefore make wrapping decisions using the column position from before %s, especially for long or multiline results. Preserve the existing accounting path and add a mixed %s plus object regression case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jsc/ConsoleObject.rs` around lines 3891 - 3912, The delegated output path
in print_percent_s must update self.estimated_line_length consistently with the
existing formatting output path. Account for the rendered %s result, including
long or multiline text, before subsequent %o/object formatting makes wrapping
decisions, and add a regression case covering mixed %s followed by an object
value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js`:
- Line 1819: Remove the per-test timeout argument from the test invocation
around isDebug, and move the 30-second debug budget to the enclosing suite-level
timeout mechanism if needed; otherwise reduce the workload while preserving the
test’s behavior.

---

Outside diff comments:
In `@src/jsc/ConsoleObject.rs`:
- Around line 3891-3912: The delegated output path in print_percent_s must
update self.estimated_line_length consistently with the existing formatting
output path. Account for the rendered %s result, including long or multiline
text, before subsequent %o/object formatting makes wrapping decisions, and add a
regression case covering mixed %s followed by an object value.

In `@test/js/web/console/console-log.test.ts`:
- Around line 220-230: Add an assertion for the awaited exitCode in the
spawned-process test, requiring it to equal 0 after the existing output, file,
and behavior assertions. Keep the current proc.stdout, proc.stderr, and
proc.exited collection unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8b0f871d-e2e0-4679-bf40-0a843d87e281

📥 Commits

Reviewing files that changed from the base of the PR and between 96c9551 and 9d197ac.

📒 Files selected for processing (4)
  • src/js/internal/util/inspect.js
  • src/jsc/ConsoleObject.rs
  • test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js
  • test/js/web/console/console-log.test.ts

Comment thread test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js Outdated
@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Addressing the outside-diff CodeRabbit findings:

  • print_percent_s line-length accounting (ConsoleObject.rs): fixed in 4b5055a. The result string now goes through BunString::from_js and add_for_new_line(str.length()) before writing, matching what the previous print_string path did, so subsequent %o/object tokens see the correct column position.
  • exitCode assertion (console-log.test.ts): already present at line 282, after the output and behavior checks.
  • per-test timeout (util-inspect.test.js): keeping; see inline reply. The test exceeded the 5s default on main under ASAN before this PR.

Comment thread test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js Outdated

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

Both earlier nits are addressed and I didn't find new issues, but this is a cross-layer change (new LazyPropertyOfGlobalObject + FFI bridge, native console now calls into JS for every non-string %s) that also picks one of two competing designs vs. #34603 — worth a maintainer's eyes.

What was reviewed:

  • Bun__callFormatPercentS exception discipline mirrors JSC__JSValue__callCustomInspectFunction above it; print_percent_s propagates via from_js_host_call and updates add_for_new_line.
  • formatPercentS is a byte-for-byte hoist of the previous case-115 body; formatWithOptionsInternal semantics unchanged.
  • Float16Array removal from util-inspect.test.js is a required consequence of the frozen builtInObjects set and is commented as such.
  • The m_utilInspectFormatPercentSFunction initLater follows the existing m_utilInspectFunction pattern and is in the GC-visited macro list.
Extended reasoning...

Overview

Eight files across three languages: hoists Node's %s decision tree into an exported formatPercentS in internal/util/inspect.js, replaces the dynamic builtInObjects scrape with a hardcoded 47-name ECMA-intrinsic list, adds Bun__callFormatPercentS in UtilInspect.cpp plus a LazyPropertyOfGlobalObject<JSFunction> on ZigGlobalObject, and reroutes the native PercentTag::S arm in ConsoleObject.rs through that bridge (with a string-literal fast path). Tests: a new 32-case three-way consistency test in console-log.test.ts, host-global %s cases added to util-format.test.js, and Float16Array dropped from two typed-array loops in util-inspect.test.js.

Security risks

None identified. No untrusted input parsing; the new FFI entry point takes a JSValue already held by the caller and hands it to a fixed internal JS function. Exception scopes are declared and checked at each fallible step.

Level of scrutiny

Medium-high. This changes user-visible output of console.log('%s', …) and util.format('%s', …) for many value classes, adds a native→JS call on a path that was previously pure-native, and freezes builtInObjects to a specific list whose exact membership (e.g. exclusion of SharedArrayBuffer, Float16Array, WebAssembly) is load-bearing for hasBuiltInToString. The PR also explicitly competes with #34603's Rust-side reimplementation — a maintainer should pick the direction.

Other factors

  • Both of my earlier inline findings (stale print_string comment; the debug-only per-test timeout ride-along) were addressed in 6c220fb and 1a2b8d7.
  • The builtInObjects hardcoding weakens an existing test (Float16Array removed from two showHidden loops); the removal is commented with the Node-parity rationale, which satisfies REVIEW.md's "every deletion needs a stated reason".
  • REVIEW.md discourages new fields on ZigGlobalObject; this one is a LazyProperty sitting alongside three existing m_utilInspect* siblings and is wired into FOR_EACH_GLOBALOBJECT_GC_MEMBER, so it's consistent with local convention rather than a god-object addition.
  • Test coverage is thorough (three-way agreement across 32 value shapes including Symbol, -0, BigInt, null-proto, revoked Proxy) and the PR shows the new test failing on the unfixed build.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js (1)

240-243: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add a dedicated Float16Array regression case.

The new intrinsic set intentionally changes Float16Array handling, but removing it from both matrices leaves that boundary untested. Keep it out of the homogeneous expectations, but add explicit showHidden assertions for the same-context and cross-context cases.

As per coding guidelines, tests must cover the complete variant matrix and every behavioral change must include an automated test in the same PR.

Also applies to: 275-275

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js` around
lines 240 - 243, Add a dedicated Float16Array regression case in the
util-inspect tests, keeping it excluded from the homogeneous typed-array
expectations. Add explicit showHidden assertions covering both same-context and
cross-context Float16Array instances, matching the intended new intrinsic
handling and completing the variant matrix.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js`:
- Around line 240-243: Add a dedicated Float16Array regression case in the
util-inspect tests, keeping it excluded from the homogeneous typed-array
expectations. Add explicit showHidden assertions covering both same-context and
cross-context Float16Array instances, matching the intended new intrinsic
handling and completing the variant matrix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf261e41-cf40-459d-928f-555e49bdfb30

📥 Commits

Reviewing files that changed from the base of the PR and between 9d197ac and edf2672.

📒 Files selected for processing (2)
  • src/jsc/ConsoleObject.rs
  • test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

On the Float16Array coverage gap: added a single positive assertion in cfdcf76 that util.inspect(new Float16Array(1), { showHidden: true }) includes "[buffer]: [Getter]", i.e. showHidden now walks into the prototype getters the way Node does. On main's builtInObjects (which scraped Float16Array from globalThis) that string is absent, so the assertion covers the boundary change.

Not adding a cross-context variant: vm.runInNewContext + Float16Array + showHidden exercises code outside what this PR touches.

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

No bugs found, but deferring to a human — this reroutes the native console.log('%s', ...) hot path through a JS call for every non-string value and picks one of two competing approaches (vs. #34603's Rust reimplementation), which is a design/perf trade-off worth a maintainer's sign-off.

What was reviewed:

  • Bun__callFormatPercentS / m_utilInspectFormatPercentSFunction follow the same LazyProperty + throw-scope pattern as the existing m_utilInspectFunction; exception paths look correct.
  • print_percent_s keeps the primitive-string fast path and updates estimated_line_length like the old print_string path did.
  • The hardcoded builtInObjects set also changes util.inspect(x, {showHidden: true}) for Float16Array/SharedArrayBuffer/DisposableStack/etc. — the PR documents this and adds a positive assertion for Float16Array.
Extended reasoning...

Overview

Eight files. In src/js/internal/util/inspect.js: replaces the dynamic Object.getOwnPropertyNames(globalThis) scrape for builtInObjects with a hardcoded 47-name literal, and hoists the case 115 body of formatWithOptionsInternal into an exported formatPercentS. In src/jsc/ConsoleObject.rs: replaces the PercentTag::S arm's print_as(Tag::String, ...) with a new print_percent_s that fast-paths primitive strings and otherwise calls Bun__callFormatPercentS (new C++ bridge in UtilInspect.cpp) which resolves and invokes the JS formatPercentS via a new LazyPropertyOfGlobalObject<JSFunction> on ZigGlobalObject. Tests: a 32-case three-way parity test in console-log.test.ts, host-global %s assertions in util-format.test.js, and a Float16Array showHidden adjustment in util-inspect.test.js.

Security risks

None identified. No untrusted-input parsing, no auth/crypto/permissions surface. The native→JS call goes through from_js_host_call with proper JSError propagation, and the C++ side has RETURN_IF_EXCEPTION after both the lazy-property resolution and the profiledCall.

Level of scrutiny

Medium-high. This is user-visible behavior on two very common APIs (console.log and util.format), and the native formatter is a hot path. The builtInObjects change has wider blast radius than %s alone — it also flips hasBuiltInToString and the showHidden prototype-walk decision for Float16Array, SharedArrayBuffer, WebAssembly, DisposableStack, AsyncDisposableStack and SuppressedError. The PR argues this matches Node's bootstrap-time set, and the Float16Array assertion checks one boundary, but the full set of side effects on util.inspect output deserves human eyes.

Other factors

  • Competing approach: #34603 reimplements Node's decision tree in Rust rather than calling into JS. Choosing between "one JS source of truth" and "no JS call on the console hot path" is an architectural call a maintainer should make.
  • Prior feedback addressed: my two earlier inline comments (stale print_string comment, ride-along debug timeout) were both fixed; comment-cop and CodeRabbit threads are all resolved.
  • Correctness spot-checks: the lazy-property initializer mirrors the sibling m_utilInspectFunction exactly (same requireIdgetIfPropertyExistsuncheckedDowncast<JSFunction> shape); print_percent_s calls add_for_new_line(str.length()) before writing, preserving column tracking for subsequent %o tokens; the string-literal fast path uses is_string_literal() which correctly excludes StringObject.
  • Test coverage is thorough for the three-way parity claim (32 value shapes across util.format / global console / Console instance) and demonstrably fails on the unfixed build per the PR body.

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

CI status at cfdcf76

This PR's own tests pass on every lane: test/js/web/console/console-log.test.ts, test/js/node/util/node-inspect-tests/parallel/util-format.test.js, test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js.

Remaining red/yellow on build 83618 are unrelated to this diff:

  • test/js/bun/http/serve.test.ts "request body backpressure > releases a paused request body ..." EPIPE on darwin 14 x64 and darwin 26 aarch64. Also failing on builds 83601/83602/83606/83608/83616/83617 across unrelated branches; reported to main-break triage.
  • bun-upgrade.test.ts (ETXTBSY), test-http-server-connections-checking-leak.js, no-orphans.test.ts, expo.test.ts: all marked flaky by the runner, passed on retry on other lanes.

The cargo clippy check is green as of edf2672.

Ready for review.

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.

2 participants