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::values::generics::grid::FlexUnit;
8use crate::values::generics::Optional;
9use crate::values::specified::angle::AngleUnit;
10use crate::values::specified::frequency::FrequencyUnit;
11use crate::values::specified::length::LengthUnit;
12use crate::values::specified::resolution::ResolutionUnit;
13use crate::values::specified::time::TimeUnit;
14
15/// https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-base-type
16#[derive(Clone, Copy, Debug, PartialEq)]
17#[repr(u8)]
18pub enum NumericBaseType {
19    /// A `<length>` unit.
20    Length,
21
22    /// An `<angle>` unit.
23    Angle,
24
25    /// A `<time>` unit.
26    Time,
27
28    /// A `<frequency>` unit.
29    Frequency,
30
31    /// A `<resolution>` unit.
32    Resolution,
33
34    /// The `<flex>` unit.
35    Flex,
36
37    /// The percentage unit.
38    Percent,
39}
40
41#[doc(hidden)] // Need to be public so that cbindgen generates it.
42pub const NUMERIC_BASE_TYPE_COUNT: usize = 7;
43
44const_assert!(NumericBaseType::Percent as usize + 1 == NUMERIC_BASE_TYPE_COUNT);
45
46/// Every numeric base type in enum-declaration order.
47pub const ALL_NUMERIC_BASE_TYPES: [NumericBaseType; NUMERIC_BASE_TYPE_COUNT] = [
48    NumericBaseType::Length,
49    NumericBaseType::Angle,
50    NumericBaseType::Time,
51    NumericBaseType::Frequency,
52    NumericBaseType::Resolution,
53    NumericBaseType::Flex,
54    NumericBaseType::Percent,
55];
56
57const fn all_numeric_base_types_are_in_order() -> bool {
58    let mut i = 0;
59    while i < NUMERIC_BASE_TYPE_COUNT - 1 {
60        if ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT[i] as u8 != i as u8 {
61            return false;
62        }
63        i += 1;
64    }
65    true
66}
67
68const_assert!(all_numeric_base_types_are_in_order());
69
70/// Every numeric base type except `Percent` in enum-declaration order.
71const ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT: [NumericBaseType; NUMERIC_BASE_TYPE_COUNT - 1] = [
72    NumericBaseType::Length,
73    NumericBaseType::Angle,
74    NumericBaseType::Time,
75    NumericBaseType::Frequency,
76    NumericBaseType::Resolution,
77    NumericBaseType::Flex,
78];
79
80const fn all_numeric_base_types_except_percent_are_in_order() -> bool {
81    let mut i = 0;
82    while i < NUMERIC_BASE_TYPE_COUNT - 1 {
83        if ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT[i] as u8 != i as u8 {
84            return false;
85        }
86        i += 1;
87    }
88    true
89}
90
91const_assert!(all_numeric_base_types_except_percent_are_in_order());
92
93/// https://drafts.css-houdini.org/css-typed-om-1/#numeric-typing
94///
95/// The spec models the per-base-type exponents as an ordered map keyed by base
96/// type. We use a fixed-size array indexed by `NumericBaseType` instead. A
97/// missing entry in the spec's map and a zero entry are observably equivalent
98/// for every operation the spec defines (comparisons and iteration only
99/// consider non-zero entries), so the array representation is simpler, avoids
100/// allocations, and produces the same results.
101///
102/// `non_zero_count` and `non_zero_except_percent_count` are derived fields
103/// maintained in sync with `exponents`, allowing O(1) type compatibility
104/// checks. They fit without padding into the 2 bytes following `percent_hint`,
105/// so the struct remains 32 bytes.
106#[derive(Clone, Debug)]
107#[repr(C)]
108pub struct NumericType {
109    exponents: [i32; NUMERIC_BASE_TYPE_COUNT],
110    percent_hint: Optional<NumericBaseType>,
111    non_zero_count: u8,
112    non_zero_except_percent_count: u8,
113}
114
115impl NumericType {
116    #[inline]
117    fn empty() -> Self {
118        Self {
119            exponents: [0; NUMERIC_BASE_TYPE_COUNT],
120            percent_hint: Optional::None,
121            non_zero_count: 0,
122            non_zero_except_percent_count: 0,
123        }
124    }
125
126    /// Constructs a numeric type from a single base type.
127    ///
128    /// Keep Gecko's StyleNumericType::WithBaseType() in sync with this
129    /// implementation.
130    #[inline]
131    fn with_base_type(base_type: NumericBaseType) -> Self {
132        let mut result = Self::empty();
133        result.exponents[base_type as usize] = 1;
134        result.non_zero_count = 1;
135        if base_type != NumericBaseType::Percent {
136            result.non_zero_except_percent_count = 1;
137        }
138        result
139    }
140
141    /// A numeric type whose exponent map is empty.
142    pub fn number() -> Self {
143        Self::empty()
144    }
145
146    /// A numeric type whose percent exponent is 1.
147    pub fn percent() -> Self {
148        Self::with_base_type(NumericBaseType::Percent)
149    }
150
151    /// A numeric type whose length exponent is 1.
152    pub fn length() -> Self {
153        Self::with_base_type(NumericBaseType::Length)
154    }
155
156    /// A numeric type whose angle exponent is 1.
157    pub fn angle() -> Self {
158        Self::with_base_type(NumericBaseType::Angle)
159    }
160
161    /// A numeric type whose time exponent is 1.
162    pub fn time() -> Self {
163        Self::with_base_type(NumericBaseType::Time)
164    }
165
166    /// A numeric type whose frequency exponent is 1.
167    pub fn frequency() -> Self {
168        Self::with_base_type(NumericBaseType::Frequency)
169    }
170
171    /// A numeric type whose resolution exponent is 1.
172    pub fn resolution() -> Self {
173        Self::with_base_type(NumericBaseType::Resolution)
174    }
175
176    /// A numeric type whose flex exponent is 1.
177    pub fn flex() -> Self {
178        Self::with_base_type(NumericBaseType::Flex)
179    }
180
181    /// <https://drafts.css-houdini.org/css-typed-om-1/#create-a-type-from-a-string>
182    pub fn try_from_unit(unit: &str) -> Result<Self, ()> {
183        if unit.eq_ignore_ascii_case("number") {
184            return Ok(Self::number());
185        }
186
187        if unit.eq_ignore_ascii_case("percent") {
188            return Ok(Self::percent());
189        }
190
191        if LengthUnit::from_str(unit).is_ok() {
192            return Ok(Self::length());
193        }
194
195        if AngleUnit::from_str(unit).is_ok() {
196            return Ok(Self::angle());
197        }
198
199        if TimeUnit::from_str(unit).is_ok() {
200            return Ok(Self::time());
201        }
202
203        if FrequencyUnit::from_str(unit).is_ok() {
204            return Ok(Self::frequency());
205        }
206
207        if ResolutionUnit::from_str(unit).is_ok() {
208            return Ok(Self::resolution());
209        }
210
211        if FlexUnit::matches(unit) {
212            return Ok(Self::flex());
213        }
214
215        Err(())
216    }
217
218    /// Creates a numeric type from a previously validated unit string.
219    pub fn from_unit_unchecked(unit: &str) -> Self {
220        let result = Self::try_from_unit(unit);
221        debug_assert!(result.is_ok(), "Expected a valid unit, got {unit:?}");
222
223        result.unwrap_or(Self::number())
224    }
225
226    fn exponent(&self, base_type: NumericBaseType) -> i32 {
227        self.exponents[base_type as usize]
228    }
229
230    fn set_exponent(&mut self, base_type: NumericBaseType, new_value: i32) {
231        let old_value = self.exponent(base_type);
232        self.exponents[base_type as usize] = new_value;
233        match (old_value != 0, new_value != 0) {
234            (false, true) => {
235                self.non_zero_count += 1;
236                if base_type != NumericBaseType::Percent {
237                    self.non_zero_except_percent_count += 1;
238                }
239            },
240            (true, false) => {
241                self.non_zero_count -= 1;
242                if base_type != NumericBaseType::Percent {
243                    self.non_zero_except_percent_count -= 1;
244                }
245            },
246            _ => {},
247        }
248    }
249
250    fn add_exponent(&mut self, base_type: NumericBaseType, delta: i32) {
251        self.set_exponent(base_type, self.exponent(base_type) + delta);
252    }
253
254    /// <https://drafts.css-houdini.org/css-typed-om-1/#apply-the-percent-hint>
255    fn apply_percent_hint(&mut self, hint: NumericBaseType) {
256        // Step 1.
257        self.percent_hint = Optional::Some(hint);
258
259        // Step 2.
260        // No-op for our array representation, the hint's slot already exists
261        // ("missing" and "zero" mean the same thing).
262
263        // Step 3.
264        if hint != NumericBaseType::Percent {
265            let percent = self.exponent(NumericBaseType::Percent);
266            if percent != 0 {
267                self.add_exponent(hint, percent);
268                self.set_exponent(NumericBaseType::Percent, 0);
269            }
270        }
271    }
272
273    /// <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-add-two-types>
274    ///
275    /// The algorithm has some complexity and uses branches rather than
276    /// numbered sub-steps, so the implementation below quotes spec text
277    /// inline. This is more verbose than usual, but should help map each
278    /// branch back to the spec when reviewing or debugging.
279    fn add_two_types(type1: &NumericType, type2: &NumericType) -> Result<Self, ()> {
280        // Step 1.
281        // "Replace type1 with a fresh copy of type1, and type2 with a fresh
282        // copy of type2."
283        let mut type1 = type1.clone();
284        let mut type2 = type2.clone();
285        // "Let finalType be a new type with an initially empty ordered map and
286        // an initially null percent hint."
287        // We don't need a separate finalType with the array representation,
288        // when the entries match, type1 already represents the merged result.
289
290        // Step 2.
291        match (type1.percent_hint, type2.percent_hint) {
292            // Step 2, first branch.
293            // "If both type1 and type2 have non-null percent hints with
294            // different values"
295            (Optional::Some(h1), Optional::Some(h2)) if h1 != h2 => {
296                // "The types can't be added. Return failure."
297                return Err(());
298            },
299            // Step 2, second branch.
300            // "If type1 has a non-null percent hint hint and type2 doesn't"
301            (Optional::Some(hint), Optional::None) => {
302                // "Apply the percent hint hint to type2."
303                type2.apply_percent_hint(hint)
304            },
305            // "Vice versa if type2 has a non-null percent hint and type1
306            // doesn't."
307            (Optional::None, Optional::Some(hint)) => type1.apply_percent_hint(hint),
308            // Step 3, third branch.
309            // "Otherwise"
310            _ => {
311                // "Continue to the next step."
312            },
313        }
314
315        // Step 3, first branch.
316        // "If all the entries of type1 with non-zero values are contained in
317        // type2 with the same value, and vice-versa"
318        // With the array representation, the check reduces to array equality
319        // ("missing" and "zero" mean the same thing).
320        if type1.exponents == type2.exponents {
321            // "Copy all of type1’s entries to finalType, and then copy all of
322            // type2’s entries to finalType that finalType doesn’t already
323            // contain. Set finalType’s percent hint to type1’s percent hint.
324            // Return finalType."
325            // As noted in Step1, type1 already represents the merged result,
326            // so extra finalType is not needed.
327            return Ok(type1);
328        }
329
330        // Step 3, second branch.
331        // "If type1 and/or type2 contain 'percent' with a non-zero value, and
332        // type1 and/or type2 contain a key other than 'percent' with a
333        // non-zero value"
334        if (type1.exponent(NumericBaseType::Percent) != 0
335            || type2.exponent(NumericBaseType::Percent) != 0)
336            && (type1.non_zero_except_percent_count != 0
337                || type2.non_zero_except_percent_count != 0)
338        {
339            // "For each base type other than 'percent' hint:"
340            for &hint in ALL_NUMERIC_BASE_TYPES_EXCEPT_PERCENT.iter() {
341                // Step 3.1.
342                // "Provisionally apply the percent hint hint to both type1
343                // and type2."
344                // Instead of modifying type1 and type2 directly and then
345                // eventually reverting them to the original state, we just
346                // clone them.
347                let mut type1 = type1.clone();
348                let mut type2 = type2.clone();
349                type1.apply_percent_hint(hint);
350                type2.apply_percent_hint(hint);
351
352                // Step 3.2.
353                // "If, afterwards, all the entries of type1 with non-zero
354                // values are contained in type2 with the same value, and vice
355                // versa,"
356                // With the array representation, the check reduces to array
357                // equality ("missing" and "zero" mean the same thing).
358                if type1.exponents == type2.exponents {
359                    // "then copy all of type1’s entries to finalType, and
360                    // then copy all of type2’s entries to finalType that
361                    // finalType doesn’t already contain. Set finalType’s
362                    // percent hint to hint. Return finalType."
363                    // type1 already represents the merged result, so extra
364                    // finalType is not needed.
365                    return Ok(type1);
366                }
367
368                // Step 3.3.
369                // "Otherwise, revert type1 and type2 to their state at the
370                // start of this loop."
371                // The revert is implicit, t1 and t2 are discarded between
372                // iterations.
373            }
374            // "If the loop finishes without returning finalType, then the
375            // types can’t be added. Return failure."
376            return Err(());
377        }
378
379        // Step 3, third branch.
380        // "Otherwise"
381        // "The types can't be added. Return failure."
382        Err(())
383    }
384
385    /// <https://drafts.css-houdini.org/css-typed-om-1/#cssnumericvalue-multiply-two-types>
386    ///
387    /// Spec text is quoted inline to make each step easy to map back to the
388    /// algorithm during review.
389    fn multiply_two_types(type1: &NumericType, type2: &NumericType) -> Result<Self, ()> {
390        // Step 1.
391        // "Replace type1 with a fresh copy of type1, and type2 with a fresh
392        // copy of type2."
393        let mut type1 = type1.clone();
394        let mut type2 = type2.clone();
395        // "Let finalType be a new type with an initially empty ordered map
396        // and an initially null percent hint."
397        // We don't need a separate finalType with the array representation,
398        // since multiplying types is equivalent to adding the exponents,
399        // type1 can be used directly.
400
401        match (type1.percent_hint, type2.percent_hint) {
402            // Step 2.
403            // "If both type1 and type2 have non-null percent hints with
404            // different values, the types can't be multiplied."
405            (Optional::Some(h1), Optional::Some(h2)) if h1 != h2 => {
406                // "Return failure."
407                return Err(());
408            },
409            // Step 3.
410            // "If type1 has a non-null percent hint hint and type2 doesn't, "
411            (Optional::Some(hint), Optional::None) => {
412                // "apply the percent hint hint to type2."
413                type2.apply_percent_hint(hint)
414            },
415            // "Vice versa if type2 has a non-null percent hint and type1
416            // doesn't."
417            (Optional::None, Optional::Some(hint)) => type1.apply_percent_hint(hint),
418            _ => {},
419        }
420
421        // Step 4.
422        // "Copy all of type1's entries to finalType,"
423        // As noted in Step 1, type1 can be used directly, so a separate
424        // finalType is not needed.
425        // "then for each baseType -> power of type2:"
426        for &base_type in ALL_NUMERIC_BASE_TYPES.iter() {
427            let power = type2.exponent(base_type);
428
429            // The spec iterates only the baseType → power entries present in
430            // type2. With the array representation we iterate all base types,
431            // so skip entries whose exponent is zero.
432            if power == 0 {
433                continue;
434            }
435
436            // Step 4.1.
437            // "If finalType[baseType] exists, increment its value by power."
438            // Step 4.2.
439            // "Otherwise, set finalType[baseType] to power."
440            // With the array representation, both cases are handled by adding
441            // the exponent, because missing entries are represented as zero.
442            type1.add_exponent(base_type, power);
443        }
444        // "Set finalType's percent hint to type1's percent hint."
445        // After Step 3, type1's percent hint equals type2's in all surviving
446        // cases (both null, both equal, or the null side was filled in), so
447        // type1 already has the final hint.
448
449        // Step 5.
450        // "Return finalType."
451        Ok(type1)
452    }
453
454    fn combine_types<'a, I>(
455        mut types: I,
456        combine: fn(&NumericType, &NumericType) -> Result<NumericType, ()>,
457    ) -> Result<Self, ()>
458    where
459        I: Iterator<Item = &'a NumericType>,
460    {
461        let mut result = types.next().ok_or(())?.clone();
462
463        for next in types {
464            result = combine(&result, next)?;
465        }
466
467        Ok(result)
468    }
469
470    /// Applies the add two types algorithm repeatedly across a sequence of
471    /// numeric types, returning the combined type.
472    pub fn add_types<'a, I>(types: I) -> Result<Self, ()>
473    where
474        I: Iterator<Item = &'a NumericType>,
475    {
476        Self::combine_types(types, Self::add_two_types)
477    }
478
479    /// Applies the multiply two types algorithm repeatedly across a sequence of
480    /// numeric types, returning the combined type.
481    pub fn multiply_types<'a, I>(types: I) -> Result<Self, ()>
482    where
483        I: Iterator<Item = &'a NumericType>,
484    {
485        Self::combine_types(types, Self::multiply_two_types)
486    }
487}