Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-values] Clarifying serialization of negative zero and expression simplification #9750

Open
cjpearson opened this issue Dec 24, 2023 · 11 comments

Comments

@cjpearson
Copy link

Recently I was discussing with @emilio how signed zeroes should be serialized in calc expressions, and couldn't find a clear answer in the spec.

For example, an expression like max(1% / -infinity, 0%) which should simplify to max(-0%, 0%). Without knowing if the percentage basis is positive or negative, it can't be simplified further since zeroes of different signs are not considered equivalent in min/max functions.

It looks like currently Chrome and Firefox serialize this as max(0%, 0%) while Safari does calc(0%). I'm not sure which one of these is correct. max(-0%, 0%) is perhaps another possibility.

I found two parts of the spec that look to be relevant.

The serialization steps mention infinities and NaN but don't have any explicit instructions for negative zero. The numeric value should be clamped to the allowed range, but the allowed range may include negative values.

10.9.1. Infinities, NaN, and Signed Zero mentions that if a top level calculation simplifies to a negative zero, it should be "censored into a standard representable value".

Could that apply here? The example expression doesn't fully simplify, but the only two possible resulting values would be equivalent to 0%, so calc(0%) seems reasonable in this case.

But I'm not sure if that would work if there are additional arguments. Would it also be ok to simplify something like max(-0%, 0%, 1%) to max(0%, 1%) if it's the top-level expression? Does it make sense in general to eliminate duplicated values? Safari serializes max(1%, 1%, 1%) as calc(1%), while Chrome and Firefox keep max(1%, 1%, 1%).

@emilio
Copy link
Collaborator

emilio commented Dec 24, 2023

cc @Loirooriol @tabatkins

My understanding is that ideally it should be serialized as max(-0%, 0%), but I think that's the only place where we'd serialize a negative zero, and it's kind of annoying implementation wise.

I think safari incorrectly simplifies percentages (percentage basis can be negative in some properties) and that's a bug on their end, though arguably you could simplify the "equal" percentages. That is probably a separate issue tho.

@Loirooriol
Copy link
Contributor

Possibly related:

document.body.style.marginRight = "calc(-0% + -0em + -0ic + -0lh + -0px + -0vw)";
document.body.style.marginRight;
  • Gecko: calc(0% - 0em - 0ic - 0lh - 0px - 0vw)
  • Blink/WebKit: calc(0% + 0em + 0ic + 0lh + 0px + 0vw)

@Loirooriol Loirooriol added the css-values-4 Current Work label Dec 24, 2023
@emilio emilio added the Agenda+ label Jan 29, 2024
@astearns astearns added this to Unsorted regular in Feb 2024 Agenda Feb 8, 2024
@astearns astearns moved this from Unsorted regular to Monday afternoon in Feb 2024 Agenda Feb 11, 2024
@astearns astearns moved this from Monday morning to Unsorted regular in Feb 2024 Agenda Feb 11, 2024
@romainmenke
Copy link
Member

romainmenke commented Feb 28, 2024

I thought that negative zero is always tokenized and parsed as positive zero and that negative zero can only be produced by calculations.

So serializing to -0 wouldn't actually work because it wouldn't round trip.

I've actually been using calc(-1 * 0) for tooling:

  • min(1% / -infinity, 0%) is converted to calc(-1 * 0%)
  • max(1% / -infinity, 0%) is converted to 0%

The context is a bit different for me as the purpose is to calculate what is possible in dev tooling without altering how a browser would parse these.

@css-meeting-bot
Copy link
Member

css-meeting-bot commented Feb 28, 2024

The CSS Working Group just discussed [css-values] Clarifying serialization of negative zero and expression simplification, and agreed to the following:

  • RESOLVED: -0% serializes to a normalized form
The full IRC log of that discussion <Frances> RESOLVED: Allow the modulus parameter to be optional in the case of rounding numbers and default to 1.
<Frances> Tab: Track positive 0 and negative 0 separately, could result in different infinities in separate cases. We mostly separate this away. Could become an unsigned 0.
<Frances> Tab: One case where it isn't true if the 0 is a percentage so could be positive or negative in +0% and -0%. How important is this? We could just stick with the spec to imply need to verify if clear, and care about percentages and simplify.
<TabAtkins> max(0%, -0%) (or min(), or clamp())
<Frances> Alan: Can we see if the spec is clear, does the spec talk enough about positive and negative percentages?
<Frances> Tab: Would have to keep the max function with both arguments, carefully threading a few arguments.
<Frances> David: If the goal is serialization round tripping, we don't parse negative 0 into a negative 0.
<TabAtkins> min(1% / -infinity, 0%)
<Frances> Tab: If writing a solution similar to it, would still have to parse in the current semantics.
<Frances> Tab: Currently it should require both arguments to be preserved.
<emilio> theq+
<emilio> err
<emilio> q+
<astearns> ack emilio
<Frances> Emilio: Preserving the original syntax, we need to serialize it to something else. Infinity basically normalized the serialization, we don't preserve the actual author documentation.
<Frances> Tab: Not a problem to simplify more aggressively in the use case in it does not matter.
<Frances> Alan: Write tests for the spec based on the test production.
<Frances> Emilio: Could become a complicated code path, serializing -0%, and draw a calc tree.
<TabAtkins> happy to figure out how to normalize such an expression, at minimum
<Frances> Alan: Any other concerns?
<Frances> David: Like the idea of trying to keep it simple for a use case.
<dbaron> s/David:/dbaron: Not really an implementor as far as this code is concerned, but I/
<dbaron> s/a use case/something without much of a use case/
<Frances> Tab: We can resolve to at minimum serialize in case of a complex tree.
<Frances> PROPOSAL: -0% serializes to a normalized form
<Frances> RESOLVED: -0% serializes to a normalized form

@Loirooriol
Copy link
Contributor

I thought that negative zero is always tokenized and parsed as positive zero

Tab seemed to agree during the call, but it doesn't seem to be the case from https://drafts.csswg.org/css-syntax-3/#consume-numeric-token

For -0%, "consume a number" takes care of the -0, returning {type: "integer", sign: "-", value: "0"}. And then the result is a <percentage-token> with {sign: "-", value: "0"}.

Browsers don't currently accept percentages in denominators, but they agree with plain numbers:

document.body.style.zIndex = "calc(1 / -0)";
getComputedStyle(document.body).zIndex; // A very negative number, unlike for `calc(1 / 0)`

you don't need calc(1 / (1 / -infinity)) or such in order to get a negative zero.

@tabatkins
Copy link
Member

Right now, we only have the "normalized serialization" for inf/nan values when they emerge at the top-level. That is, if the entire expression simplifies to inf/nan, we serialize it in a specific way (rather than requiring more of the exact tree to be preserved).

This doesn't quite work the same here - if an entire expression simplifies to -0%, we'll just serialize it as 0% (or calc(0%), depending on the value stage). This issue is only about how to serialize a -0% that occurs within an expression, and which blocks simplification.

Tab seemed to agree during the call, but it doesn't seem to be the case from

Dang it, I keep forgetting that that does work. In that case, then, I think the issue is indeed no-change. If an expression simplifies to -0%, and this blocks further simplification, then you can simply serialize it as such.

So, yes, max(1% / -infinity, 0%) should serialize as max(-0%, 0%) (until the % is resolveable, at which point you do know which is larger and can simplify the max()).

I think I (a) will add a note to the spec explicitlly calling out that -0/etc does work in a calculation, so I stop forgetting, and (b) add a test for this case.

@romainmenke
Copy link
Member

The sign character is used in the an+b type : https://drafts.csswg.org/css-syntax/#the-anb-type

I was really sure that negative zero can not be produced by parsing, but now not so much anymore :)

@Loirooriol
Copy link
Contributor

BTW, https://rawgit.com/tabatkins/parse-css/master/example.html says PERCENTAGE(0) for -0% and INT(0) for -0, but that's because the JS serialization drops the sign for zero.

And not for percentages, but css/css-values/signed-zero.html does test that -0 works.

@romainmenke
Copy link
Member

romainmenke commented Feb 28, 2024

There was a recent issue were this was discussed and it was mentioned there that negative zero is written very specifically in css-values-4 as clamp(0⁺, 0⁻, 1) as not to confuse it with clamp(+0, -0, 1) exactly because of tokenizing. But I can't seem to find it now. Again, possible I misread that.


Edit: if there is test coverage than this is cleared up, thanks 🙇

@cdoublev
Copy link
Collaborator

These tests are relatively recent. I am pretty sure calc(1 / -0) was serializing to calc(infinity) in Chrome and FF before. Besides:

Signed zeros (indicated here as 0⁺ or 0⁻) can not be written directly in CSS; 0, +0 and -0 all produce the standard "unsigned" zero, which is considered positive (0⁺) for the purposes of these rules.

Based on #7472, CSS were inadvertently producing tokens representing a negative zero. Now I am not sure whether interpreting number part as a base-10 number still implies it or not. min(1% / -infinity, 1% / infinity) may serialize to min(-1 * 0%, 0%) or min(-0%, 0%) depending on this.

@Loirooriol
Copy link
Contributor

Loirooriol commented Feb 29, 2024

I have bisected Firefox. calc(1 / -0) was invalid until https://bugzilla.mozilla.org/show_bug.cgi?id=1682444, when it became negative infinity. Blink changed from positive infinity to negative infinity in https://chromium-review.googlesource.com/c/chromium/src/+/4227272

IMO if we have the concept of negative zero, -0 should produce it? Seems quite confusing otherwise.

Also #4110 was closed "in favor of the metabug for better JS alignment", and in JS, -0 is the negative zero.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Feb 2024 Agenda
Unsorted regular
Development

No branches or pull requests

9 participants