accepted/slash-separator.md
This proposal modifies the / character to be used exclusively as a separator,
and lays out a process for deprecating its existing usage as a division
operator.
This section is non-normative.
Early on in Sass's history, the decision was made to use / as a division
operator, since that was (and is) by far the most common representation across
programming languages. The / character was used in very few plain CSS
properties, and for those it was an optional shorthand. So Sass defined a set
of heuristics that defined when / would be rendered as a literal slash
versus treated as an operator.
For a long time, these heuristics worked pretty well. In recent years, however,
new additions to CSS such as CSS Grid and CSS Color Level 4 have been
using / as a separator increasingly often. Using the same character for both
division and slash-separation is becoming more and more annoying to users, and
will likely eventually become untenable.
This section is non-normative.
We will redefine / to be only a separator. Rather than creating an unquoted
string (as it currently does when at least one operand isn't a number), it will
create a slash-separated list. As such, lists will now have three possible
separators: space, comma, and slash.
Division will instead be written as a function, math.div(). Eventually, it
will also be possible to write Sass-compatible division in calc() expressions;
however, this is not going to be implemented immediately and is outside the
scope of this proposal.
This is a major breaking change to existing Sass semantics, so we'll roll it out in a three-stage process:
The first stage won't introduce any breaking changes. It will:
Add a math.div() function which will work exactly like the / operator
does today, except that it will produce deprecation warnings for any
non-number arguments.
Add slash-separated lists to Sass's object models, without a literal syntax for creating them. That will come later, since it would otherwise be a breaking change.
Add a list.slash() function that will create slash-separated lists.
Produce deprecation warnings for all / operations that are interpreted as
division.
The second stage will be a breaking change. It will:
Make / exclusively a list separator.
Make math.div() throw errors for non-number arguments.
Deprecate the list.slash() function, since it will now be redundant.
The third stage will just remove the list.slash() function. This is not a
priority, and will be delayed until the next major version release.
This section is non-normative.
One other possible fix would be to change the syntax for division to another punctuation-based operator. We ended up deciding that anything we chose would be so different from every other programming language as to be unreadable for anyone unfamiliar with the language.
In addition, the best candidate operator we found was ~, as an ASCII character
that wasn't already in use in CSS or Sass value syntax. But ~ is difficult to
type on many non-English keyboard layouts, which makes it only marginally more
efficient to write than a function call for many Sass users.
calc()We eventually want to add native Sass support for parsing calc() expressions,
resolving them at compile-time if possible, and producing a new Sass value that
can have arithmetic performed on it if necessary. This is known as first-class
calc(), and it would mean that division could be written unambiguously
using / in the context of a calc() expression. For example, $width / 2
would be instead written calc($width / 2).
However, first-class calc() is likely to be a very complex feature to design
and implement. Most of the resources available for large-scale language features
are currently focused on the new module system, so it's likely that a full
implementation of first-class calc() won't land until mid-to-late 2020. And
the full implementation is a prerequisite for even beginning the deprecation
cycle for /-as-division, which means we probably wouldn't fully support
/-as-separator for another three to six months after that point. This is just
too much time to wait on giving users a good solution for writing /-separated
properties.
math() SyntaxA possible middle ground between calc() and the current syntax would be using
a special math() expression as a way of signaling a syntactic context where
/ is interpreted as division without needing to fully support all the edge
cases of calc(). For example, $width / 2 would be instead written
math($width / 2).
Unfortunately, this is highly likely to confuse users. They may think that
math() is necessary for all mathematical operations, when in fact it's only
necessary for division, which would lead to confusing and unnecessary math()
expressions popping up all over the place. They may also think it's a Sass
library function or a plain CSS feature, neither of which is true, and look in
the wrong place for documentation.
Worse, once we did add support for first-class calc(), there would then be
two different ways of wrapping mathematical expressions which had slightly but
meaningfully different semantics. This is a recipe for making users feel
confused and overwhelmed.
To precisely describe when a deprecation warning should be emitted, we must first describe the existing heuristic behavior.
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 both operands are syntactically either literal
Numbers or ProductExpressions that can themselves create potentially
slash-separated numbers. In this case, both operands are guaranteed to be
evaluated as numbers. The first operand is the original numerator of the
potentially slash-separated number returned by the / operator, and the second
is the original denominator.
A potentially slash-separated number is converted to a slash-free number when:
It is the value of a ParenthesizedExpression.
That is, it's in parentheses, such as in
(1 / 2). Note that if it's in a list that's in parentheses, it's not converted to a slash-free number.
It is stored in a Sass variable.
It is passed into a user-defined function or mixin.
It is returned by a function.
Any expressions that normally produce a new number (such as other mathematical operations) always produce slash-free numbers, even when their arguments are slash-separated.
When a potentially slash-separated number is "converted" to a slash-free number, a slash-free copy is made of the original. Sass values are always immutable.
When a potentially slash-separated number is converted to CSS, either when
converted to a string via interpolation or when included in a declaration's
value, it is written as the original numerator followed by / followed by the
original denominator. If either the original numerator or denominator are
themselves slash-separated, they're also written this way.
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.
Note that the existing productions being modified have not been defined explicitly before this document. The old definitions are listed in strikethrough mode to clarify the change.
This proposal modifies the existing CommaListExpression production to add
support for slash-separated lists. The new grammar for this production is:
<x><pre>
CommaListExpression ::= SpaceListExpression (',' SpaceListExpression)*
CommaListExpression ::= SlashListExpression (',' SlashListExpression)*
SlashListExpression ::= SpaceListExpression ('/' SpaceListExpression)*
</pre></x>
Note that
/may not be used in single-element lists the way,is. That is,(foo,)is valid, but(foo/)is not.This defines
/to bind tighter than,but looser than space-separated lists. This was chosen because most common uses of/in CSS conceptually bind looser than space-separated values. The only exception is thefontshorthand syntax, which is used much more rarely will still work (albeit with an unintuitive SassScript representation) with a loose-binding/.
It also modifies the existing ProductExpression production to remove / as an
operator. The new grammar for this production is:
<x><pre>
ProductExpression ::= (ProductExpression ('*' | '/' | '%'))? UnaryPlusExpression
ProductExpression ::= (ProductExpression ('*' | '%'))? UnaryPlusExpression
</pre></x>
When a SlashListExpression with one or more /s is evaluated, it produces a
list object whose contents are the values of its constituent
SpaceListExpressions and whose separator is "slash".
FunctionCall as a CalculationReplace "evaluating each Expression" with "adjusting slash precedence in and
then evaluating each Expression" in evaluting a FunctionCall as a
calculation.
This algorithm takes a calculation-safe expression expression and returns
another calculation-safe expression with the precedence of
SlashListExpressions adjusted to match division precedence.
Return a copy of expression except, for each SlashListExpression:
Let left be the first element of the list.
For each remaining element right:
If left and right are both SumExpressions:
Let last-left be the last operand of left and first-right the
first operand of right.
Set left to a SumExpression that begins with all operands and
operators of left except last-left, followed by a
SlashListExpression with elements last-left and first-right,
followed by all operators and operands of right except first-right.
For example,
slash-list(1 + 2, 3 + 4)becomes1 + (2 / 3) + 4.
Otherwise, if left is a SumExpression:
Let last-left be the last operand of left.
Set left to a SumExpression that begins with all operands and
operators of left except last-left, followed by a
SlashListExpression with elements last-left and right.
For example,
slash-list(1 + 2, 3)becomes1 + (2 / 3).
Otherwise, if right is a SumExpression or a ProductExpression:
Let first-right be the first operand of right.
Set left to an expression of the same type as right that begins a
SlashListExpression with elements left and first-right, followed
by operators and operands of right except first-right.
For example,
slash-list(1, 2 * 3)becomes(1 / 2) * 3.
Otherwise, if left is a slash-separated list, add right to the end.
Otherwise, set left to a slash-separated list containing left and
right.
Replace each element in left with the result of adjusting slash precedence
in that element.
Replace the SlashListExpression with left in the returned expression.
SlashListExpressionTo 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 evaluating element as a calculation value.
Set left to a CalcOperation with operator "/", left, and right.
Return left.
A new list separator, known as "slash", will be added. The string "slash" may
be passed to the $separator argument of append() and join(), and may be
returned by list.separator(). When converted to CSS, slash-separated lists
must have exactly one / between each adjacent pair of elements.
Although CSS doesn't currently make use of this syntax, there's nothing stopping a list from being both bracketed and slash-separated.
math.div() FunctionThe div() function in the sass:math module has the following signature:
math.div($number1, $number2)
It throws an error if either argument is not a number. If both are numbers, it
returns the same result that the / operator did prior to this proposal.
list.slash() FunctionThe slash() function in the sass:list module has the following signature:
list.slash($elements...)
It throws an error if zero or one arguments are passed. It returns an unbracketed slash-separated list containing the given elements.
rgb() FunctionThis proposal modifies the existing behavior of the rgb($channels)
overload to be the following:
If $channels is a special variable string, return a plain CSS function
string with the name "rgb" and the argument $channels.
If $channels is an unbracketed slash-separated list:
If $channels doesn't have exactly two elements, throw an error. Otherwise,
let rgb be the first element and alpha the second element.
If either rgb or alpha is a special variable string, return a plain CSS
function string with the name "rgb" and the argument $channels.
If rgb is not an unbracketed space-separated list, throw an error.
If rgb has more than three elements, throw an error.
If rgb has fewer than three elements:
If any element of rgb is a special variable string, return a
plain CSS function string with the name "rgb" and the argument
$channels.
Otherwise, throw an error.
Let red, green, and blue be the three elements of rgb.
Call rgb() with red, green, blue, and alpha as arguments and
return the result.
Otherwise, proceed with the existing definition of the function.
This ensures that calling (for example)
rgb(0 0 100% / 80%)will continue to work when/is parsed as a slash separator. It's important to define this runtime behavior in phase 1 so that users manually constructing slash-separated lists can use them as expected.
hsl() FunctionThis proposal modifies the existing behavior of the hsl($channels)
overload to be the following:
If $channels is a special variable string, return a plain CSS function
string with the name "hsl" and the argument $channels.
If $channels is an unbracketed slash-separated list:
If $channels doesn't have exactly two elements, throw an error. Otherwise,
let hsl be the first element and alpha the second element.
If either hsl or alpha is a special variable string, return a plain CSS
function string with the name "hsl" and the argument $channels.
If hsl is not an unbracketed space-separated list, throw an error.
If hsl has more than three elements, throw an error.
If hsl has fewer than three elements:
If any element of hsl is a special variable string, return a
plain CSS function string with the name "hsl" and the argument
$channels.
Otherwise, throw an error.
Let hue, saturation, and lightness be the three elements of hsl.
Call hsl() with hue, saturation, lightness, and alpha as arguments
and return the result.
Otherwise, proceed with the existing definition of the function.
This proposal modifies the "Parse a Selector From a SassScript Object" procedure to throw an error whenever it encounters a slash-separated list.
This proposal adds one additional scenario in which potentially slash-separated numbers are converted into slash-free numbers:
This change makes built-in functions/mixins consistent with user-defined ones, which do make their arguments slash-free. It also combines with Phase 1 of the deprecation process to ensure that all uses of
/-as-division will produce warnings.This could potentially be a breaking change. While most functions that could take potentially slash-separated numbers will either ignore the slash-separation or return the number and cause it to become slash-free that way, it's possible for a user to pass it to a function that puts it in a data structure, as in
list.join(1/2, ())which returns a single-element list containing a potentially slash-separated number. However, this breakage is considered exceedingly unlikely and it's easy to work around usinglist.slash()so we aren't considering it a blocker.
The deprecation process will be divided into three phases:
This phase will add no breaking changes, and will be implemented as soon as
possible. Its purpose is to notify users that /-as-division will eventually be
removed and give them alternatives to migrate to that will continue to work when
/'s behavior is changed.
Phase 1 implements none of the syntactic changes described above. It
implements all the semantics, with the exception that math.div()
allows non-number arguments. If either argument is not a number, it emits a
deprecation warning.
If either argument is not a number,
math.div()still returns the same result as the/operator, which in that case will be concatenating the two arguments into an unquoted string separated by/. This behavior is supported for the time being to make it easier to automatically migrate users tomath.div()without causing runtime errors.
While phase 1 will continue to support / as a division operator, the use of
the operator in this way will produce a deprecation warning. Specifically, a
deprecation warning is emitted when a potentially slash-separated
number is converted to a slash-free number, or when a /
operation returns a slash-free number.
In phase 1, we recommend authors write division and slash-separated lists like so:
scss@use 'sass:list; @use 'sass:math'; $ratio: math.div(12rem, 1px); $row: list.slash(span 3, 6); .grid .item1 { // It's always safe to use `/` as a separator directly in a CSS property. grid-row: span 2 / 7; }
This phase will introduce breaking changes to the language. It implements both
the syntactic changes and the semantic changes exactly
described above (so math.div() will only accept numbers). In phase 2, the
list.slash() function will emit a deprecation warning whenever it's called.
It's recommended that implementations increment their major version numbers with the release of phase 2, in accordance with semantic versioning.
In phase 2 and 3, we recommend authors write division and slash-separated lists like so:
scss@use 'sass:math'; $ratio: math.div(12rem, 1px); // As of phase 2, `/` is always parsed as a slash-separated list. $row: span 3 / 6; .grid .item1 { grid-row: span 2 / 7; }
This phase will introduce a final breaking change, removing the now-unnecessary
list.slash() function.
It's recommended that implementations increment their major version numbers again with the release of phase 3.
Phase 1 was originally scheduled to be implemented by Dart Sass as soon as the proposal was accepted. However, it was delayed considerably in the hope that it would also be implemented by LibSass. Since LibSass is now deprecated, we plan to release Phase 1 in Q2 2021.
Phase 2 will be released in Dart Sass 2.0.0. There's no solid release date for
this yet, and it may or may not be concurrent with the removal of support for
@import depending on how quickly the module system is adopted and how urgent
the need for a syntactic slash separator becomes.
Phase 3 will be released in Dart Sass 3.0.0, whenever that ends up happening.
Removing list.slash() is not considered a priority, so this will wait until
we have additional, more-compelling breaking changes we want to release.