Skip to content

[css-values] Make top-level NaN be invalid at computed value time instead of ∞ #7067

Closed

Description

@Loirooriol

From https://drafts.csswg.org/css-values/#top-level-calculation

If a top-level calculation [...] would produce a value whose numeric part is NaN, it instead act as though the numeric part is +∞.

This seems very random to me, why ∞ instead of e.g. -∞, 0, 1, or 3.14159?

This topic was already touched in #4954. @LeaVerou said

I don't understand the sentence "it produces a NaN, which is censored into an infinity". Mathematically, 0/0 does not tend to infinity and is an indeterminate form.
It seems more reasonable that it would be invalid at computed value time, or something along these lines.

And during the CSSWG discussion:

  • oriol: One argument about this, maybe we could say that saying the initial value is not possible, but we could resolve it to NaN and keep NaN into the outer calc, then bubble it up, and say if the number resolves to NaN it behaves as the initial value.
  • TabAtkins: If a top-level calculation ends up being NaN, the property would be invalid at computed value time. It's possible.

But AFAIK the discussion didn't continue.

I do think that invalid at computed value time seems more reasonable than ∞.

Activity

  1. LeaVerou commented on Feb 17, 2022

    @LeaVerou
    Member

    I agree. That also troubled us when we recently looked at this in the TAG.

    Also, errors don't always happen during development, where they can be caught by the author and fixed. Often an error is a result of unexpected user input, and can happen at any time, so producing a value that is more likely to mask the error (thus producing a more predictable user experience) should be a goal, not something to avoid. We don't want to break people's websites on purpose to alert them that they made a mistake, this is not XML! And I highly doubt that using Infinity will make the author think "oh, I think I have a NaN!". It will likely just baffle them to no end, and make them conclude that they can't understand CSS.

  2. svgeesus commented on Feb 17, 2022

    @svgeesus
    Collaborator

    This is why we introduced the none keyword and the powerless component concept in CSS Color 4 to represent things like indeterminate hue angle, because NaN had the wrong behavior.

    NaN is still used in most implementations, for doing calculations, but then the none keyword is used when parsing CSS syntax or serializing; and the prose says to convert it to 0 (not +∞) during colorspace conversion:

    For all other purposes, a missing component behaves as a zero value, in the appropriate unit for that component: 0, 0%, or 0deg. This includes rendering the color directly, converting it to another color space, performing computations on the color component values, etc.

  3. tabatkins commented on Feb 17, 2022

    @tabatkins
    Member

    (For further history, the call that originally discussed this issue is minuted at https://lists.w3.org/Archives/Public/www-style/2014Apr/0373.html.)

    This seems very random to me, why ∞ instead of e.g. -∞, 0, 1, or 3.14159?

    Because the most likely cause of a NaN is division by 0, and the limit as you approach division by zero is either +Infinity or -Infinity, and +Infinity is more likely to be correct (CSS is usually doing math on positive values). (Edited: forgot that NaN only comes out of things like 0/0; plain ol' 1/0 is Infinity and totally normal.)

    NaN can be produced via other mistakes, which can have a variety of limits, but since a NaN indicates a math error, preserving the authors precise intent wasn't considered too important; in these cases where the limit of the author's code is not Infinity, returning Infinity is the most likely value to produce a noticeably incorrect result.

    This is why we introduced the none keyword and the powerless component concept in CSS Color 4 to represent things like indeterminate hue angle, because NaN had the wrong behavior.

    No, this is a false summary of that discussion. We introduced none because NaN was completely inappropriate for the use-case. Conflating a missing component and a component that has a division by zero or other invalid math operation is completely wrong. NaN is used for missing channels in some JS color libraries because because its infectious behavior is slightly better/more recognizable if you forget to handle the case and naively do math on the channel (aka the "make it fuck up as badly as possible so authors notice the error" behavior, which Lea appears to be arguing is bad); using JS's null is more semantically appropriate (and more closely matches common JS practice in general), but less good in practice, since it coerces to 0 in many math operations and thus could hide the error. (source) CSS works differently in these regards, so the JS-based reasoning was irrelevant, and we could stick with common CSS design principles and use a keyword for the non-numeric value.

    We don't want to break people's websites on purpose to alert them that they made a mistake, this is not XML!

    We do this all the time, in many CSS features. For a relatively recent example that comes to mind, setting the grid positioning properties to a line name that doesn't exist will collapse them all to the first implicit line, which is almost certainly a bad position that will look wrong to the author. And as I mentioned in the preceding paragraph, breaking people's websites on purpose was the precise reason provided for JS color APIs to use NaN for missing channels.

    (Heck, CSS's forward-compatible parsing, which drops the entire property when there's a mistake, is also an example of this.)

    (Ah, I see I gave this same example last year.)

    And I highly doubt that using Infinity will make the author think "oh, I think I have a NaN!". It will likely just baffle them to no end, and make them conclude that they can't understand CSS.

    Do you think that using IACVT makes it more noticeable, understandable, or teachable? If so, can you elaborate on this reasoning?


    FWIW I'm not necessarily opposed to changing the behavior here, but I am opposed to some of the arguments being deployed in service of this proposed change; they're wrong and don't reflect our general design philosophy elsewhere in CSS. There is a reason for the current behavior, and it's consistent with CSS's established design philosophy.

    I don't recall if this was a specific reason for avoiding IACVT, but I'll note that you might not know the value is invalid at computed-value time; if a percentage is involved in producing a 0 division, it might not be apparent until used-value time when the % is finally resolved. So this would actually need to be a brand-new concept, and I'm not sure of the implications of that right now.

  4. Loirooriol commented on Feb 17, 2022

    @Loirooriol
    ContributorAuthor

    since a NaN indicates a math error, preserving the authors precise intent wasn't considered too important

    Initial or inherited value isn't probably what they want either (otherwise they wouldn't be setting the property).

    returning Infinity is the most likely value to produce a noticeably incorrect result.

    Yes, but it may be going overboard. If we only tried to maximize incorrect results, we could also say that setting width to a calc() producing -1px also becomes the maximum value instead of being clamped to 0px.

    you might not know the value is invalid at computed-value time

    OK, good point about percentages. So rather than IACVT, I guess it would be that it "behaves as" the initial value or the inherited value (depending on whether the property is inherited).

  5. tabatkins commented on Feb 18, 2022

    @tabatkins
    Member

    Yes, but it may be going overboard. If we only tried to maximize incorrect results, we could also say that setting width to a calc() producing -1px also becomes the maximum value instead of being clamped to 0px.

    Good thing we're not only trying to maximize incorrect results, then. ^_^

    Due to our "no open ranges" policy, we can assume that any explicitly authored values outside of a valid range are an authoring mistake, and go for the maximally incorrect result of "drop the property as invalid". As long as all ranges are closed, authors can't accidentally write a value close to the boundary that might be across the boundary in some implementations.

    This isn't the case for calculations, which due to precision issues are possible to kick slightly outside of a valid range by accident. Catastrophizing these cases would be author-hostile when we can tell what the likely intent was, so we just clamp instead. And there's no particularly good reason to set some "maximum error" beyond which we give up and do something maximally incorrect, so we just clamp all values.

    NaNs don't fall into this bucket - we have no means to predict what they're supposed to be (which is why they're NaN rather than an infinity or something). So nothing we do has even a slight assurance of matching author intent. So the only thing we can do is be maximally incorrect in some arbitrary way. Making the property invalid is out. I chose censoring to Infinity; reducing the whole value to initial or inherit is also possible.

    CSS has existing precedent (already given) in cases like this to make the value deliberately, obviously bad. I don't think initial or inherit serves that purpose quite as well as an infinity. Custom properties need to use that behavior because they can be anything and be used anywhere; we have absolutely no idea what they are meant to substitute as, so we can't do anything smarter and obviously wrong. But for numbers we do have a more obviously wrong behavior available.

    Like I said, I'm not against changing this in theory, but I do think the current behavior is fine and the suggested change would be slightly worse. I'm open to an argument that a different behavior would be better for authors, particularly in the recognizability/teachability aspect.

  6. LeaVerou commented on Feb 18, 2022

    @LeaVerou
    Member

    Do you think that using IACVT makes it more noticeable, understandable, or teachable? If so, can you elaborate on this reasoning?

    IACVT is a CSS-wide concept that can, in the future, be recognized by dev tools and displayed similarly to parse-time invalid values. Handling NaN as infinity means it will never appear as an error in dev tools.

    (I will reply to the rest when I have a little more time)

  7. tabatkins commented on Feb 18, 2022

    @tabatkins
    Member

    IACVT is a CSS-wide concept that can, in the future, be recognized by dev tools and displayed similarly to parse-time invalid values. Handling NaN as infinity means it will never appear as an error in dev tools.

    a) Remember, it won't be IACVT, but rather something similar that resolves at used-value time (after the invalid value was also passed down via inheritance, unlike IACVT which cleans up before inheritance)

    b) I don't understand why you think DevTools can handle one but not the other. Both will turn into perfectly valid values at used-value time, with no author-observable remnants telling you that they originated from an invalid value. If DevTools can detect one had a NaN as its origin, it can detect the other.

  8. Loirooriol commented on Mar 10, 2022

    @Loirooriol
    ContributorAuthor

    I have realized that making the property behave as unset is not forwards-compatible regarding shorthand expansion, unless we are willing to add some var()-like approach for calc().

    Consider background-position: calc(0px / 100% * 1px) 5px, and assume that during layout the percentage resolves to 0px, namely background-position: calc(NaN * 1px) 5px. Then we treat the entire property as unset, so it behaves as 0% 0%, i.e. 0px 0px.

    But at some point we may turn background-position into a shorthand of background-position-{x,y}. Then, at parse time the example would become background-position-x: calc(0px / 100% * 1px); background-position-y: 5px. And the behavior would end up like background-position-x: 0px; background-position-y: 5px

  9. tabatkins commented on Mar 22, 2022

    @tabatkins
    Member

    The TAG just rereviewed, with this comment:

    @LeaVerou, @atanassov and myself reviewed this issue during our Hybrid TAG f2f. We still have concerns about the current proposal that are best for the CSS WG to review, thus, we suggest another round of discussions there. In particular, we are concerned that imposing runtime errors through +infinity will break layouts in unpredictable ways. For the purposes of layout, calc expressions can be evaluated during used-value time which in turn can depend on user context. This can result in unpredictable and difficult to evaluate/debug problems.
    Further, given JS has handling of NaN different than +infinity, this proposal will introduce inconsitency in the overall web platform. We should try and avoid that.

    I still believe there is no alternative that is better than the current behavior.

    Addressing these bits of feedback directly:

    • "we are concerned that imposing runtime errors through +infinity will break layouts in unpredictable ways."

      Any and all solutions share this property. There is literally no way to divine authorial intent in this situation; that's why it resolves to NaN in the first place rather than a number or an infinity. In particular, the other solution presented here (making the property "invalid at used-value time") will be just as divergent from the author's intent as anything else. Thus, I don't believe this is a reasonable critique. There's no way to "predictably" break the layout in cases like this.

    • "For the purposes of layout, calc expressions can be evaluated during used-value time which in turn can depend on user context. This can result in unpredictable and difficult to evaluate/debug problems."

      Again, I'm not sure how this is specific to the current spec text, rather than being a generic problem that will apply to any and all possible solutions. The timing of the NaN's appearance is the same regardless of what arbitrary action we take in response to it. Seeing a length go to infinity, as opposed to resetting the property to its initial value, seems equally easy (or difficult) to notice and debug. If anything, an infinity showing up might be easier to debug, as it more readily points to a math issue, as opposed to the initial value which can look like the property simply isn't being applied for some reason, which is a situation with many possible causes.

      (None of the options have any differences wrt dev tools, so this is purely a judgement call over how the behavior presents itself in the page.)

    • "Further, given JS has handling of NaN different than +infinity, this proposal will introduce inconsitency in the overall web platform. We should try and avoid that."

      Yet again, no solution we have available to us is different in this regard. JS "deals with NaN" by making it infectious across math operations, and eventually causing an API to throw, stringifying it (to confusing results, possibly also resulting in an API throwing), or doing some other arbitrary action as a response to the unexpected input (such as following a codepath where a comparison returns false, as all the comparison operators return false when at least one arg is NaN). There is no consistency in any regard here; throwing a NaN into your JS code is very likely to cause something to break somewhere, but exactly when and where that happens is completely unpredictable.

      CSS NaN is also infectious to the same degree - across math operations. CSS can't perform the equivalent of "throwing" upon receiving a NaN, as it has to keep laying out the page, but it can do something at the value level to censor it, which all proposed solutions do (just with different censoring results).


    On review, I find the feedback to be unactionable. There is no option that does not run equally afoul of every complaint listed, and to my judgement, every discussed option is equally bad or worse. CSS already has several precedents for handling unrecoverable logic errors with no discernable author intent by making them broken in a way that's hopefully obvious to the author, and I believe the current spec comports itself handily among those precedents. Introducing a new type of IACVT (but at used-value time) would not make the situation materially better in any way, and I believe would make it slightly worse in some ways (importantly, in ease of deducing where the problem lies, imo).

    Agenda+ to suggest resolving to close this issue as no change.

  10. dbaron commented on Mar 29, 2022

    @dbaron
    Member

    As someone who's spent a bit of time over the past year debugging assertion failures (DCHECK failures) that involved NaN values being introduced into code that didn't expect them (very little code does, really), I'd like add one note about NaN values:

    The typical sources of NaN values are more diverse than portrayed above. Sources that I've seen include (with variations in sign):

    • 0.0 / 0.0
    • 0.0 * Infinity
    • sin(Infinity), cos(Infinity), tan(Infinity), etc.
    • Infinity - Infinity (which can come from Infinity - floor(Infinity))

    This, of course, assumes IEEE 754 (a.k.a. IEC 559) floating point behavior. Note that this is not guaranteed by C++, though I think many browser implementations written in C++ assume it. In C++ it can be tested with std::numeric_limits<float>::is_iec559, std::numeric_limits<double>::is_iec559, etc. Most of these things are formally undefined behavior in C++, as I understand things.


    As far as the proposed solutions go, I'd note that I weakly dislike introducing Infinity values where not necessary, since Infinity values (see a few paragraphs up) have a strong tendency to produce NaN values further down the line, which tend to lead to more bugs than the Infinity values alone do. Once you have NaN values, they tend to break a lot of developer assumptions (e.g., again assuming IEEE 754 / IEC 559), such as breaking the assumption that one of a < b, a == b, and a > b is true. On the other hand, implementation should be avoiding bugs in these cases since there are other ways to produce Infinity values -- it's just sending more cases down codepaths that are likely to be buggy.

    (For example, with transform matrices, the easiest way to get NaNs is to multiply a bunch of very large numbers until you exceed the range of the relevant floating point type and get an Infinity, and then multiply that by a 0. Most components of the transform matrix are usually zero, so once you have infinities in transform matrices you very quickly get NaNs, and then you're often just a few multiplies away from a matrix that's just all NaNs.)

  11. tabatkins commented on Mar 29, 2022

    @tabatkins
    Member

    Once you have NaN values, they tend to break a lot of developer assumptions (e.g., again assuming IEEE 754 / IEC 559), such as breaking the assumption that one of a < b, a == b, and a > b is true.

    Note that we don't (yet) have direct comparisons, and the functions that do comparison-like things (clamp/min/max) all evaluate to NaN if they have a NaN argument (see the final bulleted item in the list of division by zero consequences in Values 4, section 10.9 Type Checking, so at least we currently avoid the most obvious NaN confusions.

  12. miragecraft commented on Apr 4, 2022

    @miragecraft

    I just noticed that Chrome v100 implemented this behavior for calc(1px/0) (by error?) and found out that this is the intended behavior for the draft spec.

    I strongly disagree with this decision, because I actually built a CSS grid system which depend on divide by zero causing calc() to be invalid, thereby setting the property to its initial value.

    See here: http://miragecraft.com/projects/razorgrid.html

    Setting --span to -1 causes my flip switch which is calc((var(--span)+1)/(var(--span)+1)) to become invalid, while all other values causes it to stay at 1 (which I use to multiply the value I want, leaving it unchanged), this lets me change flex-basis to auto only when --span: -1 without affecting calc() result for all other --span values.

    So as you can see, divide by zero causing calc() to be invalid is actually a useful behavior, while making the value go to infinity is not.

    So the argument that going to infinity is picking the most blatant of two wrongs is incorrect, because it's possible - as I just demonstrated - that a developer can intentionally divide a value by zero to force the property to go to the initial value.

  13. LeaVerou commented on Apr 4, 2022

    @LeaVerou
    Member

    @miragecraft This is not the behavior we are discussing in this issue, because dividing by 0 does not produce NaN, it produces infinity (or -infinity, if dividing a negative number by 0). The only case where dividing by 0 produces NaN is when dividing 0/0.

  14. 27 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions