style/typed_om/numeric_type.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Typed OM Numeric Type.
6
7use crate::derives::*;
8use crate::values::generics::calc::CalcType;
9use crate::values::generics::grid::FlexUnit;
10use crate::values::generics::Optional;
11use crate::values::specified::angle::AngleUnit;
12use crate::values::specified::frequency::FrequencyUnit;
13use crate::values::specified::length::LengthUnit;
14use crate::values::specified::resolution::ResolutionUnit;
15use crate::values::specified::time::TimeUnit;
16
17/// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-base-type
18#[derive(
19 Clone,
20 Copy,
21 Debug,
22 Deserialize,
23 MallocSizeOf,
24 PartialEq,
25 Serialize,
26 ToAnimatedZero,
27 ToResolvedValue,
28 ToShmem,
29)]
30#[repr(u8)]
31pub enum NumericBaseType {
32 /// A `<length>` unit.
33 Length,
34
35 /// An `<angle>` unit.
36 Angle,
37
38 /// A `<time>` unit.
39 Time,
40
41 /// A `<frequency>` unit.
42 Frequency,
43
44 /// A `<resolution>` unit.
45 Resolution,
46
47 /// The `<flex>` unit.
48 Flex,
49
50 /// The percentage unit.
51 Percent,
52}
53
54#[doc(hidden)] // Need to be public so that cbindgen generates it.
55pub const NUMERIC_BASE_TYPE_COUNT: usize = 7;
56
57const_assert!(NumericBaseType::Percent as usize + 1 == NUMERIC_BASE_TYPE_COUNT);
58
59/// Every numeric base type in enum-declaration order.
60pub const ALL_NUMERIC_BASE_TYPES: [NumericBaseType; NUMERIC_BASE_TYPE_COUNT] = [
61 NumericBaseType::Length,
62 NumericBaseType::Angle,
63 NumericBaseType::Time,
64 NumericBaseType::Frequency,
65 NumericBaseType::Resolution,
66 NumericBaseType::Flex,
67 NumericBaseType::Percent,
68];
69
70const fn all_numeric_base_types_are_in_order() -> bool {
71 let mut i = 0;
72 while i < NUMERIC_BASE_TYPE_COUNT - 1 {
73 if ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT[i] as u8 != i as u8 {
74 return false;
75 }
76 i += 1;
77 }
78 true
79}
80
81const_assert!(all_numeric_base_types_are_in_order());
82
83/// Every numeric base type except `Percent` in enum-declaration order.
84const ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT: [NumericBaseType; NUMERIC_BASE_TYPE_COUNT - 1] = [
85 NumericBaseType::Length,
86 NumericBaseType::Angle,
87 NumericBaseType::Time,
88 NumericBaseType::Frequency,
89 NumericBaseType::Resolution,
90 NumericBaseType::Flex,
91];
92
93const fn all_numeric_base_types_except_percent_are_in_order() -> bool {
94 let mut i = 0;
95 while i < NUMERIC_BASE_TYPE_COUNT - 1 {
96 if ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT[i] as u8 != i as u8 {
97 return false;
98 }
99 i += 1;
100 }
101 true
102}
103
104const_assert!(all_numeric_base_types_except_percent_are_in_order());
105
106/// https://drafts.css-houdini.org/css-typed-om-1/#numeric-typing
107///
108/// The spec models the per-base-type exponents as an ordered map keyed by base
109/// type. We use a fixed-size array indexed by `NumericBaseType` instead. A
110/// missing entry in the spec's map and a zero entry are observably equivalent
111/// for every operation the spec defines (comparisons and iteration only
112/// consider non-zero entries), so the array representation is simpler, avoids
113/// allocations, and produces the same results.
114///
115/// `non_zero_count` and `non_zero_except_percent_count` are derived fields
116/// maintained in sync with `exponents`, allowing O(1) type compatibility
117/// checks. They fit without padding into the 2 bytes following `percent_hint`,
118/// so the struct remains 32 bytes.
119#[derive(Clone, Debug)]
120#[repr(C)]
121pub struct NumericType {
122 exponents: [i32; NUMERIC_BASE_TYPE_COUNT],
123 percent_hint: Optional<NumericBaseType>,
124 non_zero_count: u8,
125 non_zero_except_percent_count: u8,
126}
127
128impl NumericType {
129 #[inline]
130 fn empty() -> Self {
131 Self {
132 exponents: [0; NUMERIC_BASE_TYPE_COUNT],
133 percent_hint: Optional::None,
134 non_zero_count: 0,
135 non_zero_except_percent_count: 0,
136 }
137 }
138
139 /// Constructs a numeric type from a single base type.
140 ///
141 /// Keep Gecko's StyleNumericType::WithBaseType() in sync with this
142 /// implementation.
143 #[inline]
144 fn with_base_type(base_type: NumericBaseType) -> Self {
145 let mut result = Self::empty();
146 result.exponents[base_type as usize] = 1;
147 result.non_zero_count = 1;
148 if base_type != NumericBaseType::Percent {
149 result.non_zero_except_percent_count = 1;
150 }
151 result
152 }
153
154 /// A numeric type whose exponent map is empty.
155 pub fn number() -> Self {
156 Self::empty()
157 }
158
159 /// A numeric type whose percent exponent is 1.
160 pub fn percent() -> Self {
161 Self::with_base_type(NumericBaseType::Percent)
162 }
163
164 /// A numeric type whose length exponent is 1.
165 pub fn length() -> Self {
166 Self::with_base_type(NumericBaseType::Length)
167 }
168
169 /// A numeric type whose angle exponent is 1.
170 pub fn angle() -> Self {
171 Self::with_base_type(NumericBaseType::Angle)
172 }
173
174 /// A numeric type whose time exponent is 1.
175 pub fn time() -> Self {
176 Self::with_base_type(NumericBaseType::Time)
177 }
178
179 /// A numeric type whose frequency exponent is 1.
180 pub fn frequency() -> Self {
181 Self::with_base_type(NumericBaseType::Frequency)
182 }
183
184 /// A numeric type whose resolution exponent is 1.
185 pub fn resolution() -> Self {
186 Self::with_base_type(NumericBaseType::Resolution)
187 }
188
189 /// A numeric type whose flex exponent is 1.
190 pub fn flex() -> Self {
191 Self::with_base_type(NumericBaseType::Flex)
192 }
193
194 /// <https://drafts.css-houdini.org/css-typed-om-1/#create-a-type-from-a-string>
195 pub fn try_from_unit(unit: &str) -> Result<Self, ()> {
196 if unit.eq_ignore_ascii_case("number") {
197 return Ok(Self::number());
198 }
199
200 if unit.eq_ignore_ascii_case("percent") {
201 return Ok(Self::percent());
202 }
203
204 if LengthUnit::from_str(unit).is_ok() {
205 return Ok(Self::length());
206 }
207
208 if AngleUnit::from_str(unit).is_ok() {
209 return Ok(Self::angle());
210 }
211
212 if TimeUnit::from_str(unit).is_ok() {
213 return Ok(Self::time());
214 }
215
216 if FrequencyUnit::from_str(unit).is_ok() {
217 return Ok(Self::frequency());
218 }
219
220 if ResolutionUnit::from_str(unit).is_ok() {
221 return Ok(Self::resolution());
222 }
223
224 if FlexUnit::matches(unit) {
225 return Ok(Self::flex());
226 }
227
228 Err(())
229 }
230
231 /// Creates a numeric type from a previously validated unit string.
232 pub fn from_unit_unchecked(unit: &str) -> Self {
233 let result = Self::try_from_unit(unit);
234 debug_assert!(result.is_ok(), "Expected a valid unit, got {unit:?}");
235
236 result.unwrap_or(Self::number())
237 }
238
239 /// Consumes the type and constructs a new type, applying the given percent hint.
240 pub fn with_percent_hint(self, hint: NumericBaseType) -> Self {
241 let mut ty = self;
242 ty.apply_percent_hint(hint);
243 ty
244 }
245
246 /// Returns the percent hint for this type.
247 pub fn percent_hint(&self) -> Optional<NumericBaseType> {
248 self.percent_hint
249 }
250
251 fn exponent(&self, base_type: NumericBaseType) -> i32 {
252 self.exponents[base_type as usize]
253 }
254
255 fn set_exponent(&mut self, base_type: NumericBaseType, new_value: i32) {
256 let old_value = self.exponent(base_type);
257 self.exponents[base_type as usize] = new_value;
258 match (old_value != 0, new_value != 0) {
259 (false, true) => {
260 self.non_zero_count += 1;
261 if base_type != NumericBaseType::Percent {
262 self.non_zero_except_percent_count += 1;
263 }
264 },
265 (true, false) => {
266 self.non_zero_count -= 1;
267 if base_type != NumericBaseType::Percent {
268 self.non_zero_except_percent_count -= 1;
269 }
270 },
271 _ => {},
272 }
273 }
274
275 fn add_exponent(&mut self, base_type: NumericBaseType, delta: i32) {
276 self.set_exponent(base_type, self.exponent(base_type) + delta);
277 }
278
279 /// Inverts this numeric type as described in the CSSMathInvert branch of
280 /// <https://drafts.css-houdini.org/css-typed-om-1/#type-of-a-cssmathvalue>
281 pub fn invert(&mut self) {
282 for exp in self.exponents.iter_mut() {
283 *exp = -*exp;
284 }
285 }
286
287 #[inline]
288 fn has_null_percent_hint(&self) -> bool {
289 self.percent_hint.is_none()
290 }
291
292 #[inline]
293 fn only_non_zero_entry_is(&self, base_type: NumericBaseType, exponent: i32) -> bool {
294 self.non_zero_count == 1 && self.exponent(base_type) == exponent
295 }
296
297 #[inline]
298 fn has_no_non_zero_entries(&self) -> bool {
299 self.non_zero_count == 0
300 }
301
302 #[inline]
303 fn matches_length_in_percentage_context(&self) -> bool {
304 self.only_non_zero_entry_is(NumericBaseType::Length, 1)
305 && (self.has_null_percent_hint()
306 || self.percent_hint == Optional::Some(NumericBaseType::Length))
307 }
308
309 // Grammar matching for CSSNumericValue types.
310 //
311 // See <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-match>.
312 //
313 // The spec defines "matches <length>" (similarly for <angle>, <time>,
314 // <frequency>, <resolution>, <flex>), and separately "matches <number>",
315 // as context-sensitive predicates on numeric types. We generally
316 // implement only the strictest context (percentages disallowed, percent
317 // hint must be null), which is what is currently needed for validating
318 // arguments in some CSSTransformComponent subclasses.
319 //
320 // The only exception is <length-percentage>, whose grammar explicitly
321 // permits percentages resolved against <length>, so it accepts both a null
322 // percent hint and a <length> percent hint.
323 //
324 // StylePropertyMap.set() would use these predicates too if it ever switches
325 // from parser based validation to a FromTyped style path, at which point
326 // the remaining context-sensitive cases would need to be added.
327
328 /// Matches <length>.
329 #[unsafe(export_name = "Servo_NumericType_MatchesLength")]
330 pub extern "C" fn matches_length(&self) -> bool {
331 self.only_non_zero_entry_is(NumericBaseType::Length, 1) && self.has_null_percent_hint()
332 }
333
334 /// Matches <angle>.
335 #[unsafe(export_name = "Servo_NumericType_MatchesAngle")]
336 pub extern "C" fn matches_angle(&self) -> bool {
337 self.only_non_zero_entry_is(NumericBaseType::Angle, 1) && self.has_null_percent_hint()
338 }
339
340 /// Matches <percentage>.
341 #[unsafe(export_name = "Servo_NumericType_MatchesPercentage")]
342 pub extern "C" fn matches_percentage(&self) -> bool {
343 self.only_non_zero_entry_is(NumericBaseType::Percent, 1)
344 && (self.has_null_percent_hint()
345 || self.percent_hint == Optional::Some(NumericBaseType::Percent))
346 }
347
348 /// Matches <length-percentage>.
349 #[unsafe(export_name = "Servo_NumericType_MatchesLengthPercentage")]
350 pub extern "C" fn matches_length_percentage(&self) -> bool {
351 self.matches_length_in_percentage_context() || self.matches_percentage()
352 }
353
354 /// Matches <number>.
355 #[unsafe(export_name = "Servo_NumericType_MatchesNumber")]
356 pub extern "C" fn matches_number(&self) -> bool {
357 self.has_no_non_zero_entries() && self.has_null_percent_hint()
358 }
359
360 /// Applies the given percent hint to this type. Note that the spec algorithm
361 /// specifically says "to a type without a percent hint", so this will not
362 /// modify the type if it alreay has a percent hint.
363 ///
364 /// <https://drafts.css-houdini.org/css-typed-om-1/#apply-the-percent-hint>
365 pub fn apply_percent_hint(&mut self, hint: NumericBaseType) {
366 if self.percent_hint.is_some() {
367 return;
368 }
369
370 // Step 1.
371 self.percent_hint = Optional::Some(hint);
372
373 // Step 2.
374 // No-op for our array representation, the hint's slot already exists
375 // ("missing" and "zero" mean the same thing).
376
377 // Step 3.
378 if hint != NumericBaseType::Percent {
379 let percent = self.exponent(NumericBaseType::Percent);
380 if percent != 0 {
381 self.add_exponent(hint, percent);
382 self.set_exponent(NumericBaseType::Percent, 0);
383 }
384 }
385 }
386
387 /// <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-add-two-types>
388 ///
389 /// The algorithm has some complexity and uses branches rather than
390 /// numbered sub-steps, so the implementation below quotes spec text
391 /// inline. This is more verbose than usual, but should help map each
392 /// branch back to the spec when reviewing or debugging.
393 pub fn add_two_types(type1: &NumericType, type2: &NumericType) -> Result<Self, ()> {
394 // Step 1.
395 // "Replace type1 with a fresh copy of type1, and type2 with a fresh
396 // copy of type2."
397 let mut type1 = type1.clone();
398 let mut type2 = type2.clone();
399 // "Let finalType be a new type with an initially empty ordered map and
400 // an initially null percent hint."
401 // We don't need a separate finalType with the array representation,
402 // when the entries match, type1 already represents the merged result.
403
404 // Step 2.
405 match (type1.percent_hint, type2.percent_hint) {
406 // Step 2, first branch.
407 // "If both type1 and type2 have non-null percent hints with
408 // different values"
409 (Optional::Some(h1), Optional::Some(h2)) if h1 != h2 => {
410 // "The types can't be added. Return failure."
411 return Err(());
412 },
413 // Step 2, second branch.
414 // "If type1 has a non-null percent hint hint and type2 doesn't"
415 (Optional::Some(hint), Optional::None) => {
416 // "Apply the percent hint hint to type2."
417 type2.apply_percent_hint(hint)
418 },
419 // "Vice versa if type2 has a non-null percent hint and type1
420 // doesn't."
421 (Optional::None, Optional::Some(hint)) => type1.apply_percent_hint(hint),
422 // Step 3, third branch.
423 // "Otherwise"
424 _ => {
425 // "Continue to the next step."
426 },
427 }
428
429 // Step 3, first branch.
430 // "If all the entries of type1 with non-zero values are contained in
431 // type2 with the same value, and vice-versa"
432 // With the array representation, the check reduces to array equality
433 // ("missing" and "zero" mean the same thing).
434 if type1.exponents == type2.exponents {
435 // "Copy all of type1’s entries to finalType, and then copy all of
436 // type2’s entries to finalType that finalType doesn’t already
437 // contain. Set finalType’s percent hint to type1’s percent hint.
438 // Return finalType."
439 // As noted in Step1, type1 already represents the merged result,
440 // so extra finalType is not needed.
441 return Ok(type1);
442 }
443
444 // Step 3, second branch.
445 // "If type1 and/or type2 contain 'percent' with a non-zero value, and
446 // type1 and/or type2 contain a key other than 'percent' with a
447 // non-zero value"
448 if (type1.exponent(NumericBaseType::Percent) != 0
449 || type2.exponent(NumericBaseType::Percent) != 0)
450 && (type1.non_zero_except_percent_count != 0
451 || type2.non_zero_except_percent_count != 0)
452 {
453 // "For each base type other than 'percent' hint:"
454 for &hint in ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT.iter() {
455 // Step 3.1.
456 // "Provisionally apply the percent hint hint to both type1
457 // and type2."
458 // Instead of modifying type1 and type2 directly and then
459 // eventually reverting them to the original state, we just
460 // clone them.
461 let mut type1 = type1.clone();
462 let mut type2 = type2.clone();
463 type1.apply_percent_hint(hint);
464 type2.apply_percent_hint(hint);
465
466 // Step 3.2.
467 // "If, afterwards, all the entries of type1 with non-zero
468 // values are contained in type2 with the same value, and vice
469 // versa,"
470 // With the array representation, the check reduces to array
471 // equality ("missing" and "zero" mean the same thing).
472 if type1.exponents == type2.exponents {
473 // "then copy all of type1’s entries to finalType, and
474 // then copy all of type2’s entries to finalType that
475 // finalType doesn’t already contain. Set finalType’s
476 // percent hint to hint. Return finalType."
477 // type1 already represents the merged result, so extra
478 // finalType is not needed.
479 return Ok(type1);
480 }
481
482 // Step 3.3.
483 // "Otherwise, revert type1 and type2 to their state at the
484 // start of this loop."
485 // The revert is implicit, t1 and t2 are discarded between
486 // iterations.
487 }
488 // "If the loop finishes without returning finalType, then the
489 // types can’t be added. Return failure."
490 return Err(());
491 }
492
493 // Step 3, third branch.
494 // "Otherwise"
495 // "The types can't be added. Return failure."
496 Err(())
497 }
498
499 /// <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-multiply-two-types>
500 ///
501 /// Spec text is quoted inline to make each step easy to map back to the
502 /// algorithm during review.
503 pub fn multiply_two_types(type1: &NumericType, type2: &NumericType) -> Result<Self, ()> {
504 // Step 1.
505 // "Replace type1 with a fresh copy of type1, and type2 with a fresh
506 // copy of type2."
507 let mut type1 = type1.clone();
508 let mut type2 = type2.clone();
509 // "Let finalType be a new type with an initially empty ordered map
510 // and an initially null percent hint."
511 // We don't need a separate finalType with the array representation,
512 // since multiplying types is equivalent to adding the exponents,
513 // type1 can be used directly.
514
515 match (type1.percent_hint, type2.percent_hint) {
516 // Step 2.
517 // "If both type1 and type2 have non-null percent hints with
518 // different values, the types can't be multiplied."
519 (Optional::Some(h1), Optional::Some(h2)) if h1 != h2 => {
520 // "Return failure."
521 return Err(());
522 },
523 // Step 3.
524 // "If type1 has a non-null percent hint hint and type2 doesn't, "
525 (Optional::Some(hint), Optional::None) => {
526 // "apply the percent hint hint to type2."
527 type2.apply_percent_hint(hint)
528 },
529 // "Vice versa if type2 has a non-null percent hint and type1
530 // doesn't."
531 (Optional::None, Optional::Some(hint)) => type1.apply_percent_hint(hint),
532 _ => {},
533 }
534
535 // Step 4.
536 // "Copy all of type1's entries to finalType,"
537 // As noted in Step 1, type1 can be used directly, so a separate
538 // finalType is not needed.
539 // "then for each baseType -> power of type2:"
540 for &base_type in ALL_NUMERIC_BASE_TYPES.iter() {
541 let power = type2.exponent(base_type);
542
543 // The spec iterates only the baseType → power entries present in
544 // type2. With the array representation we iterate all base types,
545 // so skip entries whose exponent is zero.
546 if power == 0 {
547 continue;
548 }
549
550 // Step 4.1.
551 // "If finalType[baseType] exists, increment its value by power."
552 // Step 4.2.
553 // "Otherwise, set finalType[baseType] to power."
554 // With the array representation, both cases are handled by adding
555 // the exponent, because missing entries are represented as zero.
556 type1.add_exponent(base_type, power);
557 }
558 // "Set finalType's percent hint to type1's percent hint."
559 // After Step 3, type1's percent hint equals type2's in all surviving
560 // cases (both null, both equal, or the null side was filled in), so
561 // type1 already has the final hint.
562
563 // Step 5.
564 // "Return finalType."
565 Ok(type1)
566 }
567
568 fn combine_types<'a, I>(
569 mut types: I,
570 combine: fn(&NumericType, &NumericType) -> Result<NumericType, ()>,
571 ) -> Result<Self, ()>
572 where
573 I: Iterator<Item = &'a NumericType>,
574 {
575 let mut result = types.next().ok_or(())?.clone();
576
577 for next in types {
578 result = combine(&result, next)?;
579 }
580
581 Ok(result)
582 }
583
584 /// Applies the add two types algorithm repeatedly across a sequence of
585 /// numeric types, returning the combined type.
586 pub fn add_types<'a, I>(types: I) -> Result<Self, ()>
587 where
588 I: Iterator<Item = &'a NumericType>,
589 {
590 Self::combine_types(types, Self::add_two_types)
591 }
592
593 /// Applies the multiply two types algorithm repeatedly across a sequence of
594 /// numeric types, returning the combined type.
595 pub fn multiply_types<'a, I>(types: I) -> Result<Self, ()>
596 where
597 I: Iterator<Item = &'a NumericType>,
598 {
599 Self::combine_types(types, Self::multiply_two_types)
600 }
601
602 /// Returns whether this type is a dimensionless number.
603 pub fn is_number(&self) -> bool {
604 self.non_zero_count == 0
605 }
606
607 /// Returns a `CalcType` if this type represents a single data type,
608 /// like <length> or <number>.
609 pub fn as_calc_type(&self) -> Result<CalcType, ()> {
610 match self.non_zero_count {
611 0 => return Ok(CalcType::Number),
612 1 => {},
613 _ => return Err(()),
614 };
615
616 for base_type in ALL_NUMERIC_BASE_TYPES.iter() {
617 let exponent = self.exponent(*base_type);
618 if exponent == 0 {
619 continue;
620 }
621 if exponent != 1 {
622 return Err(());
623 }
624
625 // We checked before the loop that there is only one numeric
626 // base type with a non-zero exponent.
627 return Ok(match base_type {
628 NumericBaseType::Length => CalcType::Length,
629 NumericBaseType::Angle => CalcType::Angle,
630 NumericBaseType::Time => CalcType::Time,
631 NumericBaseType::Resolution => CalcType::Resolution,
632 NumericBaseType::Percent => CalcType::Percentage,
633 NumericBaseType::Frequency | NumericBaseType::Flex => return Err(()),
634 });
635 }
636
637 debug_assert!(false, "non_zero_count was 1 but all exponents were 0");
638 Ok(CalcType::Number)
639 }
640}