Repository navigation
Conversation
…e is kept as tokens A value that the parser keeps as tokens (a custom property, an unparsed property, a var() fallback, an unknown at-rule) printed a number such as `8e64` from its f32. The f32 clamps `8e64` to `3.40282e38`, reads `4.9406564584124654e-324` as 0 and rounds `1.6777217e7` to `16777218`. `Parser::raw_number_text` now also returns the source text of a token that has an exponent, unless its unit is written with an escape. A token with no fraction and no exponent no longer walks its text.
|
Updated 11:23 AM PT - Oct 29th, 2026
❌ @robobun, your commit 54a10f3 has some failures in 🧪 To try this PR locally: bunx bun-pr 47222That installs a local version of the PR into your bun-47222 --bun |
|
Status Reproduced on main (11c3671) with :root{--commit:12e45ab;--tag:8e64;--shf:4.9406564584124654e-324;--uuid:12345678-12e3-8d9b-a456-426614174000}
PR: #47222 CI (build 135219, head 54a10f3): the diff is green. 185 of 188 jobs passed and no test failed. The build is red because 3
|
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Preserve numeric token spellings src/css/css_parser.rs, test/js/bun/css/css.test.ts, test/js/bun/css/angle-serialization-hang.test.ts |
raw_number_text checks integer magnitude, adjacency, number-text length, and unescaped exponent notation when deciding whether to preserve token text. Tests cover exponent forms, escaped units, typed-value conversion, and large radian values that remain token-preserved or reach Angle::to_css. |
Possibly related PRs
- oven-sh/bun#46715: Preserves long integer source text in token-list serialization; this PR extends that behavior to exponent-form numbers.
- oven-sh/bun#47025: Changes the CSS parser and custom token serialization to preserve raw numeric token text, which is the same serialization path changed here.
Suggested reviewers: jarred-sumner
Priority:
Merge Risk: 
Minimal · up to 51fd7
-
Exponent spellings are intended to remain intact in token-preserved values. No material merge-blocking risk was identified.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description follows the template's two required sections, "What does this PR do?" (as "Problem", "Fix", "Background", "Downsides") and "How did you verify your code works?" (as "Fix" verification bullet plus detailed measurements). It explains the bug, the fix, the exception for escaped units, and the test expectations that changed. Verification names the test files run, the 19 failing rows on main, and the instruction and binary-size measurements. The description is very long and includes a large amount of supporting detail inside a collapsed notes block, but the required information is present and specific.
-
Finishing Touches: 💡 1
-
🛠️ Fix failing CI checks: 💡
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
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.
Comment @coderabbitai help to get the list of available commands.
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked the integer fast path in raw_number_text against the old digit-count rule — the tokenizer's saturating as i32 cast makes |int| >= 1_000_000 match the old "more than 6 digits after stripping leading zeros" rule for every integer token, including 01000000, -1000000, and overflowing digit runs — and checked that has_exponent only matches the e/E runs consume_numeric consumes as an exponent, so 1e-x, 1em, and 1.e3 still print as before.
Extended reasoning...
The change extends one helper in src/css/css_parser.rs that decides whether a numeric token kept as raw tokens prints its source text, adding an integer short-circuit, exponent detection, and an escape guard; it touches no security-sensitive surface. Two inline findings were posted (a browser-visible behavior change for z-index:1e3, and a pre-existing escape-merging gap the new guard only partially covers), which already signal that a human should weigh the behavioral tradeoff, so this body only records what else was examined and ruled out.
|
Reply to the review of 51fd762: no code change. The reasons and the evidence are in the two threads.
|
|
The description now states a second output that turns from right to wrong. No code change.
|
|
Merged main (da867c6) into this branch. The PR merges clean again.
|
Problem
bun buildchanges a number that has an exponent in a value kept as tokens (custom properties, unparsed values).--shf: 4.9406564584124654e-324prints--shf:0.--tag:8e64prints3.40282e38..cssfiles in 6 GitHub repositories contain that first number, and 5 divide by it. Main prints0for all 14 uses.Parser::raw_number_text(src/css/css_parser.rs:3603) keeps the text of a long number, but counts only the digits before the exponent.Fix
raw_number_textalso keeps the text of a token that has an exponent: itsf32can be another number (8e64,1e-50,1.6777217e7). No other function changes.1e3\70) prints as before, because its text can stop inside the escape.test/js/bun/css/css.test.ts, 19 rows fail on main. Also rantest/js/bun/css/,test/bundler/css/.debug_assert!.Background
f32. css: consolidate the open CSS parser fixes #47025 addedRawNumber, a number that prints its source text.Downsides
z-index:1e3and--t:1e3msprint as written (main:1000,1s). A browser drops thatz-index, as it drops the source. 12 GitHub.cssfiles have one.rgb(0 0 0)5e-1prints#0005e-1(main:#000.5), anda:unknown(1e3/**/.5)printsa:unknown(1e30.5)(main:1000.00.5, still 1000 and .5). Long fractions and leading zeros still change.raw_number_texta token with a fraction costs 145 instructions (main: 82), one without 57 (main: 84). Release.textgrows 256 bytes.Notes
What prints (
--minify, main against this branch; the same without--minify)--tag:8e643.40282e38--commit:12e45ab3.40282e38ab--small:1e-500--w:1.23456789e1012345679000--uuid:12345678-12e3-8d9b-a456-42661417400012345678-12000-8d9b-a456-426614174000--g:1e3e31000e3, and a second pass prints1000000--p:1e3%1000.0%.x{--tag:8e64}.y{--tag:9e99}.x,.y{--tag:3.40282e38}.x{--n:1e3}.y{--n:1E3}.x,.y{--n:1000}.a{z-index:7;z-index:1e3}.a{z-index:7;z-index:1000}width:calc(var(--a) + 1e3px)calc(var(--a) + 1000px)width:calc(7.3px*4.9406564584124654e-324/4.9406564584124654e-324)calc(7.3px*0/0)@unknown 1e3{a{b:1e3}}@unknown 1000{a{b:1000}}--t:1e3ms,--u:5e-11s,.5--x:8e64\70 x(a unit with an escape)3.40282e38pxwidth:1e39px,flex-grow:1e3(typed)3.40282e38px,1000--x:241.8301435407,--x:0000001241.83,1Main already prints
+1e3,1e3.5and1234567e2as written, because they have a+, touch another number or have 7 digits.1e3,1E3and1000are three texts now, so two rules that differ only in that spelling no longer merge.Real CSS
"4.9406564584124654e-324" extension:cssreturns 13 files in 6 repositories. The number is the smallest double.calc(X * N / N)roundsXto an integer with it.--shf:in 9 (KurtMage.github.io 7, kwy404/Chess, douglasspeck),--rounding:in 1 (chocmake.github.io), avar()fallback in 2 (86rk.ru) and inline in awidth:calc()in 1 (MnCSD/chess.com).0for all 14 uses, and this branch keeps all 14. 5 files divide by the number in the same file (kwy404, chocmake, 86rk twice, MnCSD). The other 8 declare--shfand do not read it in that file.max-widthrule of chocmake'scss/main.css:333and--max-width: 1223.4px: 1224px from the source, 0px with--rounding:0.width:calc(7.3px * var(--r) / var(--r))computes 7px and 0px.--radius-round: 1e5px, which main prints as100000px.--tag: 8e64for lightningcss (Minifier does not preserve values of CSS custom property parcel-bundler/lightningcss#807). esbuild 0.28.2 prints the exponent rows of the table as written, except that its--minifycuts1.23456789e10to1.23456789e1. lightningcss 1.33.0 prints these tokens from anf32, as main does (--tag:3.40282e38,--small:0).--uuid: 12345678-12e3-8d9b-a456-426614174000as its example of a custom property that must not be re-serialized from its value. Fractions and leading zeros still are.z-index:1e3(Chrome 153, computed style of the source against the output of main)CSS.supports("z-index", "1e3")andCSS.supports("order", "1e1")are false.z-index:7;z-index:1e3computes 7 and 1000.order:3;order:1e1computes 3 and 10.--n:1e3;z-index:var(--n)computesautoand 1000.order:1e1printing as10.z-index:1.0still prints1on both builds."z-index: 1e3","z-index: 1e6","z-index: 1e9"and"z-index: 1e10"withextension:cssreturns 12 files in 10 repositories. No warning is added for them.A comment between two numbers in
:unknown()(debug builds of the merge base 11c3671 and of this branch, the same with and without--minify)TokenList::parse_rawdrops a comment between two tokens, and the two tokens print with nothing between them.f32:1e3as1000.0and.5as0.5.1000.00.5still cuts into 1000 and .5. This branch prints1e3as written, and1e30.5cuts into 1e30 and .5.a:unknown(1.5/**/1e-3)prints1.51e-3(main:1.50.001).::unknown()and:state()take the same path.a:unknown(X/**/Y)anda:unknown(X/*c*/Y)with 32 tokensXand 36 tokensY(2,304 cells). A cell is right when the printed arguments cut into the written tokens. 248 cells are right on main and wrong here, and 82 are wrong on main and right here. Main is right in 828 cells and this branch in 662.XorYhas an exponent, and main printsXwith a dot andYwith a leading0..--x:1e3/**/.5prints1e3 .5.Measurements (release builds of 11c3671 without and with this change, x64 Linux)
qemu-x86_64 -one-insn-per-tb -d exec,nochain -dfilter.raw_number_textruns 757,191 instructions in 11,380 calls for the 15 files oftest/js/bun/css/fileswith--minify(main: 926,482). The fixtures have no exponent. 12 fixtures go down and 3 go up: animate.css +15,331, pico.css +6,773, hint.css +959.llvm-objdumpshows 0 differing instructions inconsume_numeric,parse_into_impl,parse_raw,TokenList::to_css,to_css_raw,dtoa_short,serialize_dimensionandwrite_numeric(4,263 instructions).consume_numericruns 5,250,426 instructions in 42,751 calls on both builds.raw_number_textis 739 bytes (main: 385)..textis 138,011,154 bytes (main: 138,010,898).dataandbssdo not change. No type changes and nothing allocates.--minify.consume_numeric. 387,841 print as written. The other 177,083 have an escape and print what main prints. No token without an exponent prints differently from main.The escape exception. The kept text of
1e3\70stops inside an escape. A blank that prints after it becomes part of the escape, and the unit reads on into the next token:a{--x:1e3\70/**/a}must stay1000p a. The check is "the text has a backslash", so a unit whose escape is complete (1e3\70 x) also prints as on main.Tests that moved. Two expectations in
angle-serialization-hang.test.ts:ro: 1.3157e308radandfilter: hue-rotate(1e50rad)are token lists and print as written. The new rowro: 1e50ra\64still sends a huge angle through a token list intoAngle::to_css.Not in this PR (the same on main and on this branch, unless a line says otherwise; I opened no tracking issues)
@media (foo:8e64)prints(foo:3.40282e38), and two such rules with different values merge. An unknown media feature does not passraw_number_text(MediaFeatureValue,src/css/media_query.rs).calc()folds inf32:width:calc(7.3px*1e-39/1e-39)prints3.40282e38px.rgb(0 0 0)5prints#0005. The colour arm ofTokenList::to_csswrites no separator. With this changergb(0 0 0)5e-1joins that case.--v:1.0.0-beta.0prints1.0.0-beta0and--x:1-0prints10.--x:1e3-.0prints10000on main and1e30here.a:unknown(1/**/5)printsa:unknown(15): the raw selector path drops a comment between two tokens.a:unknown(1e3/**/5)prints1000.05on main and1e35here. With this changea:unknown(1e3/**/.5)joins that case. The fix for all of them is a separator where the comment was, inTokenList::parse_raw.a{--x:1234567\70/**/a}prints1234567\70 a.a{--x:1000\65 3}prints1000e3: the printer escapes the uniteande-, and note3.f32reads as 0 (0.and 49 zeros, then1) prints0.Two questions for a maintainer
consume_numeric(css_parser.rs:4528) is now stated a second time, as a slice pattern inraw_number_text. One helper for both adds an edit to the function that every numeric token passes through. I can add it if you prefer one statement.raw_number_text. If a fourth comes, the other design is to keep the text of every token-list number and to shorten it as text.This is the exponent half of the first version of #46715.
[human-review] gate passed · iteration 0 · 3 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file