負の任意値の正規化とcalc式の定数畳み込み強化
Tailwind CSSの正規化処理が強化され、負の任意値における - 符号の移動と、ネストした calc() 式の定数畳み込みが新たに実装されました。これにより -left-[9rem] → left-[-9rem] のような変換や、mt-[calc(-1*calc(-1*var(--foo)))] → mt-(--foo) のような多段階の最適化が可能になります。
背景
任意値(arbitrary values) は、デフォルトのスケール外の値を指定するためのエスケープハッチです。しかし -mt-[12rem] のように負のユーティリティと組み合わせると、クラス名の先頭 - が calc(<expression> * -1) の暗黙適用を意味するため、意図が読み取りにくくなります。特に任意値の中にすでに負の値や calc() が含まれている場合、二重否定や複雑なネストが生じていました。
また、既存の正規化パイプラインでは calc(-1*var(--foo)) と calc(var(--foo)*-1) は別々の式として扱われていました。これらが同一のシグネチャを持つと認識されない限り、重複クラスの検出や最適化の適用が妨げられます。本PRはこれらの問題を一括して解消します。
技術的な変更
本PRの変更は、大きく3つの機能追加で構成されています。
1. 負の符号のクラス境界をまたいだ移動
canonicalize-candidates.ts に、任意値を持つ負のユーティリティを正規化するロジックが追加されました。変換の方向は2通りあります。
符号を内側へ移動(-left-[9rem] → left-[-9rem]):
任意値を持つ負のユーティリティは、まず - を任意値の中に移動することが試みられます。これにより、値が正の数値スケールに変換可能であれば、さらにベア値へのフォールバックも可能になります。
-mt-[492px]
↓ 符号を内側へ
mt-[-492px]
↓ ベア値へ変換
-mt-123
符号を外側へ移動(mt-[calc(-1*var(--foo))] → -mt-(--foo)):
任意値が calc(-1 * <expr>) の形式である場合、-1 の乗算を取り除いてクラス名の先頭に - を付与します。これにより CSS 変数のショートハンド記法への変換も連鎖して適用されます。
mt-[calc(-1*var(--my-var))]
↓ 符号を外側へ
-mt-[var(--my-var)]
↓ ショートハンドへ
-mt-(--my-var)
canonicalize-candidates.test.ts には、二重否定・ネストした calc()・CSS変数ショートハンドへの変換を含む計16ケースのテストが追加されており、変換の網羅性が担保されています。
2. ネストした calc() 式の定数畳み込み強化
constant-fold-declaration.ts の constantFoldDeclaration 関数が大幅に拡張されました。既存の実装は2つの定数同士の演算のみを畳み込めましたが、今回の変更でネストした calc() の内側に定数と未知値(CSS変数など)が混在するケースにも対応します。
calc(2 * calc(3 * var(--foo)))
↓ 2 × 3 を畳み込み
calc(6 * var(--foo))
特に calc(-1 * calc(-1 * var(--foo))) のケースは、-1 × -1 = 1 が恒等演算として消滅し、var(--foo) だけが残ります。これが符号の多段階移動を支える核心的なロジックです。また、AST(抽象構文木)を直接受け取る constantFoldDeclarationAst 関数が新たに公開され、呼び出し側がパース済みASTを渡せるようになりました。
3. calc式の正規化比較(canonicalize-calc-expressions.ts)
新たに追加された canonicalizeCalcExpressions 関数は、calc() 式を「正規形」に変換することでシグネチャ比較の精度を高めます。具体的には + と * の二項演算においてオペランドを辞書順に並べ替えます。
calc(-1 * var(--foo)) → calc(var(--foo) * -1)
calc(1rem + var(--foo)) → calc(var(--foo) + 1rem)
この関数はクラスのシグネチャを計算する際にのみ使用され、実際の出力される任意値を書き換えるわけではありません。- を含む演算(calc(1rem - var(--foo)))や除算(calc(1rem / 2))、すでに正規形のもの(calc(var(--a) + 1rem))は変換されません。
シグネチャ比較のために COMPARE_CANDIDATES_KEY という新しいストレージキーが canonicalize-candidates.ts に追加され、createSignatureComparison 関数によってキャッシュされた比較関数が管理されます。
設計判断
符号の移動は正規形への一方向変換として設計されています。 left-[-9rem](符号が内側)を正規形とし、-left-[9rem](符号が外側)をそこへ変換する方向で統一されています。これは任意値が明示的な値を表すというセマンティクスを尊重した判断です。
calc式の正規化(canonicalizeCalcExpressions)は比較専用として実装を分離した点も注目に値します。出力される任意値を書き換えるのではなく、シグネチャ計算時のみに適用することで、ユーザーが記述した calc() の表記をそのまま保持できます。PR本文でも「少なくとも現時点では」と断った上で、この方針が明示されています。また、constantFoldDeclarationAst を独立した関数として公開したことで、パース済みASTの再利用が可能になり、同一式に対する複数処理のオーバーヘッドを削減する設計になっています。
まとめ
本PRは、負の任意値の正規化・ネストした calc() の定数畳み込み・calc式の正規化比較という3つの機能を組み合わせることで、従来は別物として扱われていたクラスを同一のシグネチャとして認識できるようにした変更です。正規化のパイプラインを段階的に組み合わせることで、個々の変換では到達できなかった mt-[calc(-1*calc(-1*var(--foo)))] → mt-(--foo) のような多段階最適化が実現しています。