Csswg-drafts: [css-values-4] What should non-calc() math functions serialize to when they're fully resolved?

Created on 7 Oct 2019  Â·  14Comments  Â·  Source: w3c/csswg-drafts

As currently written, every math function is represented internally as a calculation tree, and the simplification process for calculation trees will crunch things down to plain numeric values as soon as possible. So, for example, a function like sqrt(4) becomes a tree that initially has two nodes - Sqrt → 4 - and then simplification will notice that it can fully resolve that and crunch it down to just the single node 2.

Serialization will then see that it's a single number, and then if it's a computed or used value, serialize it as "2" (or something else if it needs to clamp to the property's range), and otherwise serialize it to "calc(2)".

This seems definitely okay to do for non-root nodes, but it does feel slightly weird that the root node being simplified away results in this calc() function appearing out of nowhere. Is this okay?

The alternative is that I annotate the operator node with knowledge of whether it's the root of a tree or not, and never simplify away the root of a tree. That way calc(5 + sqrt(4)) (a Sum node containing a 5 child and a Sqrt child, the latter containing a 4 child) will collapse down to a plain 7 and serialize as "7" or "calc(7)", but a sqrt(4) will remain as a Sqrt node and serialize back as "sqrt(4)".

It feels like this is complexity without a good motivating reason, so I don't think I want to do it. Just checking in with other people's intuitions.

css-values-4

All 14 comments

Some questions from smfr I'm carrying over from #4505:

Some concrete examples where implementations seem to disagree. How do the following serialize?

  • min(1): min(1) or 1?
  • min(1, 2): min(1, 2) or 1?
  • min(min(1)): min(min(1)), min(1) or 1?
  • calc(min(1)): calc(min(1)), min(1) or 1 or other?

Questions:

  1. Should simplification ever remove all the math functions?
  2. Should nested math functions of different types be simplified, i.e. is it OK to simplify calc(calc(1)) but not calc(min(1))

My preferred answers, where we just simplify everything down as fast as possible:

  • min(1) => calc(1) if serialized at specified-value time; 1 at computed-value time
  • min(1, 2) => same
  • min(min(1)) => same
  • calc(min(1)) => same

My acceptable answers, if I annotate the root of the tree with knowledge that it's a root node, and what function it came from:

  • min(1) => min(1) at specified-value time; 1 at computed-value time
  • min(1,2) => min(1,2) at specified-value time; 1 at computed-value time
  • min(min(1)) => min(1) at specified-value time; 1 at computed-value time
  • calc(min(1)) => calc(1) at specified-value time; 1 at computed-value time

The latter make sense to me.

Another question: should dimensions always be converted to their canonical units? I.e. does min(1cm) serialize to min(1cm) or min(37.795px)? The simplification steps suggest that they do get converted.

This is tested by css-values/minmax-length-serialize.html, among others.

I’d rather we keep it as min(1cm) than convert it to px.

min(1cm) should yield the same as max(1cm), calc(1cm), min(1cm, 1cm), min(1cm, 10mm), calc(min(1cm)) and so on.

And do these simplify?
calc(min(1s) + min(2s))
calc(1s + min(2s))

Also does unit canonicalization happen when there are two values with non-canonical units:
calc(1cm + 2cm)

Yes, unit canonicalization is intended; we agreed to that as a policy for level-3 calc(), before unit algebra and fancier functions. That is, calc(1in + 5%) has been specified to have a computed value of calc(96px + 5%) for quite a while.

Hm tho, the old spec did seem to imply that for specified values you don't canonicalize, just sort and combine identicals. I lost that in the new text. It's easy to handle either way.

The CSS Working Group just discussed What should non-calc() math functions serialize to when they're fully resolved?, and agreed to the following:

  • RESOLVED: All math functions aggressively simplify their calculations as far as possible for a given value-computation stage.
  • RESOLVED: If numeric simplification of a math function results in a single value, the serialization is that value wrapped in `calc()`

The full IRC log of that discussion
<dael> Topic: What should non-calc() math functions serialize to when they're fully resolved?

<dael> github: https://github.com/w3c/csswg-drafts/issues/4399#issuecomment-554535978

<dael> TabAtkins: I brought up a question about when a non-calc() math function is fully resolved what should it be serialized as? We have old calc rules tht say if you do 1px+1em you combine at computed value and serialize as 17px. If you have min(10px, 20px) what should it serialize as? We can resolve it to 10px or should we preserve it was a min calculation?

<AmeliaBR> Do we ever need to keep at least a function wrapper for the same reasons as calc(-10px) needs to keep a wrapper?

<dael> TabAtkins: I'm neutral to where we go. Current text simplifies everything as far as possible which might land on a plain number. Simon preferred preserving root node's identity.

<dael> TabAtkins: I can do that, not a big deal in spec text. Just some annotation. I want to know what the other impl opinions are

<dael> smfr: Need to have a distinction between spec and computed. Most of my questions were about specified value. Text about simplification implies it's on specified value.

<dael> smfr: How much simplification happens on the specified value and then what happens on computed. I prefer computed simplified as mucht as possible to a bare value if possible. Specified value is how much do you want to round trip or is it okay to collapse redundant mins?

<AmeliaBR> Agree with smfr: if simple serialization converts calc(200px/4) to 50px, then it makes sense to do the same with simple mathematical min/max

<dael> TabAtkins: Is spec I do full simpliciation on specified. I realize from your comment I'm too agressive. Still have the question about what is the best to do with a min that has identical units? Does it retain its structure at spec value time?

<dael> TabAtkins: I can go either. I can do light that combines idential units like old calc did or I can do more agressive and not cannonicalize units.

<dael> emilio: Browsers do cannonicalize units, right?

<dael> TabAtkins: At specified value I don't think calc(1in) becomes px

<dael> emilio: calc(1px+1q) I get 10.something px.

<dael> smfr: Different units get cannonicalized

<dael> TabAtkins: So the tests smfr were wrong?

<fantasai> :/

<dael> smfr: Lots of incompat. WPT have a mixture of behaviors so I don't thinkw e should go on what those tests are doing

<dael> smfr: One of the consideratiosn for specified value is are there authoring tools that expect nested calc to be preserved. I know glazou was concerned back in 2017 because it would break authoring tools.

<dael> TabAtkins: At time we resolved getting rid of nested calc was fine. The loss to authoring tools was considered to be fine due to simplifications in impl and easier for end user.

<dael> TabAtkins: Don't want to revisit that if possible. Still a range of stuff we can do. I don't want to preserve more than the old calc. I prefer preserve as little as possible

<astearns> ack fantasai

<dael> fantasai: calc if nested is eq/ to parans. Switching min or max is a little different. Not quite the same situation

<dael> TabAtkins: I would argue same as dsitributing multiplication across properties. That's a re-write of algebraic structure which seems similar to simplifying away a min

<dael> fremy: I was hearing emilio and I found Chrome behavior is weird. If you set border calc(1inch) you get back calc(1inch) but calc(1in+1px) you get 97px. I think ther'es not web compat and we an do what feels right

<dael> TabAtkins: That's super weird, that feels like us going we're done and can exit early

<emilio> fantasai: document.body.style.left = "calc(1q)"; document.body.style.left

<emilio> Firefox behaves the same as Chrome here, but agreed it feels weird

<dael> TabAtkins: Proposal: Since calc in general does do agressive simplification we preserve that through the new math expressions. At specified value time we simplify down. Maybe preserve root node at specified time, but that's thrown away at computed value time

<dael> TabAtkins: Does that sound fine and, if so, which way on the root node?

<dael> smfr: Prefer preserve the root node. If you have calc with a nested min and you could reduce, I don't know if you should

<emilio> fremy: fwiw what happens on Firefox is that as soon as we need to canonicalize &lt;length>s we simplify both to px

<dael> smfr: Keeping root node as a function is right. And simplify as much as possible for computed

<dael> TabAtkins: Will argue to get rid of nested things. min is a binary operator square root is. Preserving the later things as functions seems inconsistant to me

<AmeliaBR> q+

<dael> smfr: That implies if you have a calc with min inside you'd replace the calc with a min?

<dael> TabAtkins: No, the root node retains at specified value.

<dael> smfr: Doesn't change function type?

<dael> TabAtkins: Yeah. Preserved. Everything under simplifies as much as possible. calc(min(px,%)) stays. calc(min(px,px)) simplifies

<dael> emilio: I'm fine with dropping root, but I don't mind

<emilio> fantasai: in the case of FF it's just an accident fwiw

<dael> AmeliaBR: I agree with arguments, but I think we lost them a long time ago. I don't like that we do a lot of math simplification at serializtion with calc, but we do. calc(10px/3) reads back as calc(3.333px).

<dael> AmeliaBR: Similar logic in the other functions seems to be a consistency thing. Important to keep the wrapper that says this is a math function b/c rules about clamping.

<emilio> fremy: fantasai: https://searchfox.org/mozilla-central/rev/652014ca1183c56bc5f04daf01af180d4e50a91c/servo/components/style/values/specified/length.rs#391, fwiw

<dael> AmeliaBR: That would presumably happen with other math functions too. If you say in spec value min(10px,20px) it should still read back with functional wrapper of min(10px) as simplified.

<emilio> fantasai: so it sorta makes sense, we preserve the unit as much as possible, but drop it if needed

<astearns> ack AmeliaBR

<dael> AmeliaBR: You want to keep b/c what happens if it's min and a value is negative and negative is invalid in that context. We need the wrapper

<dael> TabAtkins: If you forget the root node it serliazes as a calc.

<dael> AmeliaBR: Do we turn all simplified math functions into a calc wrapper is the question?

<dael> TabAtkins: Yep

<dael> AmeliaBR: Then yeah, simplifying...if the result is a single value it makes sense as a single value with a calc wrapper rather than preserve min/max when we can't do that with other functions

<dael> astearns: We're proposing to simplify functions like we do calc and change calc specified value to retain the wrapper

<dael> TabAtkins: CHanging the new stuff.

<dael> TabAtkins: The root node of a calculation tree retains it's identify

<dael> astearns: If root is calc function?

<dael> TabAtkins: Then it's a calc

<dael> TabAtkins: That's preserved in specified. At computed it goes away.

<dael> astearns: functions simplify the way calc does. Outer most function context is maintained for new functions.

<dael> AmeliaBR: I have a problem with the second. Should we get the first resolved?

<dael> TabAtkins: I'll write proposed resolution

<TabAtkins> Proposed resolution 1: All math functions aggressively simplify their calculations as far as possible for a given value-computation stage.

<dael> astearns: Objections or continued discussion on proposed resolution 1?

<dael> RESOLVED: All math functions aggressively simplify their calculations as far as possible for a given value-computation stage.

<dael> astearns: TabAtkins will you type the 2nd?

<TabAtkins> Proposed resolution 2: At specified-value time, the root of a calculation tree retains knowledge of what type of function it is and serializes accordingly, even if it could be simplified to a single number. (At computed-value time, they'll turn into a plain number if possible.)

<dael> TabAtkins: Yeah

<dael> AmeliaBR: Problem with root of calc tree retaiining knowledge is some function types are math operators like power. If you simplify square root of 4 it means erase function wrapper

<dael> TabAtkins: The be precise here I would not simplify root node, start simplification at children of root node

<dael> AmeliaBR: If I have squareroot 4 inside a calc it's simplified, but sqt4 is not simplified?

<dael> TabAtkins: Correct. It's either all or none. I don't want to keep min but not sqrt

<TabAtkins> calc(sqrt(4)) => calc(2); sqrt(4) => sqrt(4)

<dael> AmeliaBR: COnsidering sqrt and power anything simplified to a single value simplifies to that wrapped in a calc.

<dael> TabAtkins: THat's int he spec today. smfr expressed desire to keep root node around. Easy to do spec wise.

<dael> smfr: I think AmeliaBR is suggesting sqrt(2) that becomes calc(2.14...). You replace sqrt with calc.

<dael> TabAtkins: That's spec today

<AmeliaBR> yes, serialization of sqrt(4) would be calc(2)

<dael> florian: DOes spec say [missed]

<dael> TabAtkins: No

<dael> florian: I think that's what AmeliaBR is suggesting

<dael> TabAtkins: AmeliaBR are you suggesting extra calcs?

<dael> AmeliaBR: If we need a wrapper use cal

<fantasai> s/2.14/1.41/

<dael> florian: calc(sqrt(4)) what is that?

<AmeliaBR> Also min(10,20) serializes as calc(10)

<dael> florian: calc(2) or calc(calc(2))

<dael> AmeliaBR: YOu just need the outer wrapper

<dael> florian: That is current spec

<dael> AmeliaBR: I think we're at the point where everyone understands, but not consensus on strategy

<dael> TabAtkins: I think we've converged. I was arguing smfr wants exact ID but that's nto case. WE fully simplify. At specified value time we need to wrap it in a calc if it's single number. So no change to current rules for calculation trees

<dael> smfr: Root function might be a calc. Might still be a min/max

<dael> TabAtkins: Yes, if you can resolve min it's a min.

<dael> smfr: I like that calc is way to signal out of range things

<dael> TabAtkins: We can go with no change to current rules for calculation trees if no objection?

<dael> AmeliaBR: I'm writing down a version

<AmeliaBR> Proposed resolution: if numeric simplification of a math function results in a single value, the serialization is that value wrapped in calc()

<dael> smfr: Current rules is ambiguous.

<dael> TabAtkins: New version os spec shouldn't be ambigous. I'll put in a note that it stays a calculation tree is clear. IT's in spec, but easy to go past.

<dael> smfr: I meant there are 3 specs and not sure which people are talking about.

<dael> smfr: ED right?

<dael> TabAtkins: Yeah

<dael> astearns: TabAtkins is AmeliaBR proposed resolution what you're thinking about?

<dael> TabAtkins: Fine

<dael> TabAtkins: End result is no change, but the explicit wording works

<dael> smfr: Spec could be if you encounted bare math you open calc

<dael> TabAtkins: I'll work with you in the issue to make it super clear

<dael> smfr: Still want clarity on unit canonicaliztion happens

<dael> TabAtkins: We'll continue that in issue

<dael> astearns: Objections to if numeric simplification of a math function results in a single value, the serialization is that value wrapped in calc()

<dael> RESOLVED: If numeric simplification of a math function results in a single value, the serialization is that value wrapped in calc()

<dael> astearns: Please do continue about unit canonization in the issue

So it looks like all browsers do canonicalize units at specified-value time; Chrome just has a quirk that it only canonicalizes if it has to combine disparate units: calc(1in + 1in) serializes to calc(2in), but calc(1in + 1cm) serializes to calc(133.795px). Firefox, I think, doesn't have that quirk, and eagerly canonicalizes everything. I don't know what Safari's behavior is.

So this is roughly in accord with the resolution, and the current spec, which aggressively canonicalizes and serializes as far as possible.

I'm going to add a note to the spec emphasizing that a calculation tree that's been reduced to a single numeric value is still a calculation tree, and serializes as such (thus hitting the serialization algo that wraps it in a calc() at specified-value time, but clamps and serializes it directly at computed/later time).

Two of the comments above oppose always canonicalizing units, however.

I only see one, from @ExE-Boss. @Crissov is asking for heavy simplification/canonicalization, as I read them.

Also, the group has had consensus on canonicalizing units (from resolutions on lvl 3-style calc()) for a long time, and I don't want to revisit that decision without good reason.

I'm also loathe to spec something like Chrome's behavior, where it only canonicalizes when combining disparate units. It feels pretty hacky to me for that to be the one and only simplification we don't do eagerly; if people expect calc(1in) to stay 1in, they'd presumably expect calc(1in + 1in) to stay as 1in + 1in too, but Chrome simplifies it down to 2in.

Was this page helpful?
0 / 5 - 0 ratings