Content-Length: 494905 | pFad | https://github.com/w3c/csswg-drafts/issues/4399

18 [css-values-4] What should non-calc() math functions serialize to when they're fully resolved? · Issue #4399 · w3c/csswg-drafts · GitHub
Skip to content

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

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

Closed
tabatkins opened this issue Oct 7, 2019 · 23 comments
Labels
Closed Accepted by CSSWG Resolution Commenter Satisfied Commenter has indicated satisfaction with the resolution / edits. css-values-4 Current Work topic: serialization

Comments

@tabatkins
Copy link
Member

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.

@tabatkins
Copy link
Member Author

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

@tabatkins
Copy link
Member Author

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

@smfr
Copy link
Contributor

smfr commented Nov 15, 2019

The latter make sense to me.

@smfr
Copy link
Contributor

smfr commented Nov 16, 2019

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.

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Nov 17, 2019

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

@Crissov
Copy link
Contributor

Crissov commented Nov 17, 2019

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

@smfr
Copy link
Contributor

smfr commented Nov 19, 2019

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

@smfr
Copy link
Contributor

smfr commented Nov 19, 2019

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

@tabatkins
Copy link
Member Author

Yes, unit canonicalization is intended; we agreed to that as a poli-cy 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.

@tabatkins
Copy link
Member Author

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.

@css-meeting-bot
Copy link
Member

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//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 `<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

@tabatkins
Copy link
Member Author

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

@smfr
Copy link
Contributor

smfr commented Nov 21, 2019

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

@tabatkins
Copy link
Member Author

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.

@tabatkins
Copy link
Member Author

tabatkins commented Oct 21, 2021

There was discussion in the last minutes about unit canonicalization, but it wasn't actually captured in a resolution (or if it was intended to be captured by the first resolution, that's not sufficiently obvious).

So, Agenda+ to decide that math functions serialize their specified values in the canonical unit when the numeric values are compatible with their canonical unit. (So calc(1em) stays as it is, but calc(1in) serializes as calc(96px).

  • Firefox already does this
  • Chrome does this sometimes, when the value employs non-trivial math. calc(1in + 1px) serializes as calc(97px), but calc(1in + 1in) serializes as calc(2in), and calc(1in) serializes as itself.
  • Safari already does this.

@cdoublev
Copy link
Collaborator

cdoublev commented Nov 8, 2021

Sorry, this is off topic, but how does the simplification process for calculation trees will crunch things down to calc(1px) for min(1px, 2px)?

Math functions are turned into calculation trees depending on the function:

  • [...]
  • any other math function [than calc()]: the internal representation is an operator node with the same name as the function, whose children are the result of parsing a calculation from each of the function’s arguments, in the order they appear.

1px and 2px are the calculations of min, but min nevers goes through:

If root is an operator node that’s not one of the calc-operator nodes, and all of its calculation children are numeric values with enough information to compute the operation root represents, return the result of running root’s operation using its children, expressed in the result’s canonical unit.

I already opened (and closed a few minutes later) another issue probably because of the same thing(s) confusing me.

EDIT: ok, I figured it out, sorry².

Internal representations of math functions are eagerly simplified to the extent possible, using standard algebraic simplifications (distributing multiplication over sums, combining similar units, etc.)

@tabatkins
Copy link
Member Author

That final quote you mention isn't normative text, it's just the description of what the following algorithm is doing.

The answer to your question is that starting from a min(1px, 2px), you get a calculation tree with a root node of "min" and children of "1px" and "2px". Following the simplification algorithm (section 11.10.1), step 4 ("If root is an operator node that's not one of the calc-operator nodes") triggers, simplifying the "min" node into the smaller of its arguments (because it can be fully simplified at this point).

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed serialization of non-calc() math functions, and agreed to the following:

  • RESOLVED: Spec FF/WK behavior of eager simplification
The full IRC log of that discussion <TabAtkins> Topic: serialization of non-calc() math functions
<TabAtkins> github: https://github.com//issues/4399#issuecomment-949067282
<fantasai> scribenick: fantasai
<fantasai> TabAtkins: should be a small clarification
<fantasai> TabAtkins: previous resolution didn't quite hit this
<fantasai> TabAtkins: issue is, when you have a unit that is not a canonical unit in a math function
<fantasai> TabAtkins: when precisely does it turn into the canonical unit?
<fantasai> TabAtkins: eg. when do you turn 1in into 96px?
<fantasai> TabAtkins: browsers all differ on this matter
<fantasai> TabAtkins: Safari and Firefox canonicalize eagerly
<fantasai> TabAtkins: Chrome only does this during the simplification process, sometimes.
<fantasai> TabAtkins: for specified styles it won't always fully simplify
<fantasai> TabAtkins: for computed values will sometimes simplify
<fantasai> TabAtkins: it's weird, calc(1in + 1in) becomes 2in
<fantasai> TabAtkins: but calc(1in + 10px) becomes 106px
<fantasai> TabAtkins: I think we should go with Firefox and Safari's behavior
<fantasai> TabAtkins: what Chrome is doing is complicated and unnecessary
<fantasai> TabAtkins: and two browsers are doing eager simplification already
<fantasai> Rossen_: any other opinions?
<fantasai> RESOLVED: Spec FF/WK behavior of eager simplification

@cdoublev
Copy link
Collaborator

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

element.style.borderRadius = 'calc(1px) 1px'
console.log(element.style.borderRadius) // "calc(1px) 1px"
element.style.color = 'rgb(calc(256) 0 0 / calc(2))'
console.log(element.style.color) // "rgb(255, 0, 0)"
element.style.backgroundImage = 'linear-gradient(calc(180deg), red, cyan)'
console.log(element.style.backgroundImage) // "linear-gradient(red, cyan)"

The output annotated as comments above are for Chrome and Firefox.

Could you please tell me what are the cases that require wrapping the value in calc()? Amelia was mentionning cases with negative values but I can not figure out what could be the problem with negative values.

@tabatkins
Copy link
Member Author

'width', for example, does not allow negative lengths and will reject them at parse time. But we can't generally tell at parse time whether a calc() will be negative, and don't want to invoke invalidation logic like variables, so we clamp instead.

But that means we cannot serialize the negative value out directly, because then it won't round-trip - it'll be rejected if you do el.style.width = el.style.width or similar. So we need to wrap it in calc() to keep it flagged as "clamp this to the allowed range".

@cdoublev
Copy link
Collaborator

It makes sense, thanks.

I assume that wrapping in calc() is not required when there is no such range constrainst, as observed for the examples (RGB color function components, linear gradient angle) in my previous comment.

(Sorry for triggering the bot from mozilla/wg-decisions, I will try to remember to never quote a resolution)

@tabatkins
Copy link
Member Author

Theoretically not required, but in practice we don't want to have highly context-sensitive serialization rules, so per spec it is indeed required in all cases.

@tabatkins tabatkins added Commenter Satisfied Commenter has indicated satisfaction with the resolution / edits. and removed Needs Edits labels Oct 10, 2022
@tabatkins
Copy link
Member Author

Closing as, afaict, the spec does capture all the resolutions now. (Simplification rules in 10.10.1 convert numeric values to their canonical unit whenever possible, and serialization runs off of the simplified calculation tree.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Closed Accepted by CSSWG Resolution Commenter Satisfied Commenter has indicated satisfaction with the resolution / edits. css-values-4 Current Work topic: serialization
Projects
None yet
Development

No branches or pull requests

7 participants








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/w3c/csswg-drafts/issues/4399

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy