Skip to content

Minify: pick the shorter of decimal vs scientific for floats - #29312

Closed
robobun wants to merge 1 commit into
mainfrom
farm/e97ab081/fix-minify-scientific-notation
Closed

robobun wants to merge 1 commit into
mainfrom
farm/e97ab081/fix-minify-scientific-notation

Conversation

@robobun

@robobun robobun commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator

Closes #29311.

Repro

// index.js
console.log(1e300);

Before:

$ bun build index.js --minify
console.log(10000000000000000000000000…[301 digits]…0000000);

After (and what esbuild produces):

$ bun build index.js --minify
console.log(1e300);

Cause

printNonNegativeFloat in src/js_printer.zig has explicit fast-paths for a handful of integer powers of ten (1e4..1e9), but every other large or small float fell through to p.fmt("{d}", ...), which always expands to the shortest round-trip decimal — 301 digits for 1e300.

Fix

Added printFloatSmart next to printNonNegativeFloat. When --minify-syntax or --minify-whitespace is on, it formats the value with both {d} and Zig's {e}, applies two length-preserving rewrites (0.5 → .5, 3.14159e0 → 3.14159), and emits whichever is shorter. Non-minified output keeps the existing {d} behaviour so source maps and snapshots that lean on decimal form don't shift.

Verification

test/regression/issue/29311.test.ts — two tests:

  • --minify: confirms 1e300, 1e-5, 1e-7, 1e5, 1e12, .5, etc. are emitted, runs the minified JS, and checks each value round-trips through the JS runtime.
  • No-minify: confirms 1.5e20 still prints as the 21-digit decimal form (no unintentional change for non-minified output).

One existing bundler expectation in test/bundler/bundler_minify.test.ts (minify/ConstantFoldingUnaryPlusString) updated: +'0.1' now minifies to .1 instead of 0.1 (strictly shorter, same semantics).

Fail-before verified with USE_SYSTEM_BUN=1 — the minify test fails against stock bun and passes against this build.

@robobun

robobun commented Apr 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:46 AM PT - Aug 13th, 2026

✅ @robobun, your commit 9f6342f7b0dde97ac7b916c3863872f91e92fa64 passed in Build #94036! 🎉


🧪   To try this PR locally:

bunx bun-pr 29312

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

bun-29312 --bun

@coderabbitai

coderabbitai Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Diverted float printing in the JS printer to a new internal printFloatSmart used for minified JS (non-JSON) that computes both decimal and scientific forms, normalizes them (e.g., removes 0. → . and e0), and emits the strictly shorter representation; added/updated tests for minified and non‑minified outputs.

Changes

Cohort / File(s) Summary
JS printer change
src/js_printer.zig
Added fn printFloatSmart(p: *Printer, float: f64) void; rerouted non-negative float printing in minified JS (non-JSON) to this helper. Computes {d} and {e} into fixed buffers, removes leading 0. → ., drops trailing e0 from scientific when exponent is zero, chooses strictly shorter representation (decimal on ties), and falls back to {d} on formatting failures.
Minify test expectation update
test/bundler/bundler_minify.test.ts
Updated expected captured values for unary-plus string coercion from "0.1" to ".1" to match new minified numeric output.
Regression tests added
test/regression/issue/29311.test.ts
Added regression tests exercising many numeric literal cases with bun build --minify and without --minify, asserting build success, inspecting minified output for shortest numeric spellings (and absence of pathological zero expansion), and validating runtime evaluation of the emitted bundle.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change: selecting shorter float representations during minification by comparing decimal vs scientific notation.
Description check ✅ Passed Description covers all required template sections with detailed repro, cause analysis, fix explanation, and verification details.
Linked Issues check ✅ Passed PR fully addresses issue #29311: implements shorter float selection between decimal and scientific notation when minifying, with tests verifying round-trip correctness.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the minification float-representation objective; no unrelated modifications to other subsystems or features detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/js_printer.zig`:
- Around line 1615-1620: The code in printFloatSmart() currently strips the
leading zero from dec_full into dec (dec_full[1..]) which breaks JSON output
used by printJSON(); change printFloatSmart to avoid removing the integer 0 when
used for JSON by adding a boolean parameter (e.g., keep_leading_zero or is_json)
or a context flag, and only perform the dec_full[1..] transformation when the
flag is false; update callers (including printJSON) to pass the appropriate flag
so JSON printing preserves "0.5" and "-0.5".

In `@test/regression/issue/29311.test.ts`:
- Line 38: Tests currently assert exitCode with expect(exitCode).toBe(0) before
verifying stdout/content; move each expect(exitCode).toBe(0) to the end of its
respective test assertion block so stdout/content checks (e.g.,
expect(stdout).toContain(...), expect(content).toMatch(...)) run first and the
exit code assertion runs last; update the three occurrences (the
expect(exitCode).toBe(0) at the current locations and the ones referenced in the
comment around lines 74-75 and 111-113) accordingly.
- Line 37: Remove the fragile assertion that stderr is empty in the regression
test by deleting or commenting out the expect(stderr).toBe(""); check in the
test (the assertion on stderr found in the test/regression/issue/*.test.ts
file); for stability, either remove the stderr assertion entirely or replace it
with a non-fatal check (e.g., only fail on non-zero exit code) so ASAN/JSC
warnings in spawned Bun subprocesses do not cause CI flakes—update the test that
contains expect(stderr).toBe(""); and any duplicate assertions (e.g., the other
occurrence noted) accordingly.
🪄 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: 51dbc057-c25b-4c55-a4c2-2cf6f34c8c3c

📥 Commits

Reviewing files that changed from the base of the PR and between 903015a and ddf3aed.

📒 Files selected for processing (3)
  • src/js_printer.zig
  • test/bundler/bundler_minify.test.ts
  • test/regression/issue/29311.test.ts

Comment thread src/js_printer.zig Resolved
Comment thread test/regression/issue/29311.test.ts Resolved
Comment thread test/regression/issue/29311.test.ts Resolved

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

♻️ Duplicate comments (2)
test/regression/issue/29311.test.ts (2)

34-34: ⚠️ Potential issue | 🟡 Minor

Assert exit code last in each spawned-process assertion block.

Move expect(...exitCode).toBe(0) to the end, after stdout/content checks, for clearer failures and to match test conventions.

Suggested patch
-  expect(exitCode).toBe(0);
-
   // The bug: 1e300 was expanded to 301 digits. Guard against any regression
   // that re-introduces long runs of zeros in the minified output.
   expect(stdout).not.toMatch(/0{20,}/);
@@
   // 1.5e20 and 15e19 are both 6 chars — tie goes to decimal/scientific
   // depending on Zig's {e} output; both are acceptable.
   expect(stdout).toMatch(/1\.5e20|15e19/);
+  expect(exitCode).toBe(0);
@@
-  expect(runExitCode).toBe(0);
   expect(runStdout.split("\n").filter(Boolean)).toEqual([
@@
     "-1e+300",
     "150000000000000000000",
   ]);
+  expect(runExitCode).toBe(0);
@@
-  expect(exitCode).toBe(0);
   // Without --minify, 1.5e20 stays in its full decimal form as before.
   expect(stdout).toContain("150000000000000000000");
+  expect(exitCode).toBe(0);

As per coding guidelines: Expect stdout assertions before exit code assertions - expect(stdout).toBe(...) BEFORE expect(exitCode).toBe(0) for more useful error messages.

Also applies to: 70-71, 103-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/29311.test.ts` at line 34, Several spawned-process
assertion blocks in test/regression/issue/29311.test.ts assert exitCode too
early; move each expect(exitCode).toBe(0) to the end of its respective assertion
block so stdout/content checks (e.g., expect(stdout).toContain(...) /
expect(stderr).toBe(...)) come first. Update the three affected blocks (the one
containing the current expect(exitCode).toBe(0) near the start, and the other
blocks referenced around the comment ranges) so that exitCode assertions run
after all stdout/stderr/content expectations for clearer failure messages.

33-33: ⚠️ Potential issue | 🟠 Major

Remove empty-stderr assertions in regression subprocess tests.

expect(stderr).toBe("") is brittle in ASAN/debug CI where benign warnings can appear on stderr even when behavior is correct.

Suggested patch
-  const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+  const [stdout, _stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
 
-  expect(stderr).toBe("");
+  void _stderr;

Based on learnings: In regression tests under test/regression/issue/*.test.ts, avoid asserting spawned Bun subprocess stderr is empty because ASAN may emit warnings.

Also applies to: 102-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/29311.test.ts` at line 33, Remove the brittle
empty-stderr assertion by deleting the expect(stderr).toBe("") check found in
this regression test (and any identical occurrences in other tests under the
regression issue suite); simply stop asserting stderr is exactly empty (remove
the expect(stderr).toBe("") line) so spawned Bun subprocess tests no longer fail
due to benign ASAN/debug warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/regression/issue/29311.test.ts`:
- Line 34: Several spawned-process assertion blocks in
test/regression/issue/29311.test.ts assert exitCode too early; move each
expect(exitCode).toBe(0) to the end of its respective assertion block so
stdout/content checks (e.g., expect(stdout).toContain(...) /
expect(stderr).toBe(...)) come first. Update the three affected blocks (the one
containing the current expect(exitCode).toBe(0) near the start, and the other
blocks referenced around the comment ranges) so that exitCode assertions run
after all stdout/stderr/content expectations for clearer failure messages.
- Line 33: Remove the brittle empty-stderr assertion by deleting the
expect(stderr).toBe("") check found in this regression test (and any identical
occurrences in other tests under the regression issue suite); simply stop
asserting stderr is exactly empty (remove the expect(stderr).toBe("") line) so
spawned Bun subprocess tests no longer fail due to benign ASAN/debug warnings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4a75986-a3ca-4b8b-b67f-a61c0e94fcb4

📥 Commits

Reviewing files that changed from the base of the PR and between ddf3aed and f40bbfe809f2cc8e53b04ba57fa72b69cb181040.

📒 Files selected for processing (1)
  • test/regression/issue/29311.test.ts

Comment thread src/js_printer.zig Resolved
Comment thread test/regression/issue/29311.test.ts Resolved
Comment thread test/regression/issue/29311.test.ts Resolved
Comment thread src/js_printer.zig Resolved
Comment thread test/regression/issue/29311.test.ts Resolved
@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from f40bbfe to 20e0911 Compare April 14, 2026 19:11

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

LGTM — all prior review concerns (JSON guard, integer fast-path bypass, test precision, assertion ordering) addressed in the follow-up commit.

Extended reasoning...

Overview

This PR introduces printFloatSmart in src/js_printer.zig that, when minifying JS, picks the shorter of the decimal and scientific forms for every f64 value. It also adds a regression test and updates one bundler snapshot expectation.

Security risks

None. This is purely a code-generation optimization in the JS printer with no security implications.

Level of scrutiny

Moderate — the change touches the printer's float-emission path, which is used extensively. However, the logic is self-contained, well-commented, and the two critical correctness risks (JSON invalidity from .5 and integer fast-path bypass) were both found and fixed in the follow-up commit 20e0911.

Other factors

My previous two rounds of reviews flagged a 🔴 JSON-printer correctness bug (0.5→.5 invalid in RFC 8259), a 🔴 integer fast-path bypass, and several 🟡 test-quality issues. Commit 20e0911 addresses all of them: the early-return guard before the switch ensures minified non-JSON paths always go through printFloatSmart, the comptime is_json guard keeps JSON output RFC-compliant, and the regression tests now use precise assertions. No outstanding issues remain.

Comment thread test/regression/issue/29311.test.ts Resolved

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29311.test.ts`:
- Around line 5-7: In the test header comment in
test/regression/issue/29311.test.ts remove the three-line bug-history prose that
follows the issue URL (the sentences about Minifier expanding
scientific-notation floats and the example) and leave only the issue URL and any
comments that explain the test design rationale; ensure you do not remove or
alter comments that describe why the test exists or how it validates
behavior—only delete the historical/contextual prose.
🪄 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: d6c4420f-e8df-4648-bd03-6aa0c48f4601

📥 Commits

Reviewing files that changed from the base of the PR and between a71395e and 66ae44e.

📒 Files selected for processing (1)
  • test/regression/issue/29311.test.ts

Comment thread test/regression/issue/29311.test.ts Resolved
@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from f026816 to 46a3033 Compare April 14, 2026 20:04

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29311.test.ts`:
- Around line 27-28: Add two focused test cases in
test/regression/issue/29311.test.ts that run Bun.build with each standalone
flag: spawn processes using Bun.spawn invoking bunExe() with ["build",
"--minify-syntax", `${dir}/index.js`] and ["build", "--minify-whitespace",
`${dir}/index.js`] respectively (similar to the existing combined "--minify"
invocation). For each process (created like the existing proc), await
completion, check exit code and stdout/stderr to mirror the expectations used
for the combined minify test so the suite verifies each flag is wired
independently (use the same assertions/assert helpers currently used for the
combined case). Ensure tests are named/described to indicate they are standalone
checks for --minify-syntax and --minify-whitespace.
🪄 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: 4deb24c5-4101-4af4-b4ad-9b57784f2f26

📥 Commits

Reviewing files that changed from the base of the PR and between 66ae44e and 46a3033.

📒 Files selected for processing (1)
  • test/regression/issue/29311.test.ts

Comment thread test/regression/issue/29311.test.ts Resolved
Comment thread test/bundler/bundler_minify.test.ts Resolved
Comment thread test/regression/issue/29311.test.ts Resolved
@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from ff3ac3f to dbdb8bf Compare April 14, 2026 20:32

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

LGTM — all previously reported issues resolved.

Extended reasoning...

Overview

The PR adds printFloatSmart to src/js_printer.zig and hooks it into printNonNegativeFloat. When --minify-syntax or --minify-whitespace is active (and the printer is not in JSON mode), it formats each float with both Zig's {d} and {e} formatters, applies two normalizations (0.5→.5 and dropping trailing e0), and emits whichever form is shorter. Four files changed: the Zig printer, two bundler test expectation files, and a new regression test.

Security risks

None. The change is purely a formatting/output optimization with no auth, crypto, permissions, or external data-handling.

Level of scrutiny

Moderate — the fix touches a hot path in the JS printer that affects all minified builds, and a JSON correctness concern (invalid .5) was initially present. Both the JSON printer guard (comptime is_json early return in printFloatSmart, plus \!is_json on the early-dispatch in printNonNegativeFloat) and the integer fast-path bypass have been correctly addressed in subsequent commits.

Other factors

All red and yellow inline bug reports from prior review rounds have been marked resolved and confirmed fixed by robobun. The regression test covers: the primary 1e300 bug, small-exponent floats, the 0.5→.5 rewrite, integer fast-path ranges (20000, 2000000000), standalone --minify-syntax and --minify-whitespace flags, non-minified decimal preservation, and a runtime round-trip correctness check.

@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from dbdb8bf to 941c208 Compare May 4, 2026 15:17

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

All prior review feedback has been addressed and the bug hunter found nothing new, but since this changes float emission in the core JS printer (affecting every minified bundle), I'd like a human to give it a final look before merge.

Extended reasoning...

Overview

This PR adds printFloatSmart to src/js_printer/js_printer.zig (~70 lines) and reroutes printNonNegativeFloat through it whenever minify_syntax or minify_whitespace is set for non-JSON output. It formats each float with both {d} and {e}, applies two rewrites (0.5→.5, strip trailing e0), and emits the shorter form. Three test files are touched: a new regression test, and updated capture expectations in bundler_minify.test.ts and bundler_edgecase.test.ts.

Security risks

None. This is pure output formatting of numeric literals; no untrusted input handling, auth, or I/O changes.

Level of scrutiny

Medium-high. The JS printer is on the hot path for every bun build invocation, and float emission correctness directly affects whether minified bundles parse and round-trip. Earlier review rounds caught a real correctness bug (.5 being emitted in JSON mode, breaking bun publish), an incomplete fix (integer fast-path ranges bypassing the smart path), and a stale test snapshot (bundler_edgecase NaN-boxed capture). All of those are now fixed and verified in the current diff — the is_json comptime guard is in place at both call sites, the early-return covers all values, and the edgecase capture is updated to -1.7976931348623157e308.

Other factors

The regression test is thorough (shape assertions + runtime round-trip + per-flag coverage), CI is green except for two unrelated flakes (bun-add, webview-chrome), and the implementation matches esbuild's approach. I'm deferring rather than approving only because this is a behavioural change to a critical printer path that took several iterations to get right, and a human sign-off on the final Zig diff seems prudent.

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

All prior review feedback is addressed and the bug hunter found nothing new — deferring to a human since this changes numeric-literal output in the core JS printer for every minified bundle, and Build #51318 hasn't confirmed green yet.

Extended reasoning...

Overview

This PR adds printFloatSmart to src/js_printer/js_printer.zig and routes all non-negative floats through it when --minify-syntax or --minify-whitespace is enabled (non-JSON only). It compares Zig's {d} and {e} renderings, applies two rewrites (0.5→.5, strip trailing e0), and emits the shorter form. Three test files are updated/added: a new regression suite, plus capture updates in bundler_minify.test.ts and bundler_edgecase.test.ts.

Security risks

None. This is pure output formatting of numeric literals; no user input parsing, auth, filesystem, or network surface is touched.

Level of scrutiny

Moderate-to-high. The change is small and well-scoped, but printNonNegativeFloat runs for every numeric literal in every minified bundle Bun produces. A subtle round-trip mismatch between Zig's float formatter and JS number parsing would silently corrupt user code at runtime. The regression test does include a runtime round-trip check (bun -e <minified output>) which is good mitigation, and the JSON path is correctly guarded by comptime is_json at two layers — but this is still core-printer territory where a human sign-off is warranted.

Other factors

  • This PR has been through several review rounds; every issue I (and CodeRabbit) raised is now resolved in the current diff, including the JSON .5 guard, the integer fast-path bypass, the bundler_edgecase NaN-boxed capture, the anchored console.log(1e300) assertion, and standalone --minify-syntax/--minify-whitespace coverage.
  • The robobun status comment still shows ❌ for Build #51318; the latest commit (eba26f5) is a CI retrigger because the previous jobs expired before running, so CI hasn't actually confirmed the current revision passes across all platforms.
  • Given the production-critical path and the not-yet-green CI, I'm deferring rather than approving.

@robobun

robobun commented May 5, 2026

Copy link
Copy Markdown
Collaborator Author

FYI: builds 51318 and 51364 both suffered mass BuildKite job expirations (nearly every failure is status Expired, meaning the job was evicted from the queue before running). The jobs that actually executed passed. This isn't a code or test issue — it's infrastructure. Would appreciate a maintainer-triggered BK rebuild once the queue clears.

@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from eba26f5 to 94f68bd Compare May 5, 2026 02:09

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

All prior feedback is addressed and I found no new issues. Deferring to a human since this reroutes every float literal in minified output through new {e}-vs-{d} selection logic — a hot, correctness-critical printer path — and CI hasn't yet produced a clean green run (recent BK builds expired before executing).

Extended reasoning...

Overview

This PR adds printFloatSmart to src/js_printer/js_printer.zig and an early-return in printNonNegativeFloat so that, when --minify-syntax or --minify-whitespace is enabled (and not JSON), every non-negative float is formatted with both Zig's {d} and {e} specifiers and the strictly shorter form is emitted (with 0. → . and trailing e0 stripped). It updates two existing bundler test expectations (bundler_minify.test.ts +'0.1' → .1; bundler_edgecase.test.ts NaN-boxed -1.797…e308 capture) and adds a thorough regression test at test/regression/issue/29311.test.ts covering shape assertions, runtime round-tripping, the integer fast-path bypass, and standalone-flag wiring.

Security risks

None. This is pure output-formatting of numeric literals; no untrusted input parsing, auth, crypto, filesystem, or network surface is touched.

Level of scrutiny

High. printNonNegativeFloat runs for every numeric literal in every minified bundle bun produces. Correctness here depends on Zig's {e} formatter emitting a shortest-round-trip representation that re-parses in JS engines to the exact same f64 (Ryu-based, so this should hold), and on the two string rewrites preserving JS lexical validity. The JSON path is correctly guarded by comptime is_json at both the early-return and inside printFloatSmart, so bun publish etc. won't emit .5. The logic looks right and the regression test includes a runtime round-trip check, but a subtle mismatch would silently corrupt numeric constants across the ecosystem — that warrants a human sign-off rather than bot approval.

Other factors

  • Over four prior review rounds, every issue I and CodeRabbit raised (JSON .5, integer fast-path bypass, vacuous test assertions, exitCode ordering, ASAN-stderr flakiness, missing bundler_edgecase snapshot update, standalone-flag coverage) has been addressed; all inline threads are resolved.
  • No CODEOWNERS entry covers src/js_printer/.
  • Per the author's latest note, BuildKite builds 51318/51364 mass-expired before running, so there is not yet a fully green CI run on the final revision — another reason to wait for a human + green CI before merging.

@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from da2fcf7 to b307264 Compare June 25, 2026 13:13
@robobun

robobun commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main and resolved the conflict.

The conflict was non-trivial: since this PR was opened, the runtime was ported from Zig to Rust (src/js_printer.zig → src/js_printer/lib.rs, which deleted the file the original diff touched). I re-implemented the fix in Rust as print_float_smart:

  • When minifying JS (non-JSON), format the value with both Display ({}, decimal) and LowerExp ({:e}, scientific), apply 0.5 → .5 and strip a trailing e0, then emit whichever is strictly shorter (decimal on ties). Rust's {:e} produces the same scientific form Zig's {e} did (verified against every test case).
  • Every value routes through print_float_smart when minifying, so the integer fast path (2000000000 → 2e9) is covered too.
  • JSON keeps the decimal form, guarded by the IS_JSON const generic.

Test-side changes carried over unchanged (they're runtime-agnostic bun build --minify subprocess assertions). Also updated the bundler_edgecase NaN-boxed capture (-1.79e308 → -1.7976931348623157e308) and bundler_minify (0.1 → .1).

Verified locally with bun bd: regression test 4/4, bundler_minify 39/39, bundler_edgecase EnumInlining 2/2.

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

All prior feedback is addressed and I didn't find new issues, but this changes float printing for every minified bundle and was just re-implemented from Zig to Rust during the rebase — worth a human look at the print_float_smart port specifically (Rust's {}/{:e} semantics differ from Zig's {d}/{e}).

Extended reasoning...

Overview

This PR adds print_float_smart to src/js_printer/lib.rs and routes all non-JSON float printing through it when minify_whitespace or minify_syntax is enabled. It formats the value with both Rust's Display ({}) and LowerExp ({:e}), applies two rewrites (0.5 → .5, strip trailing e0), and emits whichever is strictly shorter. It also updates two existing bundler snapshot expectations (bundler_minify.test.ts, bundler_edgecase.test.ts) and adds a comprehensive regression test with runtime round-trip verification.

Security risks

None. This is pure output-formatting logic for numeric literals; there's no user input handling, auth, crypto, or filesystem interaction. The worst-case failure mode is emitting a numeric literal that doesn't round-trip to the same f64, which would be a correctness bug, not a security issue.

Level of scrutiny

Medium-high. print_non_negative_float runs for every numeric literal in every bundled/minified file, so a subtle round-trip bug here would silently corrupt user bundles. More importantly, this PR was originally written and reviewed against src/js_printer.zig, but the runtime has since been ported to Rust and the author re-implemented the fix during rebase. Rust's Display for f64 (Ryu-based shortest round-trip) and LowerExp have different edge-case behavior than Zig's {d}/{e}, so the prior review rounds don't fully transfer. The !IS_JSON guard at the call site correctly preserves RFC 8259 compliance for JSON output (the earlier in-function backstop was dropped, which is fine since there's only one call site).

Other factors

  • All eight prior inline comments (mine and CodeRabbit's) are resolved and verified addressed in the current diff: JSON guard, integer fast-path bypass, exitCode ordering, ASAN stderr, vacuous .5/1e300 substring assertions, bundler_edgecase snapshot, per-flag coverage.
  • The regression test is thorough (shape assertions + runtime round-trip + per-flag coverage), and the author reports 4/4 + 39/39 + 2/2 passing locally on the rebased build.
  • CI currently shows musl build failures and a Windows napi.test.ts failure — these look unrelated to this change but the build is not green.
  • Given the cross-language re-implementation of a hot path in the printer, I'd prefer a human confirm the Rust formatting semantics rather than auto-approve.

@robobun

robobun commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

The diff itself is green — every lane that exercises the change passes. The remaining red is unrelated Windows flake:

  • Build 64607: terminal/terminal-platform-gaps.test.ts (ConPTY newline rendering) + napi/napi.test.ts
  • Build 64616: bake/deinitialization.test.ts (dev-server WebSocket, flagged 1 retry) + napi/napi.test.ts

The failing set changes run-to-run, which is the flake signature. None of these tests touch the JS printer or float formatting — the diff is scoped to src/js_printer/lib.rs (print_float_smart) plus its three test files, and bundler_minify / bundler_edgecase / the #29311 regression test all pass locally under bun bd.

I've used my one CI re-roll for this round, so I'm not going to keep pushing retrigger commits. This needs a maintainer to re-run the flaky Windows lanes (or confirm napi.test.ts is a pre-existing Windows failure on main). Happy to rebase again if anything lands that conflicts.

@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from cb0ebe8 to a804c69 Compare August 13, 2026 03:25
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
Comment thread src/js_printer/lib.rs Resolved
@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from a804c69 to 3aa6915 Compare August 13, 2026 03:28
Comment thread src/js_printer/lib.rs Resolved
@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from 3aa6915 to 0f76f8e Compare August 13, 2026 03:30
Comment thread src/js_printer/lib.rs Resolved
print_non_negative_float always emitted the shortest round-trip decimal,
so 1e300 expanded to 301 digits under --minify. When minifying JS (not
JSON), format the value with both Display (decimal) and LowerExp
(scientific), apply two length-preserving rewrites (0.5 -> .5, strip a
trailing e0), and emit whichever is shorter. Matches esbuild.

Routing every value through print_float_smart when minifying also covers
the integer fast path (e.g. 2000000000 -> 2e9), which previously emitted
full decimal for anything that was not a round 1e4..1e9 power of ten.

JSON output keeps the decimal form (a bare leading '.' is invalid JSON),
guarded by the IS_JSON const generic.

Closes #29311
@robobun
robobun force-pushed the farm/e97ab081/fix-minify-scientific-notation branch from 0f76f8e to 9f6342f Compare August 13, 2026 03:32
Comment thread src/js_printer/lib.rs Resolved

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

LGTM — the earlier comment-cop / verbosity feedback is addressed; comments are now single-line.

What was reviewed

  • print_float_smart: traced every test value through {} vs {:e}, the 0.→. and trailing-e0 rewrites, and the tie-break — all produce the asserted output and round-trip.
  • Confirmed print_number filters NaN/±Infinity before print_non_negative_float, so print_float_smart only sees finite non-negative values (no inf leakage).
  • IS_JSON guard keeps JSON output on the plain decimal path; non-minified path unchanged.
  • bundler_edgecase / bundler_minify snapshot updates match the new shorter forms; no other float-shape assertions found in the suite.
Extended reasoning...

Overview

Adds print_float_smart to src/js_printer/lib.rs (~35 lines) and an early-return in print_non_negative_float that routes every finite non-negative float through it when minify_syntax || minify_whitespace is set and the output is not JSON. The helper formats both {} (decimal) and {:e} (scientific) into stack buffers, applies two length-preserving JS-specific rewrites (0.5→.5, strip trailing e0), and prints whichever slice is strictly shorter (decimal on ties, matching esbuild). Updates two existing bundler snapshot expectations and adds a 4-case regression test that also round-trips the minified output through the runtime.

Security risks

None. Pure formatting of an already-parsed f64 into a fixed stack buffer; buf_print returns Err on overflow and both let-else arms fall back to the previous streaming path. No allocation, no user-controlled length, no FFI.

Level of scrutiny

Moderate. The printer is hot-path and its output is user-visible, but the change is gated to minified JS only (non-minified and JSON output are byte-identical to before), the transform is length-preserving-or-shortening by construction, and the regression test executes the minified output to prove semantic equivalence for every covered value. I hand-traced the twelve fixture values plus 0.0, 10, and 1.5 through both branches and the rewrites; each matches the asserted string.

Other factors

This PR has been through three prior review rounds (mine and CodeRabbit's) — every thread is resolved, including the comment-cop verbosity flags (current diff has single-line comments only). The caller print_number handles NaN/Infinity/sign before this function, so leading-dot output (.5) only appears where a number literal is syntactically valid in JS. The bundler_edgecase NaN-boxed capture and bundler_minify +'0.1' capture are the only pre-existing assertions on decimal float shape under minify, and both are updated here.

@robobun

robobun commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #41156, which prints esbuild's shortest number form for every number (this PR's cases included, with and without a minify flag) and is under review. Its test file now carries the 1e300, 1e-7 and 15e19 cases from here. #29311 stays open as the tracking issue until that lands.

@robobun robobun closed this Sep 2, 2026
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.

Bun always expands numbers in scientific notation even if the scientific form is shorter

1 participant