32 KiB
Calculation Functions: Draft 3.2
Table of Contents
- Background
- Summary
- Definitions
- Syntax
- Types
- Operations
- Procedures
- Semantics
- Interaction with Forward Slash as a Separator
- API
- Embedded Protocol
- Deprecation Process
Background
This section is non-normative.
Sass added support for first-class calculation objects recently, but this
initial support only included the calc()
, min()
, max()
, and clamp()
expressions since these were the only ones supported in browsers at the time.
Since then, between Firefox and Safari browser support has landed for the rest
of the expressions listed in Values and Units 4.
Summary
This section is non-normative.
This proposal parses the full range of functions defined in Values and Units 4
as calculation values in Sass: round()
, mod()
, rem()
, sin()
, cos()
,
tan()
, asin()
, acos()
, atan()
, atan2()
, pow()
, sqrt()
, hypot()
,
log()
, exp()
, abs()
, and sign()
.
Since Sass already defines top-level functions named round()
and abs()
,
these will fall back to Sass function calls in a similar way to how min()
and
max()
already work.
This poses a small compatibility problem: in Sass, abs(10%)
will always return
10%
because the number is positive, but in plain CSS it could return the
equivalent of -10%
since percentages are resolved before calculations. To
handle this, we'll deprecate the global abs()
function with a percentage and
recommend users explicitly write math.abs()
or abs(#{})
instead.
This also expands calculation parsing to allow constructs like calc(1 var(--plus-two))
(where for example --plus-two: + 2
) which are valid CSS but
weren't supported by the old first-class calculation parsing.
Design Decisions
Merged Syntax
This proposal substantially changes the way calculations are parsed, merging the syntax with the standard Sass expression syntax. Now the only difference between a calculation and a normal Sass function is how it's evaluated. This has the notable benefit of allowing calculations to coexist with user-defined Sass functions of the same name, preserving backwards-compatibility.
Because this overlap is always going to be somewhat confusing for readers, we
considered simply disallowing Sass functions whose names matched CSS
calculations after a suitable deprecation period. However, in addition to the
intrinsic value of avoiding breaking changes, the function name rem()
in
particular is widely used in Sass libraries as a means of converting pixel
widths to relative ems, so this is a fairly substantial breaking change in
practice.
This does also require its own breaking change to the way interpolation is
handled in calculations—calc(#{"1px +"} 1%)
was formerly valid but is no
longer. However, this is likely to break many fewer users in practice, and is
relatively easy to continue supporting in a deprecated state in the short term.
Changing Mod Infinity Behavior
This proposal changes the behavior of the %
operation when the right-hand side
is infinite and has a different sign than the left-hand side. Sass used to
return the right-hand side in accordance with the floating point specification,
but it now returns NaN to match CSS's mod()
function.
Although this is technically a breaking change, we think it's highly unlikely that it will break anyone in practice, so we're not going to do a deprecation process for it.
Definitions
Calculation-Safe Expression
An expression is "calculation-safe" if it is one of:
- A
FunctionExpression
. - A
ParenthesizedExpression
whose contents is calculation-safe. - A
SumExpression
whose operands are calculation-safe. - A
ProductExpression
whose operator is*
or/
and whose operands are calculation-safe. - A
Number
. - A
Variable
. - An
InterpolatedIdentifier
. - An unbracketed
SpaceListExpression
with more than one element, whose elements are all calculation-safe.
Because calculations have special syntax in CSS, only a subset of SassScript expressions are valid (and these are interpreted differently than elsewhere).
Exact Equality
Two doubles are said to be exactly equal if they are equal according to the
compareQuietEqual
predicate as defined by IEEE 754 2019, §5.11.
This is as opposed to fuzzy equality.
Known Units
A number has known units unless it has unit %
.
This is relevant for calculations, because in plain CSS they resolve percentages before doing their operations. This means that any non-linear operations involving percentages must be passed through to plain CSS rather than handled by Sass.
More complex units involving percentages are allowed because any non-linear function will throw for complex units anyway.
Potentially Slash-Separated Number
Replace the definition of Potentially Slash-Separated Number
with the
following:
A Sass number may be potentially slash-separated. If it is, it is associated with two additional Sass numbers, the original numerator and the original denominator. A number that is not potentially slash-separated is known as slash-free.
A potentially slash-separated number is created when a ProductExpression
with
a /
operator is evaluated and each operand is syntactically one of the
following:
- a
Number
, - a
FunctionCall
, or - a
ProductExpression
that can itself create potentially slash-separated numbers.
If the result of evaluating the ProductExpression
is a number, that number is
potentially slash-separated if all of the following are true:
- the results of evaluating both operands were numbers, and
- if either operand was a
FunctionCall
, it was evaluated as a calculation and its name was not"abs"
,"max"
,"min"
, or"round"
.
If both of these are true, the first operand is the original numerator of the
potentially slash-separated number returned by the /
operator, and the second
is the original denominator.
Syntax
Calculations are no longer parsed differently than other Sass productions. Instead, they're evaluated differently at runtime. This allows them to coexist with user-defined Sass functions even when their names overlap.
FunctionExpression
Remove CssMinMax
and CalculationExpression
from the definition of
FunctionExpression
.
CssMinMax
Remove the CssMinMax
production.
CalculationExpression
Remove the CalculationExpression
production.
Types
Calculation
Delete the CalculationInterpolation
type and remove all references to it.
This type only existed to track where we needed to defensively insert parentheses. Now that we track parentheses as part of the calculation AST, this is no longer necessary
Operations
Modulo
Replace the definition of modulo for numbers with the following:
Differences are highlighted in bold.
Let n1
and n2
be two numbers. To determine n1 % n2
:
-
Let
c1
andc2
be the result of matching units forn1
andn2
allowing unitless. -
If
c2
is infinity and has a different sign thanc1
(including oppositely-signed zero), return NaN with the same units asc1
.This matches the behavior of CSS's
mod()
function. -
Let
remainder
be a number whose value is the result ofremainder(c1.value, c2.value)
as defined by IEEE 754 2019, §5.3.1; and whose units are the same asc1
's. -
If
c2
's value is less than 0 andremainder
's value isn't exactly equal to0
, returnremainder - c2
.This is known as floored division. It differs from the standard IEEE 754 specification, but matches the behavior of CSS's
mod()
function.Note: These comparisons are not the same as
c2 < 0
orremainder == 0
, because they don't do fuzzy equality. -
Otherwise, return
remainder
.
Procedures
Evaluating a FunctionCall
as a Calculation
This algorithm takes a FunctionCall
call
whose name is a plain identifier
and returns a number or a calculation.
-
If
call
'sArgumentInvocation
contains one or moreKeywordArgument
s or one or moreRestArgument
s, throw an error. -
Let
calc
be a calculation whose name is the lower-case value ofcall
's name and whose arguments are the result of evaluating eachExpression
incall
'sArgumentInvocation
as a calculation value. -
Return the result of simplifying
calc
.
Evaluating an Expression as a Calculation Value
This algorithm takes an expression expression
and returns a
CalculationValue
.
-
If
expression
isn't calculation-safe, throw an error. -
Otherwise, evaluate
expression
using the semantics defined in the Calculations specification if there are any, or the standard semantics otherwise.
Simplifying a Calculation
Replace the definition of "Simplifying a Calculation" with the following:
This algorithm takes a calculation calc
and returns a number or a calculation.
This algorithm is intended to return a value that's CSS-semantically identical to the input.
-
If
calc
was parsed from an expression within aSupportsDeclaration
'sExpression
, but outside any interpolation, return acalc
as-is. -
Let
arguments
be the result of simplifying each ofcalc
's arguments. -
If
calc
's name is"calc"
andarguments
contains exactly a single number or calculation, return it. -
If
calc
's name is"mod"
,"rem"
,"atan2"
, or"pow"
;arguments
has fewer than two elements; and none of those are unquoted strings, throw an error.It's valid to write
pow(var(--two-args))
orpow(#{"2, 3"})
, but otherwise calculations' arguments must match the expected number. -
If
calc
's name is"sin"
,"cos"
,"tan"
,"asin"
,"acos"
,"atan"
,"sqrt"
,"log"
, or"round"
andarguments
contains exactly a single number, return the result of passing that number to the function insass:math
whose name matchescalc
's.The
sass:math
functions will check units here for the functions that require specific or no units. -
If
calc
's name is"abs"
andarguments
contains exactly a single number with known units, return the result of passing that number to the function insass:math
whose name matchescalc
's. -
If
calc
's name is"exp"
andarguments
contains exactly a single numbernumber
, return the result of callingmath.pow(math.$e, number)
.This will throw an error if the argument has units.
-
If
calc
's name is"sign"
andarguments
contains exactly a single numbernumber
with known units:-
If
number
's value is positive, return1
. -
If
number
's value is negative, return-1
. -
Otherwise, return a unitless number with the same value as
number
.In this case,
number
is either+0
,-0
, or NaN.
To match CSS's behavior, these computations don't use fuzzy comparisons.
-
-
If
calc
's name is"log"
:-
If any argument is a number with units, throw an error.
-
Otherwise, if
arguments
contains exactly two numbers, return the result of passing its arguments to thelog()
function insass:math
.
-
-
If
calc
's name is"pow"
:-
If any argument is a number with units, throw an error.
-
Otherwise, if
arguments
contains exactly two numbers, return the result of passing those numbers to thepow()
function insass:math
.
-
-
If
calc
's name is"atan2"
andarguments
contains two numbers which both have known units, return the result of passing those numbers to theatan2()
function insass:math
.This will throw an error if either argument has units.
atan2()
passes percentages along to the browser because they may resolve to negative values, andatan2(-x, -y) != atan2(x, y)
. -
If
calc
's name is"mod"
or"rem"
:-
If
arguments
has only one element and it's not an unquoted string, throw an error. -
Otherwise, if
arguments
contains exactly two numbersdividend
andmodulus
:-
If
dividend
andmodulus
are definitely-incompatible, throw an error. -
If
dividend
andmodulus
are mutually compatible:-
Let
result
be the result ofdividend % modulus
. -
If
calc
's name is"rem"
, and ifdividend
is positive andmodulus
is negative or vice versa:- If
modulus
is infinite, returndividend
. - If
result
exactly equals 0, return-result
. - Otherwise, return
result - modulus
.
- If
-
Otherwise, return
result
.
-
-
-
-
If
calc
's name is"round"
:-
If
arguments
has exactly three elements, setstrategy
,number
, andstep
to those arguments respectively. -
Otherwise, if
arguments
has exactly two elements:-
If the first element is an unquoted string or interpolation with value
"nearest"
,"up"
,"down"
, or"to-zero"
, and the second argument isn't an unquoted string, throw an error.Normally we allow unquoted strings anywhere in a calculation, but this helps catch the likely error of a user accidentally writing
round(up, 10px)
without realizing that it needs a third argument. -
Otherwise, set
number
andstep
to the two arguments respectively andstrategy
to an unquoted string with value"nearest"
.
-
-
Otherwise, if the single argument isn't an unquoted string, throw an error.
-
If
strategy
,number
, andstep
are set:-
If
strategy
isn't a special variable string, nor is it an unquoted string or interpolation with value"nearest"
,"up"
,"down"
, or"to-zero"
, throw an error. -
If
strategy
is an unquoted string or interpolation and bothnumber
andstep
are numbers:-
If
number
andstep
are definitely-incompatible, throw an error. -
If
number
andstep
are mutually compatible:-
If
number
's andstep
's values are both infinite, ifstep
is exactly equal to 0, or if eithernumber
's orstep
's values are NaN, return NaN with the same units asnumber
. -
If
number
's value is infinite, returnnumber
. -
If
step
's value is infinite:-
If
strategy
's value is"nearest"
or"to-zero"
, return+0
ifnumber
's value is positive or+0
, and-0
otherwise. -
If
strategy
's value is"up"
, return positive infinity ifnumber
's value is positive,+0
ifnumber
's value is+0
, and-0
otherwise. -
If
strategy
's value is"down"
, return negative infinity ifnumber
's value is negative,-0
ifnumber
's value is-0
, and+0
otherwise.
-
-
Set
number
andstep
to the result of matching units fornumber
andstep
. -
If
number
's value is exactly equal tostep
's, returnnumber
. -
Let
upper
andlower
be the two integer multiples ofstep
which are closest tonumber
such thatupper
is greater thanlower
. Ifupper
would be 0, it's specifically-0
; iflower
would be zero, it's specifically-0
. -
If
strategy
's value is"nearest"
, return whichever ofupper
andlower
has the smallest absolute distance fromnumber
. If both have an equal difference, returnupper
. -
If
strategy
's value is"up"
, returnupper
. -
If
strategy
's value is"down"
, returnlower
. -
If
strategy
's value is"to-zero"
, return whichever ofupper
andlower
has the smallest absolute difference from 0.
-
-
-
-
-
If
calc
's name is"clamp"
:-
If
arguments
has fewer than three elements, and none of those are unquoted strings, throw an error. -
Otherwise, if any two elements of
arguments
are definitely-incompatible numbers, throw an error. -
Otherwise, if
arguments
are all mutually compatible numbers, return the result of callingmath.clamp()
with those arguments.
-
-
If
calc
's name is"hypot"
:-
If any two elements of
arguments
are definitely-incompatible numbers, throw an error. -
Otherwise, if all
arguments
are all numbers with known units that are mutually compatible, return the result of callingmath.hypot()
with those arguments.hypot()
has an exemption for percentages because it squares its inputs, sohypot(-x, -y) != -hypot(x, y)
.
-
-
If
calc
's name is"min"
or"max"
andarguments
are all numbers:-
If the arguments with units are all mutually compatible, call
math.min()
ormath.max()
(respectively) with those arguments. If this doesn't throw an error, return its result.min()
andmax()
allow unitless numbers to be mixed with units because they need to be backwards-compatible with Sass's old globalmin()
andmax()
functions. -
Otherwise, if any two of those arguments are definitely-incompatible, throw an error.
-
-
Otherwise, return a calculation with the same name as
calc
andarguments
as its arguments.
Simplifying a CalculationValue
Replace the block "If value
is a calculation" in the procedure for
simplifying a CalculationValue
with the following:
-
If
value
is a calculation:-
Let
result
be the result of simplifyingvalue
. -
If
result
isn't a calculation whose name is"calc"
, returnresult
. -
If
result
's argument isn't an unquoted string, returnresult
. -
If
result
's argument begins case-insensitively with"var("
; or if it contains whitespace,"/"
, or"*"
; return"(" +
result's argument+ ")"
as an unquoted string.This is ensures that values that could resolve to operations end up parenthesized if used in other operations. It's potentially a little overzealous, but that's unlikely to be a major problem given that the output is still smaller than including the full
calc()
and we don't want to encourage users to inject calculations with interpolation anyway.
-
Semantics
FunctionCall
Add the following to the semantics for FunctionCall
before checking for a
global function:
-
If
function
is null;name
is case-insensitively equal to"min"
,"max"
,"round"
, or"abs"
;call
'sArgumentInvocation
doesn't have anyKeywordArgument
s orRestArgument
s; and all arguments incall
'sArgumentInvocation
are calculation-safe, return the result of evaluatingcall
as a calculation.For calculation functions that overlap with global Sass function names, we want anything Sass-specific like this to end up calling the Sass function. For all other calculation functions, we want those constructs to throw an error (which they do when evaluating
call
as a calculation). -
If
function
is null andname
is case-insensitively equal to"calc"
,"clamp"
,"hypot"
,"sin"
,"cos"
,"tan"
,"asin"
,"acos"
,"atan"
,"sqrt"
,"exp"
,"sign"
,"mod"
,"rem"
,"atan2"
,"pow"
, or"log"
, return the result of evaluatingcall
as a calculation.
Calculations
Remove all prior semantics for Calculations. The following semantics apply only when evaluating expressions as calculation values.
FunctionExpression
and Variable
To evaluate a FunctionExpression
or a Variable
as a calculation value,
evaluate it using the standard semantics. If the result is a number, an unquoted
string, or a calculation, return it. Otherwise, throw an error.
Allowing variables to return unquoted strings here supports referential transparency, so that
$var: fn(); calc($var)
works the same ascalc(fn())
.
SumExpression
and ProductExpression
To evaluate a SumExpresssion
or a ProductExpression
as a calculation value:
-
Let
left
be the result of evaluating the first operand as a calculation value. -
For each remaining
"+"
,"-"
,"*"
, or"/"
tokenoperator
and operandoperand
:-
Let
right
be the result of evaluatingoperand
as a calculation value. -
Set
left
to aCalcOperation
withoperator
,left
, andright
.
-
-
Return
left
.
SpaceListExpression
To evaluate a SpaceListExpresssion
as a calculation value:
-
Let
elements
be the results of evaluating each element as a calculation value. -
If
elements
has two adjacent elements that aren't unquoted strings, throw an error.This ensures that valid CSS constructs like
calc(1 var(--plus-two))
and similar Sass constructs likecalc(1 #{"+ 2"})
work while preventing clear errors likecalc(1 2)
.This does allow errors like
calc(a b)
, but the complexity of verifying that the unquoted strings could actually be a partial operation isn't worth the benefit of eagerly producing an error in this edge case. -
Let
serialized
be an empty list. -
For each
element
ofelements
:-
Let
css
be the result of serializingelement
. -
If
element
is aCalcOperation
that was produced by evaluating aParenthesizedExpression
, setcss
to"(" + css + ")"
. -
Append
css
toserialized
.
-
-
Return an unquoted strings whose contents are the elements of
serialized
separated by" "
.
ParenthesizedExpression
If a
var()
or an interpolation is written directly within parentheses, it's necessary to preserve those parentheses. CSS resolvesvar()
by literally replacing the function with the value of the variable and then parsing the surrounding context.For example, if
--ratio: 2/3
,calc(1 / (var(--ratio)))
is parsed ascalc(1 / (2/3)) = calc(3/2)
butcalc(1 / var(--ratio))
is parsed ascalc(1 / 2/3) = calc(1/6)
.
To evaluate a ParenthesizedExpression
with contents expression
as a
calculation value:
-
Let
result
be the result of evaluatingexpression
as a calculation value. -
If
result
is an unquoted string, return"(" + result + ")"
as an unquoted string. -
Otherwise, return
result
.
InterpolatedIdentifier
To evaluate an InterpolatedIdentifier
ident
as a calculation value:
-
If
ident
is case-insensitively equal topi
, return 3.141592653589793.This is the closest double approximation of the mathematical constant π.
-
If
ident
is case-insensitively equal toe
, return 2.718281828459045.This is the closest double approximation of the mathematical constant e.
-
If
ident
is case-insensitively equal toinfinity
, return the doubleInfinity
. -
If
ident
is case-insensitively equal to-infinity
, return the double-Infinity
. -
If
ident
is case-insensitively equal tonan
, return the doubleNaN
. -
Otherwise, return the result of evaluating
ident
using standard semantics.This will be an
UnquotedString
.
Interaction with Forward Slash as a Separator
Although the Forward Slash as a Separator proposal has not yet been integrated into the canonical spec, it will affect some of the constructs modified by this proposal. This section defines additional modifications to the spec as it will exist when that proposal is integrated.
Remove "or /
" from the definition of a calculation-safe ProductExpression
.
Add "An unbracketed SlashListExpression
with more than one element, all of
which are calculation-safe" to the list of calculation-safe expressions.
Replace "evaluating each Expression
" with "adjusting slash precedence in and
then evaluating each Expression
" in evaluting a FunctionCall
as a
calculation.
Adjusting Slash Precedence
This algorithm takes a calculation-safe expression expression
and returns
another calculation-safe expression with the precedence of
SlashListExpression
s adjusted to match division precedence.
-
Return a copy of
expression
except, for eachSlashListExpression
:-
Let
left
be the first element of the list. -
For each remaining element
right
:-
If
left
andright
are bothSumExpression
s:-
Let
last-left
be the last operand ofleft
andfirst-right
the first operand ofright
. -
Set
left
to aSumExpression
that begins with all operands and operators ofleft
exceptlast-left
, followed by aSlashListExpression
with elementslast-left
andfirst-right
, followed by all operators and operands ofright
exceptfirst-right
.For example,
slash-list(1 + 2, 3 + 4)
becomes1 + (2 / 3) + 4
.
-
-
Otherwise, if
left
is aSumExpression
:-
Let
last-left
be the last operand ofleft
. -
Set
left
to aSumExpression
that begins with all operands and operators ofleft
exceptlast-left
, followed by aSlashListExpression
with elementslast-left
andright
.For example,
slash-list(1 + 2, 3)
becomes1 + (2 / 3)
.
-
-
Otherwise, if
right
is aSumExpression
or aProductExpression
:-
Let
first-right
be the first operand ofright
. -
Set
left
to an expression of the same type asright
that begins aSlashListExpression
with elementsleft
andfirst-right
, followed by operators and operands ofright
exceptfirst-right
.For example,
slash-list(1, 2 * 3)
becomes(1 / 2) * 3
.
-
-
Otherwise, if
left
is a slash-separated list, addright
to the end. -
Otherwise, set
left
to a slash-separated list containingleft
andright
.
-
-
Replace each element in
left
with the result of adjusting slash precedence in that element. -
Replace the
SlashListExpression
withleft
in the returned expression.
-
SlashListExpression
To evaluate a SlashListExpression
as a calculation value:
-
Let
left
be the result of evaluating the first element of the list as a calculation value. -
For each remaining element
element
:-
Let
right
be the result of evaluatingelement
as a calculation value. -
Set
left
to aCalcOperation
with operator"/"
,left
, andright
.
-
-
Return
left
.
API
Types
CalculationInterpolation
Replace the definition of this class, other than its TypeScript API, with the following:
A deprecated alternative JS API representation of an unquoted Sass string that's always surrounded by parentheses. It's never returned by the Sass compiler, but for backwards-compatibility users may still construct it and pass it to the Sass compiler.
CalculationInterpolation
s are no longer generated by the Sass compiler, because it can now tell at evaluation time whether an interpolation was originally surrounded by parentheses. However, until we make a breaking revision of the JS API, users may continue to passCalculationInterpolation
s
internal
A private property like Value.internal
that refers to a Sass string.
Constructor
Creates a CalculationInterpolation
with its internal
set to an unquoted Sass
string with text "(" + value + ")"
and returns it.
value
Returns internal
's value
field's text, without the leading and
trailing parentheses.
equals
Whether other
is a CalculationInterpolation
and internal
is
equal to other.internal
in Sass.
hashCode
Returns the same number for any two CalculationInterpolation
s that are equal
according to equals
.
Embedded Protocol
CalculationValue.value.interpolation
Add the following to this field's documentation:
The compiler must treat this as identical to a string
option whose value is
"(" + interpolation + ")"
.
This field is deprecated and hosts should avoid using it.
Deprecation Process
This proposal causes two breaking changes, each of which will be mitigated by supporting something very close to the old behavior with a deprecation warning until the next major version release.
abs-percent
Under this proposal, if a number with unit
%
is passed to the globalabs()
function, it will be emitted as a plain CSSabs()
rather than returning the absolute value of the percentage itself.
During the deprecation period, when simplifying a calculation named "abs"
whose sole argument is a number without known units, return the result of
calling math.abs()
with that number and emit a deprecation warning named
abs-percent
.