Repository navigation
fix(css): resolve <number> operands in calc() - #44
Merged
LastLeaf merged 3 commits intoOct 26, 2026
Merged
Conversation
`calc(0.5 * (1rem - 16px))` — the half-scale formula used by the official elder-transform toolchain — is parsed as `Mul(Sub(1rem, 16px), Number(0.5))` (the parser normalizes the number into the right operand), because the operands cannot be folded at parse time. `resolve_to_f32` returned `None` for `CalcExpr::Number`, so the whole expression was unresolvable and the property silently fell back to its default value. Resolve `<number>` nodes when they are the factor of a `*` or the divisor of a `/`, as `<number>` is the empty type map in css-values-4 §10.9: a bare number (`calc(2 * 3)`) and a number added to a length (`calc(2 + 3px)`) are still rejected.
`calc((1rem - 16px) * (1 + 0.5))` failed to parse: `*` and `/` fold their numeric operands at parse time, but `+` and `-` kept a `Plus(Number, Number)` tree, which the enclosing `*` rejected because neither operand was a number. Fold a numeric sum the same way, so a numeric sub-expression always ends up as a literal no matter where it appears.
gaojunran
force-pushed
the
fix/calc-number-operands
branch
2 times, most recently
from
October 22, 2026 05:27
2400c0e to
6076d26
Compare
`font-size: calc(17px + 0.5 * (1rem - 16px))` used to resolve to the inherited 16px because the `<number>` operand was unresolvable; it now resolves to 17px at the 16px root font size. The tree still keeps `Mul(Sub(1rem, 16px), Number(0.5))` instead of distributing it into `Plus(Px(9), Rem(0.5))`. That is the algebraic simplification described in css-values-4 §10.10.1, which is a separate piece of work and does not change the resolved value.
gaojunran
force-pushed
the
fix/calc-number-operands
branch
from
October 22, 2026 05:33
6076d26 to
85b7807
Compare
LastLeaf
approved these changes
Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
calc()cannot resolve a<number>operand, so several spec-valid expressions silently fall back to the property default:calc(0.5 * (1rem - 16px) + 17px)17px@ 16px basecalc(2 * min(10px, 5vw))20pxcalc((100% - 10px) / 2)3px(16px reference)calc((1rem - 16px) * (1 + 0.5))12px@ 24px baseThe first one is the half-scale formula used by the official
miniprogram-elder-transformtoolchain for font scaling, andfloat-pigment-css/tests/calc.rsalready carries aFIXMEfor exactly that expression.Root cause
Two independent gaps, both around
<number>:CalcExpr::resolve_to_f32returnsNoneforCalcExpr::Number, i.e. both operands of*and/had to resolve as lengths. Per css-values-4 §10.9 a<number>is the empty type map, solength * numberandlength / numberare still lengths. Worse, the parser folds every literal case and rejectslength * length, so anyMul/Divnode that survives parsing necessarily has a<number>operand — the old code resolved none of them.*and/fold their both-number operands into a literal, but+and-keep aPlus(Number, Number)tree, which an enclosing*then rejects because neither operand is aCalcExpr::Number.Fix
CalcExpr::resolve_number(), which resolves the scalar side of a*or/;MulandDivuse it for the right operand.parse_calc_sum_expr, mirroring the product rule, so a numeric sub-expression always ends up as a literal no matter where it appears — parenthesized, insidemin()/max()/clamp(), or at the top level.A bare number (
calc(2 * 3)) and a number added to a length (calc(2 + 3px)) are still rejected, as required by the type rules.