Skip to main content

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}