accepted/floating-point.md
(Issue)
This proposal standardizes Sass on using 64-bit floating-point numbers.
This section is non-normative.
In the original Ruby Sass implementation, numbers were represented using Ruby's
numeric stack. If a number was written without a decimal point in Sass (or
returned by an integer-valued function like red()), it would be represented as
an arbitrary-sized integer type that would transparently support integers of
arbitrary size. If it was written with a decimal point (or returned by a
float-valued function like random()), it used Ruby's floating-point
representation whose size varied based on how Ruby was compiled.
LibSass varied from this behavior by representing all numbers as 64-bit floating-point numbers.
Dart Sass initially matched Ruby Sass's implementation by virtue of the fact that Dart versions before 2.0.0 supported a similar transparently-updating integer stack. However, when Dart 2.0.0 was released its integer representation instead became fixed-size, and only guaranteed to be fully accurate up to 53 bits.
In addition to the specific details of numeric representation, Ruby Sass papered
over floating-point numbers' accuracy issues by defining a heuristic for
determining when similar numbers were considered equivalent to Sass's logic.
This heuristic has persisted relatively unchanged through to modern
implementations, but it introduces a problematic intransitivity in Sass's
equality semantics: 1 == 1.000000000005 and 1.000000000005 == 1.000000000010, but 1 != 1.000000000010. This also means that the hashing
Sass uses for its map keys is inherently flawed when dealing with numbers with
very small variations.
In practice, these changes rarely come up in practice because CSS tends to involve numbers within the well-behaved ranges almost exclusively. However, inconsistent edge cases can lead to severely bad user experiences as well as difficulty writing truly robust library code.
This section is non-normative.
This proposal standardizes Dart Sass on 64-bit IEEE 754 floating-point numbers,
like Dart, Java, and C#'s double type and—most pertinently—like JavaScript's
Number type. There will no longer be a separate representation of integers and
floating-point numbers, again similarly to JavaScript. In practice this is not a
large change, because Sass has always treated integer-like floating-point
numbers interchangeably with integers anyway.
This proposal also rationalizes Sass's numeric equality heuristic to make it
transitive. In particularly, two numbers will be considered equivalent if they
round to the same 1e-11. Using the example above, this will mean that 1 != 1.000000000005, 1.000000000005 == 1.000000000010, and 1 != 1.000000000010.
This proposal also adds numeric constants to the sass:math module that
represent various boundaries when dealing with floating-point values:
math.$epsilon: The difference between 1 and the smallest floating-point
number greater than 1.
math.$max-safe-integer: The maximum integer that can be represented "safely"
in Sass—that is, the maximum integer n such that n and n + 1 both have a
precise representation.
math.$min-safe-integer: The minimum integer that can be represented "safely"
in Sass—that is, the minimum integer n such that n and n - 1 both have a
precise representation.
math.$max-number: The maximum numeric value representable in Sass.
math.$min-number: The smallest positive numeric value representable in Sass.
This proposal introduces changes that cause observable behavioral differences which could, in principle, break existing Sass code. However, these differences are only observable in extremely large and extremely small numbers, or numbers that have extremely small differences between them. It's unlikely that this comes up often in practice.
Even more importantly, the existing behavior is clearly undesirable. Integer overflow depending on the internal state of a number object is user-hostile behavior, as is an intransitive equality operation. To the extent that these behaviors are observed by users, it's highly likely that they're seen as bugs where a change would be welcome.
Finally, there's not a realistic way for us to provide deprecation messaging for this change without dire performance implications. Given that, this proposal immediately changes the behavior of the language without a deprecation period.
The existing spec for Sass's suite of math functions carves out a number of
special cases where the mathematical functions have asymptotic behavior around a
particular integer argument. For example, since the tangent function tends to
infinity as its input approaches π/4 ± 2πn, Sass defined math.tan() to
return Infinity for any input that fuzzy-equals 90deg +/- 360deg * n.
However, this has a number of problems:
It's inconsistent with math.div(), which does not do this special-casing
for divisors very close to 0.
It's inconsistent with CSS Values and Units 4, which uses standard floating-point operations everywhere.
Most importantly, it runs the risk of losing information if the small differences between values are semantically meaningful.
Given these, we decided to introduce a rule of thumb. A number is always treated as a standard double except for:
A double is a floating-point datum representable in a format with
b = 2p = 53emax = 1023as defined by IEEE 754 2019, §3.2-3.3.
This is the standard 64-bit floating point representation, defined as
binary64in IEEE 754 2019, §3.6.
A set of units is structure with:
When not otherwise specified, a single unit refers to numerator units containing only that unit and empty denominator units.
Two doubles are said to be fuzzy equal to one another if either:
They are equal according to the compareQuietEqual predicate as defined
by IEEE 754 2019, §5.11.
They are both finite numbers and the mathematical numbers they represent produce the same value when rounded to the nearest 1e⁻¹¹ (with ties away from zero).
A SassScript number n is said to be an integer if there exists an integer
m with an exact double representation and n fuzzy equals that double.
If m exists, we say that n's integer value is the double that represents
m.
To avoid ambiguity, specification text will generally use the term "mathematical integer" when referring to the abstract mathematical objects.
Update the definition of compatible units as follows:
Two numbers' units are said to be compatible if both:
There's a one-to-one mapping between those numbers' numerator units such that each pair of units is either identical, or both units have a conversion factor and those two conversion factors have the same unit. This mapping is known as the numbers' numerator compatibility map.
There's the same type of mapping between those numbers' denominator units. This mapping is known as the numbers' denominator compatibility map.
Similarly, a number is compatible with a set of units if it's compatible with a number that has those units; and two sets of units are compatible if a number with one set is compatible with a number with the other.
This is not a functional change, it just makes it easier to refer to the details of compatibility between the two numbers.
Define the value type known as a number as three components:
Several shorthands exist when referring to numbers:
A number's units refers to the set of units containing its numerator units and denominator units.
A number is unitless if its numerator and denominator units are both empty.
A number is in a given unit (such as "in px") if it has that unit as its
single numerator unit and has no denominator units.
Let n1 and n2 be two numbers. To determine n1 == n2:
Let c1 and c2 be the result of matching units for n1 and n2. If this
throws an error, return false.
Return true if c1's value fuzzy equals c2's and false otherwise.
Let n1 and n2 be two numbers. To determine n1 >= n2:
Let c1 and c2 be the result of matching units for n1 and n2 allowing
unitless.
Return true if c1's value fuzzy equals c2's, or if
compareQuietGreaterEqual(c1.value, c2.value) returns true as defined by
IEEE 754 2019, §5.11. Return false otherwise.
Let n1 and n2 be two numbers. To determine n1 <= n2:
Let c1 and c2 be the result of matching units for n1 and n2 allowing
unitless.
Return true if c1's value fuzzy equals c2's, or if
compareQuietLessEqual(c1.value, c2.value) returns true as defined by IEEE
754 2019, §5.11. Return false otherwise.
Let n1 and n2 be two numbers. To determine n1 > n2, return n1 >= n2 and n1 != n2.
Let n1 and n2 be two numbers. To determine n1 < n2, return n1 <= n2 and n1 != n2.
Let n1 and n2 be two numbers. To determine n1 + n2:
Let c1 and c2 be the result of matching units for n1 and n2 allowing
unitless.
Return a number whose value is the result of addition(c1.value, c2.value) as defined by
IEEE 754 2019, §5.4.1; and whose units are the same as c1's.
Let n1 and n2 be two numbers. To determine n1 - n2:
Let c1 and c2 be the result of matching units for n1 and n2 allowing
unitless.
Return a number whose value is the result of subtraction(c1.value, c2.value)
as defined by IEEE 754 2019, §5.4.1; and whose units are the same as c1's.
Let n1 and n2 be two numbers. To determine n1 * n2:
Let product be a number whose value is the result of
multiplication(n1.value, n2.value) as defined by IEEE 754 2019, §5.4.1;
whose numerator units are the concatenation of n1's and n2's numerator
units; and whose denominator units are the concatenation of n1's and n2's
denominator units.
Return the result of simplifying product.
Let n1 and n2 be two numbers. To determine n1 % n2:
Let c1 and c2 be the result of matching units for n1 and n2 allowing
unitless.
Let remainder be a number whose value is the result of remainder(c1.value, c2.value) as defined by IEEE 754 2019, §5.3.1; and whose units are the same
as c1's.
If c2's value is less than 0 and remainder's value isn't 0 or -0,
return result - c2.
This is known as floored division. It differs from the standard IEEE 754 specification because it was originally inherited from Ruby when that was used for Sass's original implementation.
Note: These comparisons are not the same as
c2 < 0orremainder == 0, because they don't do fuzzy equality.
Otherwise, return result.
Let number be a number. To determine -number, return a number whose value is
the result of negate(number) as defined by IEEE 754 2019, §5.5.1; and whose
units are the same as number's.
This algorithm takes a SassScript number number and a set of units units.
It returns a number with the given units. It's written "convert number to
units" or "convert number to units allowing unitless".
If number is unitless and this procedure allows unitless, return
number with units.
Otherwise, if number's units aren't compatible with units, throw an
error.
Let value be number's value.
For each pair of units u1, u2 in the numerator compatibility
map between number and units such that u1 != u2:
Let v1 and v2 be the values of u1 and u2's conversion factors.
Set value to division(multiplication(value, v1), v2) as defined by
IEEE 754 2019, §5.4.1.
For each pair of units u1, u2 in the denominator compatibility map
between number and units such that u1 != u2:
Let v1 and v2 be the values of u1 and u2's conversion factors.
Set value to division(multiplication(value, v2), v1) as defined by
IEEE 754 2019, §5.4.1.
Return a number with value value and units units.
This algorithm takes two SassScript numbers n1 and n2 and returns two
numbers. It's written "match units for n1 and n2" or "match units for n1
and n2 allowing unitless".
If n1 is unitless and this procedure allows unitless, return n1
with the same units as n2 and n2.
Otherwise, if n2 is unitless and this procedure allows unitless, return n1
and n2 with the same units as n1.
Return n1 and the result of converting n2 to n1's units.
This algorithm takes a SassScript number number and returns an equivalent
number with simplified units.
Let mapping be a one-to-one mapping between number's numerator units and
its denominator units such that each pair of units is either identical, or
both units have a conversion factor and those two conversion factors have
the same unit.
Let newUnits be a copy of number's units without any of the units in
mapping.
newUnitsfor1px*px/pxispx, because only one of the numeratorpxis included in the mapping.
Return the result of converting number to newUnits.
$eA unitless number whose value is the closest possible double approximation of the mathematical constant e.
This is
2.718281828459045.
$piA unitless number whose value is the closest possible double approximation of the mathematical constant π.
This is
3.141592653589793.
$epsilonA unitless number whose value is the difference between 1 and the smallest double greater than 1.
This is
2.220446049250313e-16.
$max-safe-integerA unitless number whose value represents the maximum mathematical integer n
such that n and n + 1 both have an exact double representation.
This is
9007199254740991.
$min-safe-integerA unitless number whose value represents the minimum mathematical integer n
such that n and n - 1 both have an exact double representation.
This is
-9007199254740991.
$max-numberA unitless number whose value represents the greatest finite number that can be represented by a double.
This is
1.7976931348623157e+308.
$min-numberA unitless number whose value represents the least positive number that can be represented by a double.
This is
5e-324.
math.ceil()Replace this function's procedure with:
convertToIntegerTowardPositive($number.value) as defined by IEEE 754 2019,
§5.8; and whose units are the same as $number's.math.floor()Replace this function's procedure with:
convertToIntegerTowardNegative($number.value) as defined by IEEE 754 2019,
§5.8; and whose units are the same as $number's.math.round()Replace this function's procedure with:
convertToIntegerTiesToAway($number.value) as defined by IEEE 754 2019,
§5.8; and whose units are the same as $number's.math.abs()Replace this function's procedure with:
abs($number.value) as defined
by IEEE 754 2019, §5.5.1; and whose units are the same as $number's.math.log()Replace this function's procedure with:
If $number has units, throw an error.
Return a unitless number whose value is the result of log($number.value) as
defined by IEEE 754 2019, §9.2.
This is the natural logarithm.
math.pow()Replace this function's procedure with:
If $base or $exponent has units, throw an error.
Return a unitless number whose value is the result of pow($number.value) as
defined by IEEE 754 2019, §9.2.
math.sqrt()Replace this function's procedure with:
If $number has units, throw an error.
Return a unitless number whose value is the result of rootn($number.value, 2) as defined by IEEE 754 2019, §9.2.
math.acos()Replace this function's procedure with:
If $number has units, throw an error.
Let result be a number in rad whose value is the result of
acos($number.value) as defined by IEEE 754 2019, §9.2.
Return the result of converting result to deg.
math.asin()Replace this function's procedure with:
If $number has units, throw an error.
Let result be a number in rad whose value is the result of
asin($number.value) as defined by IEEE 754 2019, §9.2.
Return the result of converting result to deg.
math.atan()Replace this function's procedure with:
If $number has units, throw an error.
Let result be a number in rad whose value is the result of
atan($number.value) as defined by IEEE 754 2019, §9.2.
Return the result of converting result to deg.
math.atan2()Replace the last line of this function's procedure with:
Let result be a number in rad whose value is the result of
atan2($y.value, $x.value) as defined by IEEE 754 2019, §9.2.
Return the result of converting result to deg.
math.cos()Replace this function's procedure with:
Let double be the value of converting $number to rad allowing
unitless.
Return a unitless number whose value is the result of cos(double) as defined
by IEEE 754 2019, §9.2.
math.sin()Replace this function's procedure with:
Let double be the value of converting $number to rad allowing
unitless.
Return a unitless number whose value is the result of sin(double) as defined
by IEEE 754 2019, §9.2.
math.tan()Replace this function's procedure with:
Let double be the value of converting $number to rad allowing
unitless.
Return a unitless number whose value is the result of tan(double) as defined
by IEEE 754 2019, §9.2.
math.div()Replace the line
$number1's value by $number2's value.with
divide($number1.value, $number2.value) as defined
by IEEE 754 2019, §5.4.1.