Skip to content

feat: budgeted console formatter (inspect builtin) - #416

Open
edusperoni wants to merge 2 commits into
feat/primordialsfrom
feat/inspect
Open

feat: budgeted console formatter (inspect builtin)#416
edusperoni wants to merge 2 commits into
feat/primordialsfrom
feat/inspect

Conversation

@edusperoni

@edusperoni edusperoni commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

What

Replaces the console's no-DevTools formatting pipeline — which could hang the app on a single console.log — with internal/inspect.js, a budgeted util.inspect-lite built on primordials. Stacked on #415 (primordials), which is stacked on #411 (builtins).

The problem

console.log(obj) without a DevTools frontend serialized the whole object graph:

  • JSON.stringify(obj, replacer, 2) with no depth or size limit — logging a store/component tree serialized everything reachable, then shipped megabytes through a stack-frame regex and NSLog.
  • The replacer's seen array made cycle tracking O(n²) — quadratic interpreter work under jitless — and never popped on subtree exit, so shared acyclic references falsely printed [Circular].
  • A separate hand-rolled array printer recursed with no depth limit and only caught an array containing itself — any indirect cycle was a stack overflow.
  • JSON.stringify invokes getters and toJSON — a log line could execute arbitrary user code.
  • console.dir had a third bespoke dump with the same issues.

The fix

inspect.js (function-body builtin, primordials-clean — it must work precisely when the app is broken):

  • Budgets everywhere: depth 2 (dir uses 4), 100 entries per collection, 10k chars per string, 16KB hard cap per call with an explicit truncation marker. A pathological log is now bounded work by construction.
  • Correct cycles: ancestor Set (enter/exit) — true cycles say [Circular], diamonds print normally, O(1) membership.
  • Getters are never invoked ([Getter]/[Setter] tags). Two deliberate exceptions: a guarded error.stack read (V8 materializes stacks via a lazy accessor and every error path in the runtime already reads it), and custom toString overrides — NativeScript core's ViewBase/Observable convention (Button(42)-style short forms) is honored, matching the previous console: an own-or-inherited toString other than Object.prototype's is detected via descriptors (no invocation to detect), then invoked guarded and capped; broken overrides degrade to structural rendering.
  • Tamper-immune: brands via captured Object.prototype.toString, Map/Set walked through captured iterator next (early-exit capable, unlike forEach), sizes via captured accessor getters. Verified by device tests that break Array.prototype.*, Object.keys, JSON.stringify etc. and log anyway.
  • Native wrappers render as short hints ([UILabel], [class UIView], [Pointer]) via a binding.getNativeWrapperHint native callback instead of walking native-backed graphs.
  • Type-aware: Map/Set with sizes, Date/RegExp/Error (with stack), [Function: name]/[class Name], TypedArrays/ArrayBuffers with lengths, null-prototype and constructor-name prefixes, sparse arrays, BigInt, symbols.
  • Exposed as the internal __inspect global (testability + app-level escape hatch).

C++ side shrinks: console.log/dir/assert all route through one cached formatter; the array printer, the dir dump, and smart-stringify.js (+ its Helpers/Caches plumbing) are deleted. Top-level strings still print raw. If the formatter ever fails to initialize, logging degrades to ToDetailString instead of breaking.

Also extends primordials.js (Set methods, iterator next captures, accessor getters, brand/regexp helpers) and the ESLint captured-statics list.

Output changes to be aware of

Logs now look like Node's inspect output — { a: 1, b: [Circular] }, Map(3) { ... }, ... 150 more items, truncation markers — instead of pretty-printed JSON. console.dir keeps its dump markers but uses depth 4.

Testing

  • 31 host-side functional assertions (Node harness over the same function bodies), including hostile stack getters and tampered prototypes.
  • 9 new device specs (InspectTests.js): cycles vs diamonds, caps, getter non-invocation, native-wrapper hints, tampered-prototype formatting, and a regression test that console.log of a 5000-node cyclic graph completes in bounded time.
  • Full suite: 922 tests, 0 failures (912 baseline + 10 new).

Also fixes a latent js2c bug this PR flushed out: builtin sources containing non-ASCII bytes (UTF-8 >= 0x80) failed to compile as const char array initializers; the generator now emits unsigned char.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c3bc5f5d-728a-4704-9e4b-2a3ee0605e1e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@edusperoni
edusperoni marked this pull request as ready for review July 30, 2026 17:20
…ed logging

console.log without DevTools serialized entire object graphs through
JSON.stringify with an O(n^2) seen-array, no depth or size limits, a
hand-rolled array printer that only caught direct self-cycles, and a
third bespoke dump in console.dir. A large object could hang the app
for seconds and shared acyclic references printed as [Circular].

internal/inspect.js is a util.inspect-lite built on primordials: depth,
per-collection and total-output budgets make unbounded work impossible;
an ancestor set reports only true cycles; getters are never invoked
(except the guarded error.stack read every error path already does);
brands come from Object.prototype.toString so tampered prototypes
cannot break or hijack logging. Native wrappers render as a short
class-name hint via the binding bag instead of being walked. Console
log/dir/assert all route through it, smart-stringify is retired, and
the formatter doubles as the internal __inspect global.
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.

2 participants