console: %i and %f convert with parseInt and parseFloat, and a BigInt prints
RO
21d ago+512+512+012345678901234567890123456789-143-143-012345678901234567890123456789
Fixes #31777
Problem
console.log('took %d ns', 12n)printstook, then throwsTypeError: Conversion from 'BigInt' to 'number' is not allowed.console.log('%i %i', [1, 2, 3, 4, 5])printsNaN %i. Node printstook 12n nsand1 %i.Formatter::write_with_formatting(src/jsc/ConsoleObject.rs:2452,:2533) converts the argument with ToNumber, then imitatesparseIntin ani64.parseIntandparseFloatconvert with ToString first.
Fix
- One arm handles the three specifiers.
%iisparseInt(value)and%fisparseFloat(value), with JSC's parsers. A BigInt under%dor%iprints as12n. %dstays the integer part ofNumber(value). Its digits are now exact.- Verified:
test/js/web/console/console-log.test.ts(main fails 7 tests). Alsotest/js/web/console/withBUN_JSC_validateExceptionChecks=1. - Self-reviewed: 10 concerns raised, 9 addressed. Not taken: the integer fast path in
FormatDouble::dtoa_with_negative_zero. It adds 22 instructions toconsole.log(1234.5).
Background
- The console has a native format scanner.
util.formathas a second one in JS, which matches node. - The WHATWG formatter has
parseInt(value, 10)for%dand%i. Node hasNumber(value)for%d. This PR does not choose for%d: #45951 holds that change. - Considered a call of the global
parseIntfunction (#31778): one VM entry for each converted argument.
Downsides
- Output changes on 91 of 336 table cells.
%iofnullprinted0and now printsNaN, as node does. %dstill differs from node on 38 of 112 values:%dof3.9prints3.%dofundefinedcosts 82 more instructions (331 to 413). The release binary grows by 4,096 bytes.
Notes
Table. 112 values under %d, %i and %f, against node v26.3.0:
%d | %i | %f | throw | |
|---|---|---|---|---|
| main 017b2439a7 | 49 differ | 42 | 36 | 18 |
| this PR | 38 | 0 | 0 | 0 |
- The 13
%dcells that change: 7 digit fixes (2 ** 60,2 ** 63,2 ** 64,123456789012345680000,Number.MIN_VALUE,Number.MIN_VALUE * 1.5,'12345678901234567890') and 6 BigInt values that threw. - 6
%iand%fcells change only in the sign of zero, to node's-0:%iof-0.5,-0.9999999,'-0','-0.0'and%fof'-0','-0.0'. %fof a Number whose shortest digits tie now prints whatString(number)returns:%fof999999999999998.25printed999999999999998.3and now prints999999999999998.2.- 5,000 generated strings and 17,644 generated Numbers (fixed seeds): 0 lines differ from
util.formatfor%iand%f, and 0 from the integer part for%d. Main: 1,832 and 1,809 string lines, 936, 1,277 and 19 Number lines. - The fixture
console-log.expected.txtchanges 8 lines: 5 digit fixes and the 3Hello NaNlines that recorded the reported bug.
Cost. Release builds of main 017b2439a7 and of this branch. Instructions of one write_with_formatting call, callees included (gdb stepi):
| call | main | this PR |
|---|---|---|
| no specifier | 129 | 131 |
%d of an int32 | 457 | 462 |
%f of 1234.5 | 1590 | 698 |
%d of 1700000000000 | 554 | 432 |
%i of 1234.5 | 513 | 381 |
%i of '42' | 763 | 477 |
%f of '42.5' | 1817 | 889 |
%d of '42' | 763 | 705 |
%i of undefined | 331 | 329 |
%f of undefined | 278 | 325 |
%d of undefined | 331 | 413 |
%i of {} | 541 | 508 |
- The int32 rows make the same 11 calls in both builds. The difference is register moves.
- No VM entry and no allocator call for a Number, a string,
undefinedor{}. A VM entry only runs atoStringorvalueOfof the argument. console.log("abc"),console.log(42),console.log(1234.5)andconsole.log(1700000000000): 0 instructions of difference for the whole call.size: text 106,738,870 to 106,743,054 bytes (+4,184).write_with_formattingshrinks from 3,156 to 2,621 bytes in each of its two copies. The helpers add 5,556 bytes.- Line layout: 6,915 swept calls with a trailing array or object print byte-identical to main.
- Test file on debug with ASAN and exception validation: 5.3 to 6.6 s (main's 5 tests: about 3.1 s). 5 more subprocesses, 6 on release builds. Each new test takes 0.4 to 1.2 s alone.
Not run. scripts/jsc-exception-lint needs the full WebKit sources under vendor/WebKit. cargo mordant is not installed here. cargo clippy for bun_core and bun_jsc, cargo fmt --check and clang-format pass.
Left for later.
%dasNumber(value), which node prints: #45951, stacked on this PR. It changes output that%dhas had since 2022.console.assert(false, ...)aborts underBUN_JSC_validateExceptionChecks=1with or without a format string, so its test is a todo on debug and ASAN builds (#44950, oven-sh/WebKit#790).- The line-width estimate counts an integer under
%dor%ias one column. This PR keeps that, so no line breaks move. - A format string in a later argument is still substituted (#45571).
console.tablewith a string first argument takes this scanner too. test.eachtitles (#41565) and the unreachable scanner insrc/runtime/test_runner/pretty_format.rs. #41565 adds its ownparseFloatcopy in the same file. The one that lands second can callparseFloatCharacters.- #39592 moves the
node:worker_threadsconsole onto this scanner. Those workers print node's text today.
This PR revives the token split and the tests of #31778.