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}