sass/accepted/floating-point.md

22 KiB

Floating Point Numbers: Draft 1.2

(Issue)

This proposal standardizes Sass on using 64-bit floating-point numbers.

Table of Contents

Background

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.

Summary

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.

Potentially-Breaking Changes

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.

Design Decisions

Math Function Special Cases

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:

  • explicit Sass-level equality comparisons (including map access),
  • rounding RGB color channels (until we support Color Level 4),
  • and serializing a number to CSS.

Definitions

Double

A double is a floating-point datum representable in a format with

  • b = 2
  • p = 53
  • emax = 1023

as defined by IEEE 754 2019, §3.2-3.3.

This is the standard 64-bit floating point representation, defined as binary64 in IEEE 754 2019, §3.6.

Set of Units

A set of units is structure with:

  • A list of strings called "numerator units".
  • A list of strings called "denominator units".

When not otherwise specified, a single unit refers to numerator units containing only that unit and empty denominator units.

Fuzzy Equality

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

Integer

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.

Compatible Units

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.

Types

Define the value type known as a number as three components:

  • A double called its "value".
  • A list of strings called numerator units.
  • A list of strings called denominator units.

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.

Operations

Equality

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.

Greater Than or Equal To

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.

Less Than or Equal To

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.

Greater Than

Let n1 and n2 be two numbers. To determine n1 > n2, return n1 >= n2 and n1 != n2.

Less Than

Let n1 and n2 be two numbers. To determine n1 < n2, return n1 <= n2 and n1 != n2.

Addition

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.

Subtraction

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.

Multiplication

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.

Modulo

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 < 0 or remainder == 0, because they don't do fuzzy equality.

  • Otherwise, return result.

Negation

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.

Procedures

Converting a Number to Units

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.

Matching Two Numbers' 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].

Simplifying a Number

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.

    newUnits for 1px*px/px is px, because only one of the numerator px is included in the mapping.

  • Return the result of converting number to newUnits.

Variables

$e

A unitless number whose value is the closest possible double approximation of the mathematical constant e.

This is 2.718281828459045.

$pi

A unitless number whose value is the closest possible double approximation of the mathematical constant π.

This is 3.141592653589793.

$epsilon

A unitless number whose value is the difference between 1 and the smallest double greater than 1.

This is 2.220446049250313e-16.

$max-safe-integer

A 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-integer

A 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-number

A unitless number whose value represents the greatest finite number that can be represented by a double.

This is 1.7976931348623157e+308.

$min-number

A unitless number whose value represents the least positive number that can be represented by a double.

This is 5e-324.

Functions

Bounding Functions

math.ceil()

Replace this function's procedure with:

  • Return a number whose value is the result of 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:

  • Return a number whose value is the result of 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:

  • Return a number whose value is the result of convertToIntegerTiesToAway($number.value) as defined by IEEE 754 2019, §5.8; and whose units are the same as $number's.

Distance Functions

math.abs()

Replace this function's procedure with:

  • Return a number whose value is the result of abs($number.value) as defined by IEEE 754 2019, §5.5.1; and whose units are the same as $number's.

Exponential Functions

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.

Trigonometric Functions

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:

math.cos()

Replace this function's procedure with:

math.sin()

Replace this function's procedure with:

math.tan()

Replace this function's procedure with:

Other Functions

math.div()

Replace the line

  • Its value is the result of dividing $number1's value by $number2's value.

with

  • Its value is the result of divide($number1.value, $number2.value) as defined by IEEE 754 2019, §5.4.1.