Skip to content

Avoid floating point noise when constant folding calc() and unit converstion - #20585

Open
otarampinelli wants to merge 3 commits into
tailwindlabs:mainfrom
otarampinelli:fix/constant-fold-float-noise
Open

otarampinelli wants to merge 3 commits into
tailwindlabs:mainfrom
otarampinelli:fix/constant-fold-float-noise

Conversation

@otarampinelli

@otarampinelli otarampinelli commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Constant folding on constant-fold-declaration.ts were computing every calc() fold and unit conversion, so decimal inputs leakead floating point noise into canonicalization

examples:

// before
canonicalize('w-[calc(0.1rem+0.2rem)]') // 'w-[0.30000000000000004rem]'
canonicalize('w-[calc(0.3rem/0.1)]')    // 'w-[calc(0.3rem/0.1)]' (not folded)
canonicalize('w-[25.4mm]')              // 'w-[25.4mm]'
canonicalize('w-[12.7cm]')              // 'w-[12.7cm]'

// With `--spacing: 0.2rem`
canonicalize('w-[0.6rem]')              // 'w-[0.6rem]'
canonicalize('w-[1.4rem]')              // 'w-[1.4rem]'

// after
canonicalize('w-[calc(0.1rem+0.2rem)]') // 'w-[0.3rem]'
canonicalize('w-[calc(0.3rem/0.1)]')    // 'w-12'
canonicalize('w-[25.4mm]')              // 'w-24'
canonicalize('w-[12.7cm]')              // 'w-120'

// With `--spacing: 0.2rem`
canonicalize('w-[0.6rem]')              // 'w-3'
canonicalize('w-[1.4rem]')              // 'w-7'

the compiled css isn't affected, only canonicalization because it feeds the canonicalize CLI, the upgrade codemod and IntelliSense, so the noise could be written into users source files, it also made equivalent values compare as different:

// Before: the same length, three different canonical values
constantFoldDeclaration('1cm', 16)  // '37.79527559055118px'
constantFoldDeclaration('10mm', 16) // '37.795275590551185px'
constantFoldDeclaration('40q', 16)  // '37.795275590551185px'

// After
constantFoldDeclaration('1cm', 16)  // '37.7952755906px'
constantFoldDeclaration('10mm', 16) // '37.7952755906px'
constantFoldDeclaration('40q', 16)  // '37.7952755906px'

solution approach:

the operands are already decimal string in the css, so this does just the math on those strings

  • new decimals utility where a number is parsed from its written form into a int plus a scale, like 4.35 -> { int: 435, scale: 2 }, and and, sub, mul and div work on int numbers, div returns a result only if it ends whitin 2 decimals, the same limit as the current implementation, any input that isn't a plain decimal, or any intermediate value that isn't a safe integer returns null
  • unit conversion uses rational factors, cm = ×4800/127, mm = ×480/127, q = ×120/127 and pt = ×4/3are exact multiplication plus one float division, round to 12 significant digits, so 1cm, 10mm and 40q cannonicalize identically, in, pc, rem, grad, turn, ms and khz convert exactly, rad use pi, so it is computed with float and rounded to 12 significant digits, significant digits (not decimal places) keep the relative error the same at any magnitude, so a tiny converted value that a calc() multiplies back up stays correct, like calc(0.00000000001pt * 1000000000000) → 10pt instead of 0, values that can't be written with 12 digits without an exponent aren't converted
  • the spacing ration in createSpacingCache is exact with a fallback to the float ration for values that aren't 2 decimal multipliers, the fallback keeps line-height matching such as 28.5px / 4px = 7.125 → text-sm/[28.5px] working
  • unitless and unkown values are normalized as written intead of going through Number, so items behave like 1.50 → 1.5 and -0 → 0, this also stops output like 1e-7, 1e+200px and Infinity

stackblitz exmaple using main code: https://stackblitz.com/github/otarampinelli/tw-canon-repro?file=repro.mjs

Test plan

• src/utils/decimal.test.ts every export, including each null case, unsafe integers, division by zero, maxDecimals, no exponent output, -0
• src/constant-fold-declaration.test.ts noise free + - * / and nested re association, the 2 decimal division limit, rem at 14 and 18, exact conversions and cm/mm/q equivalence, no exponent notation, no overflow or precision loss, and rad rounding
• src/canonicalize-candidates.test.ts end-to-end rows from the table above, plus rotate-[0.785398163397rad] → rotate-45 and rotate-[1rad] staying arbitrary.

@otarampinelli
otarampinelli requested a review from a team as a code owner October 16, 2026 18:51
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9859b78d-7890-4d87-a66d-b347143409ff
📥 Commits

Reviewing files that changed from the base of the PR and between c9dc08b and 8e52a74.

📒 Files selected for processing: 4
  • packages/tailwindcss/src/canonicalize-candidates.test.ts
  • packages/tailwindcss/src/constant-fold-declaration.test.ts
  • packages/tailwindcss/src/utils/decimal.test.ts
  • packages/tailwindcss/src/utils/decimal.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Walkthrough

The change adds decimal-string arithmetic and uses it in CSS declaration folding, dimension conversion, and spacing multiplier calculation. The tests cover decimal formatting, arithmetic precision and limits, unit conversion, and spacing and angle canonicalization.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 8e52a

The change replaces floating-point arithmetic with decimal-string arithmetic when canonicalizing CSS values. No concrete merge-blocking risk was found in the supplied context.

Pre-merge checks: Passed 4
Check name Status Explanation
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.
Title check Passed The title clearly summarizes the main change: reducing floating-point noise during constant folding and unit conversion. It contains a minor spelling error in “converstion,” but remains clear and spec…
Description check Passed The description directly explains the floating-point issue, the decimal-arithmetic solution, affected canonicalization behavior, and planned tests.
Full details: Title check

Explanation

The title clearly summarizes the main change: reducing floating-point noise during constant folding and unit conversion. It contains a minor spelling error in “converstion,” but remains clear and specific.

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

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] No separate new finding blocks this update.

Reviews (2) · Last reviewed commit: "rounding unit conversions to significant..." · Reviewed by Greptile

Comment thread packages/tailwindcss/src/utils/decimal.ts Outdated
@otarampinelli otarampinelli changed the title Avoid floating point noise when contastand folding calc() and unit converstion Avoid floating point noise when constant folding calc() and unit converstion Oct 16, 2026
@greptile-apps

greptile-apps Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

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.

1 participant