sixtyfps_compilerlib/
langtype.rs

1// Copyright © SixtyFPS GmbH <info@sixtyfps.io>
2// SPDX-License-Identifier: (GPL-3.0-only OR LicenseRef-SixtyFPS-commercial)
3
4use std::borrow::Cow;
5use std::collections::{BTreeMap, HashMap, HashSet};
6use std::fmt::Display;
7use std::rc::Rc;
8
9use itertools::Itertools;
10
11use crate::expression_tree::{Expression, Unit};
12use crate::object_tree::Component;
13use crate::parser::syntax_nodes;
14use crate::typeregister::TypeRegister;
15
16#[derive(Debug, Clone)]
17pub enum Type {
18    /// Correspond to an uninitialized type, or an error
19    Invalid,
20    /// The type of an expression that return nothing
21    Void,
22    /// The type of a property two way binding whose type was not yet inferred
23    InferredProperty,
24    /// The type of a callback alias whose type was not yet inferred
25    InferredCallback,
26    Component(Rc<Component>),
27    Builtin(Rc<BuiltinElement>),
28    Native(Rc<NativeClass>),
29
30    Callback {
31        return_type: Option<Box<Type>>,
32        args: Vec<Type>,
33    },
34    Function {
35        return_type: Box<Type>,
36        args: Vec<Type>,
37    },
38
39    // Other property types:
40    Float32,
41    Int32,
42    String,
43    Color,
44    Duration,
45    PhysicalLength,
46    LogicalLength,
47    Angle,
48    Percent,
49    Image,
50    Bool,
51    Model,
52    PathData, // Either a vector of path elements or a two vectors of events and coordinates
53    Easing,
54    Brush,
55    /// This is usually a model
56    Array(Box<Type>),
57    Struct {
58        fields: BTreeMap<String, Type>,
59        /// When declared in .60 as  `struct Foo := { }`, then the name is "Foo"
60        /// When there is no node, but there is a name, then it is a builtin type
61        name: Option<String>,
62        /// When declared in .60, this is the node of the declaration.
63        node: Option<syntax_nodes::ObjectType>,
64    },
65    Enumeration(Rc<Enumeration>),
66
67    /// A type made up of the product of several "unit" types.
68    /// The first parameter is the unit, and the second parameter is the power.
69    /// The vector should be sorted by 1) the power, 2) the unit.
70    UnitProduct(Vec<(Unit, i8)>),
71
72    ElementReference,
73
74    /// This is a `SharedArray<f32>`
75    LayoutCache,
76}
77
78impl core::cmp::PartialEq for Type {
79    fn eq(&self, other: &Self) -> bool {
80        match self {
81            Type::Invalid => matches!(other, Type::Invalid),
82            Type::Void => matches!(other, Type::Void),
83            Type::InferredProperty => matches!(other, Type::InferredProperty),
84            Type::InferredCallback => matches!(other, Type::InferredCallback),
85            Type::Component(a) => matches!(other, Type::Component(b) if Rc::ptr_eq(a, b)),
86            Type::Builtin(a) => matches!(other, Type::Builtin(b) if Rc::ptr_eq(a, b)),
87            Type::Native(a) => matches!(other, Type::Native(b) if Rc::ptr_eq(a, b)),
88            Type::Callback { args: a, return_type: ra } => {
89                matches!(other, Type::Callback { args: b, return_type: rb } if a == b && ra == rb)
90            }
91            Type::Function { return_type: lhs_rt, args: lhs_args } => {
92                matches!(other, Type::Function { return_type: rhs_rt, args: rhs_args } if lhs_rt == rhs_rt && lhs_args == rhs_args)
93            }
94            Type::Float32 => matches!(other, Type::Float32),
95            Type::Int32 => matches!(other, Type::Int32),
96            Type::String => matches!(other, Type::String),
97            Type::Color => matches!(other, Type::Color),
98            Type::Duration => matches!(other, Type::Duration),
99            Type::Angle => matches!(other, Type::Angle),
100            Type::PhysicalLength => matches!(other, Type::PhysicalLength),
101            Type::LogicalLength => matches!(other, Type::LogicalLength),
102            Type::Percent => matches!(other, Type::Percent),
103            Type::Image => matches!(other, Type::Image),
104            Type::Bool => matches!(other, Type::Bool),
105            Type::Model => matches!(other, Type::Model),
106            Type::PathData => matches!(other, Type::PathData),
107            Type::Easing => matches!(other, Type::Easing),
108            Type::Brush => matches!(other, Type::Brush),
109            Type::Array(a) => matches!(other, Type::Array(b) if a == b),
110            Type::Struct { fields, name, node: _ } => {
111                matches!(other, Type::Struct{fields: f, name: n, node: _} if fields == f && name == n)
112            }
113            Type::Enumeration(lhs) => matches!(other, Type::Enumeration(rhs) if lhs == rhs),
114            Type::UnitProduct(a) => matches!(other, Type::UnitProduct(b) if a == b),
115            Type::ElementReference => matches!(other, Type::ElementReference),
116            Type::LayoutCache => matches!(other, Type::LayoutCache),
117        }
118    }
119}
120
121impl Display for Type {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        match self {
124            Type::Invalid => write!(f, "<error>"),
125            Type::Void => write!(f, "void"),
126            Type::InferredProperty => write!(f, "?"),
127            Type::InferredCallback => write!(f, "callback"),
128            Type::Component(c) => c.id.fmt(f),
129            Type::Builtin(b) => b.name.fmt(f),
130            Type::Native(b) => b.class_name.fmt(f),
131            Type::Callback { args, return_type } => {
132                write!(f, "callback")?;
133                if !args.is_empty() {
134                    write!(f, "(")?;
135                    for (i, arg) in args.iter().enumerate() {
136                        if i > 0 {
137                            write!(f, ",")?;
138                        }
139                        write!(f, "{}", arg)?;
140                    }
141                    write!(f, ")")?
142                }
143                if let Some(rt) = return_type {
144                    write!(f, "-> {}", rt)?;
145                }
146                Ok(())
147            }
148            Type::Function { return_type, args } => {
149                write!(f, "function(")?;
150                for (i, arg) in args.iter().enumerate() {
151                    if i > 0 {
152                        write!(f, ",")?;
153                    }
154                    write!(f, "{}", arg)?;
155                }
156                write!(f, ") -> {}", return_type)
157            }
158            Type::Float32 => write!(f, "float"),
159            Type::Int32 => write!(f, "int"),
160            Type::String => write!(f, "string"),
161            Type::Duration => write!(f, "duration"),
162            Type::Angle => write!(f, "angle"),
163            Type::PhysicalLength => write!(f, "physical-length"),
164            Type::LogicalLength => write!(f, "length"),
165            Type::Percent => write!(f, "percent"),
166            Type::Color => write!(f, "color"),
167            Type::Image => write!(f, "image"),
168            Type::Bool => write!(f, "bool"),
169            Type::Model => write!(f, "model"),
170            Type::Array(t) => write!(f, "[{}]", t),
171            Type::Struct { name: Some(name), .. } => write!(f, "{}", name),
172            Type::Struct { fields, name: None, .. } => {
173                write!(f, "{{ ")?;
174                for (k, v) in fields {
175                    write!(f, "{}: {},", k, v)?;
176                }
177                write!(f, "}}")
178            }
179
180            Type::PathData => write!(f, "pathdata"),
181            Type::Easing => write!(f, "easing"),
182            Type::Brush => write!(f, "brush"),
183            Type::Enumeration(enumeration) => write!(f, "enum {}", enumeration.name),
184            Type::UnitProduct(vec) => {
185                const POWERS: &[char] = &['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
186                let mut x = vec.iter().map(|(unit, power)| {
187                    if *power == 1 {
188                        return unit.to_string();
189                    }
190                    let mut res = format!("{}{}", unit, if *power < 0 { "⁻" } else { "" });
191                    let value = power.abs().to_string();
192                    for x in value.as_bytes() {
193                        res.push(POWERS[(x - b'0') as usize]);
194                    }
195
196                    res
197                });
198                write!(f, "({})", x.join("×"))
199            }
200            Type::ElementReference => write!(f, "element ref"),
201            Type::LayoutCache => write!(f, "layout cache"),
202        }
203    }
204}
205
206impl Type {
207    /// valid type for properties
208    pub fn is_property_type(&self) -> bool {
209        matches!(
210            self,
211            Self::Float32
212                | Self::Int32
213                | Self::String
214                | Self::Color
215                | Self::Duration
216                | Self::Angle
217                | Self::PhysicalLength
218                | Self::LogicalLength
219                | Self::Percent
220                | Self::Image
221                | Self::Bool
222                | Self::Model
223                | Self::Easing
224                | Self::Enumeration(_)
225                | Self::ElementReference
226                | Self::Struct { .. }
227                | Self::Array(_)
228                | Self::Brush
229                | Self::InferredProperty
230        )
231    }
232
233    pub fn ok_for_public_api(&self) -> bool {
234        !matches!(self, Self::Easing)
235    }
236
237    pub fn lookup_property<'a>(&self, name: &'a str) -> PropertyLookupResult<'a> {
238        match self {
239            Type::Component(c) => c.root_element.borrow().lookup_property(name),
240            Type::Builtin(b) => {
241                let resolved_name =
242                    if let Some(alias_name) = b.native_class.lookup_alias(name.as_ref()) {
243                        Cow::Owned(alias_name.to_string())
244                    } else {
245                        Cow::Borrowed(name)
246                    };
247                match b.properties.get(resolved_name.as_ref()) {
248                    None => {
249                        if b.is_non_item_type {
250                            PropertyLookupResult { resolved_name, property_type: Type::Invalid }
251                        } else {
252                            crate::typeregister::reserved_property(name)
253                        }
254                    }
255                    Some(p) => PropertyLookupResult { resolved_name, property_type: p.ty.clone() },
256                }
257            }
258            Type::Native(n) => {
259                let resolved_name = if let Some(alias_name) = n.lookup_alias(name.as_ref()) {
260                    Cow::Owned(alias_name.to_string())
261                } else {
262                    Cow::Borrowed(name)
263                };
264                let property_type =
265                    n.lookup_property(resolved_name.as_ref()).cloned().unwrap_or_default();
266                PropertyLookupResult { resolved_name, property_type }
267            }
268            _ => PropertyLookupResult {
269                resolved_name: Cow::Borrowed(name),
270                property_type: Type::Invalid,
271            },
272        }
273    }
274
275    /// List of sub properties valid for the auto completion
276    pub fn property_list(&self) -> Vec<(String, Type)> {
277        match self {
278            Type::Component(c) => {
279                let mut r = c.root_element.borrow().base_type.property_list();
280                r.extend(
281                    c.root_element
282                        .borrow()
283                        .property_declarations
284                        .iter()
285                        .map(|(k, d)| (k.clone(), d.property_type.clone())),
286                );
287                r
288            }
289            Type::Builtin(b) => {
290                b.properties.iter().map(|(k, t)| (k.clone(), t.ty.clone())).collect()
291            }
292            Type::Native(n) => {
293                n.properties.iter().map(|(k, t)| (k.clone(), t.ty.clone())).collect()
294            }
295            _ => Vec::new(),
296        }
297    }
298
299    pub fn lookup_type_for_child_element(
300        &self,
301        name: &str,
302        tr: &TypeRegister,
303    ) -> Result<Type, String> {
304        match self {
305            Type::Component(component) => {
306                return component
307                    .root_element
308                    .borrow()
309                    .base_type
310                    .lookup_type_for_child_element(name, tr)
311            }
312            Type::Builtin(builtin) => {
313                if let Some(child_type) = builtin.additional_accepted_child_types.get(name) {
314                    return Ok(child_type.clone());
315                }
316                if builtin.disallow_global_types_as_child_elements {
317                    let mut valid_children: Vec<_> =
318                        builtin.additional_accepted_child_types.keys().cloned().collect();
319                    valid_children.sort();
320
321                    return Err(format!(
322                        "{} is not allowed within {}. Only {} are valid children",
323                        name,
324                        builtin.native_class.class_name,
325                        valid_children.join(" ")
326                    ));
327                }
328            }
329            _ => {}
330        };
331        tr.lookup_element(name).and_then(|t| {
332            if !tr.expose_internal_types && matches!(&t, Type::Builtin(e) if e.is_internal) {
333                Err(format!("Unknown type {}. (The type exist as an internal type, but cannot be accessed in this scope)", name))
334            } else {
335                Ok(t)
336            }
337        })
338    }
339
340    pub fn lookup_member_function(&self, name: &str) -> Expression {
341        match self {
342            Type::Builtin(builtin) => builtin
343                .member_functions
344                .get(name)
345                .cloned()
346                .unwrap_or_else(|| crate::typeregister::reserved_member_function(name)),
347            Type::Component(component) => {
348                component.root_element.borrow().base_type.lookup_member_function(name)
349            }
350            _ => Expression::Invalid,
351        }
352    }
353
354    /// Assume this is a builtin type, panic if it isn't
355    pub fn as_builtin(&self) -> &BuiltinElement {
356        match self {
357            Type::Builtin(b) => b,
358            Type::Component(_) => panic!("This should not happen because of inlining"),
359            _ => panic!("invalid type"),
360        }
361    }
362
363    /// Assume this is a builtin type, panic if it isn't
364    pub fn as_native(&self) -> &NativeClass {
365        match self {
366            Type::Native(b) => b,
367            Type::Component(_) => {
368                panic!("This should not happen because of native class resolution")
369            }
370            _ => panic!("invalid type"),
371        }
372    }
373
374    /// Assume it is a Component, panic if it isn't
375    pub fn as_component(&self) -> &Rc<Component> {
376        match self {
377            Type::Component(c) => c,
378            _ => panic!("should be a component because of the repeater_component pass"),
379        }
380    }
381
382    /// Assume it is an enumeration, panic if it isn't
383    pub fn as_enum(&self) -> &Rc<Enumeration> {
384        match self {
385            Type::Enumeration(e) => e,
386            _ => panic!("should be an enumeration, bug in compiler pass"),
387        }
388    }
389
390    /// Return true if the type can be converted to the other type
391    pub fn can_convert(&self, other: &Self) -> bool {
392        let can_convert_struct = |a: &BTreeMap<String, Type>, b: &BTreeMap<String, Type>| {
393            // the struct `b` has property that the struct `a` doesn't
394            let mut has_more_property = false;
395            for (k, v) in b {
396                match a.get(k) {
397                    Some(t) if !t.can_convert(v) => return false,
398                    None => has_more_property = true,
399                    _ => (),
400                }
401            }
402            if has_more_property {
403                // we should reject the conversion if `a` has property that `b` doesn't have
404                if a.keys().any(|k| !b.contains_key(k)) {
405                    return false;
406                }
407            }
408            true
409        };
410        match (self, other) {
411            (a, b) if a == b => true,
412            (_, Type::Invalid)
413            | (_, Type::Void)
414            | (Type::Float32, Type::Int32)
415            | (Type::Float32, Type::String)
416            | (Type::Int32, Type::Float32)
417            | (Type::Int32, Type::String)
418            | (Type::Array(_), Type::Model)
419            | (Type::Float32, Type::Model)
420            | (Type::Int32, Type::Model)
421            | (Type::PhysicalLength, Type::LogicalLength)
422            | (Type::LogicalLength, Type::PhysicalLength)
423            | (Type::Percent, Type::Float32)
424            | (Type::Brush, Type::Color)
425            | (Type::Color, Type::Brush) => true,
426            (Type::Struct { fields: a, .. }, Type::Struct { fields: b, .. }) => {
427                can_convert_struct(a, b)
428            }
429            (Type::UnitProduct(u), o) => match o.as_unit_product() {
430                Some(o) => unit_product_length_conversion(u.as_slice(), o.as_slice()).is_some(),
431                None => false,
432            },
433            (o, Type::UnitProduct(u)) => match o.as_unit_product() {
434                Some(o) => unit_product_length_conversion(u.as_slice(), o.as_slice()).is_some(),
435                None => false,
436            },
437            _ => false,
438        }
439    }
440
441    pub fn collect_contextual_types(
442        &self,
443        context_restricted_types: &mut HashMap<String, HashSet<String>>,
444    ) {
445        let builtin = match self {
446            Type::Builtin(ty) => ty,
447            _ => return,
448        };
449        for (accepted_child_type_name, accepted_child_type) in
450            builtin.additional_accepted_child_types.iter()
451        {
452            context_restricted_types
453                .entry(accepted_child_type_name.clone())
454                .or_default()
455                .insert(builtin.native_class.class_name.clone());
456
457            accepted_child_type.collect_contextual_types(context_restricted_types);
458        }
459    }
460
461    /// If this is a number type which should be used with an unit, this returns the default unit
462    /// otherwise, returns None
463    pub fn default_unit(&self) -> Option<Unit> {
464        match self {
465            Type::Duration => Some(Unit::Ms),
466            Type::PhysicalLength => Some(Unit::Phx),
467            Type::LogicalLength => Some(Unit::Px),
468            // Unit::Percent is special that it does not combine with other units like
469            Type::Percent => None,
470            Type::Angle => Some(Unit::Deg),
471            Type::Invalid => None,
472            Type::Void => None,
473            Type::InferredProperty | Type::InferredCallback => None,
474            Type::Component(_) => None,
475            Type::Builtin(_) => None,
476            Type::Native(_) => None,
477            Type::Callback { .. } => None,
478            Type::Function { .. } => None,
479            Type::Float32 => None,
480            Type::Int32 => None,
481            Type::String => None,
482            Type::Color => None,
483            Type::Image => None,
484            Type::Bool => None,
485            Type::Model => None,
486            Type::PathData => None,
487            Type::Easing => None,
488            Type::Brush => None,
489            Type::Array(_) => None,
490            Type::Struct { .. } => None,
491            Type::Enumeration(_) => None,
492            Type::UnitProduct(_) => None,
493            Type::ElementReference => None,
494            Type::LayoutCache => None,
495        }
496    }
497
498    /// Return a unit product vector even for single scalar
499    pub fn as_unit_product(&self) -> Option<Vec<(Unit, i8)>> {
500        match self {
501            Type::UnitProduct(u) => Some(u.clone()),
502            Type::Float32 | Type::Int32 => Some(Vec::new()),
503            _ => self.default_unit().map(|u| vec![(u, 1)]),
504        }
505    }
506}
507
508impl Default for Type {
509    fn default() -> Self {
510        Self::Invalid
511    }
512}
513
514/// Information about properties in NativeClass
515#[derive(Debug, Clone)]
516pub struct BuiltinPropertyInfo {
517    /// The property type
518    pub ty: Type,
519    /// When set, this is the initial value that we will have to set if no other binding were specified
520    pub default_value: Option<Expression>,
521    /// Most properties are just set from the .60 code and never modified by the native code.
522    /// But some properties, such as `TouchArea::pressed` are being set by the native code, these
523    /// are output properties which are meant to be read by the .60.
524    /// `is_native_output` is true if the native item can modify the property.
525    pub is_native_output: bool,
526}
527
528impl BuiltinPropertyInfo {
529    pub fn new(ty: Type) -> Self {
530        Self { ty, default_value: None, is_native_output: false }
531    }
532}
533
534#[derive(Debug, Clone, Default)]
535pub struct NativeClass {
536    pub parent: Option<Rc<NativeClass>>,
537    pub class_name: String,
538    pub cpp_vtable_getter: String,
539    pub properties: HashMap<String, BuiltinPropertyInfo>,
540    pub deprecated_aliases: HashMap<String, String>,
541    pub cpp_type: Option<String>,
542    pub rust_type_constructor: Option<String>,
543}
544
545impl NativeClass {
546    pub fn new(class_name: &str) -> Self {
547        let cpp_vtable_getter = format!("SIXTYFPS_GET_ITEM_VTABLE({}VTable)", class_name);
548        Self {
549            class_name: class_name.into(),
550            cpp_vtable_getter,
551            properties: Default::default(),
552            ..Default::default()
553        }
554    }
555
556    pub fn new_with_properties(
557        class_name: &str,
558        properties: impl IntoIterator<Item = (String, BuiltinPropertyInfo)>,
559    ) -> Self {
560        let mut class = Self::new(class_name);
561        class.properties = properties.into_iter().collect();
562        class
563    }
564
565    pub fn property_count(&self) -> usize {
566        self.properties.len() + self.parent.clone().map(|p| p.property_count()).unwrap_or_default()
567    }
568
569    pub fn lookup_property(&self, name: &str) -> Option<&Type> {
570        if let Some(bty) = self.properties.get(name) {
571            Some(&bty.ty)
572        } else if let Some(parent_class) = &self.parent {
573            parent_class.lookup_property(name)
574        } else {
575            None
576        }
577    }
578
579    pub fn lookup_alias(&self, name: &str) -> Option<&str> {
580        if let Some(alias_target) = self.deprecated_aliases.get(name) {
581            Some(alias_target)
582        } else if self.properties.contains_key(name) {
583            None
584        } else if let Some(parent_class) = &self.parent {
585            parent_class.lookup_alias(name)
586        } else {
587            None
588        }
589    }
590}
591
592#[derive(Debug, Clone, Copy)]
593pub enum DefaultSizeBinding {
594    /// There should not be a default binding for the size
595    None,
596    /// The size should default to `width:100%; height:100%`
597    ExpandsToParentGeometry,
598    /// The size should default to the item's implicit size
599    ImplicitSize,
600}
601
602impl Default for DefaultSizeBinding {
603    fn default() -> Self {
604        Self::None
605    }
606}
607
608#[derive(Debug, Clone, Default)]
609pub struct BuiltinElement {
610    pub name: String,
611    pub native_class: Rc<NativeClass>,
612    pub properties: HashMap<String, BuiltinPropertyInfo>,
613    pub additional_accepted_child_types: HashMap<String, Type>,
614    pub disallow_global_types_as_child_elements: bool,
615    /// Non-item type do not have reserved properties (x/width/rowspan/...) added to them  (eg: PropertyAnimation)
616    pub is_non_item_type: bool,
617    pub accepts_focus: bool,
618    pub member_functions: HashMap<String, Expression>,
619    pub is_global: bool,
620    pub default_size_binding: DefaultSizeBinding,
621    /// When true this is an internal type not shown in the auto-completion
622    pub is_internal: bool,
623}
624
625impl BuiltinElement {
626    pub fn new(native_class: Rc<NativeClass>) -> Self {
627        Self { name: native_class.class_name.clone(), native_class, ..Default::default() }
628    }
629}
630
631#[derive(PartialEq, Debug)]
632pub struct PropertyLookupResult<'a> {
633    pub resolved_name: std::borrow::Cow<'a, str>,
634    pub property_type: Type,
635}
636
637impl<'a> PropertyLookupResult<'a> {
638    pub fn is_valid(&self) -> bool {
639        self.property_type != Type::Invalid
640    }
641}
642
643#[derive(Debug, Clone)]
644pub struct Enumeration {
645    pub name: String,
646    pub values: Vec<String>,
647    pub default_value: usize, // index in values
648}
649
650impl PartialEq for Enumeration {
651    fn eq(&self, other: &Self) -> bool {
652        self.name.eq(&other.name)
653    }
654}
655
656impl Enumeration {
657    pub fn default_value(self: Rc<Self>) -> EnumerationValue {
658        EnumerationValue { value: self.default_value, enumeration: self.clone() }
659    }
660
661    pub fn try_value_from_string(self: Rc<Self>, value: &str) -> Option<EnumerationValue> {
662        self.values.iter().enumerate().find_map(|(idx, name)| {
663            if name == value {
664                Some(EnumerationValue { value: idx, enumeration: self.clone() })
665            } else {
666                None
667            }
668        })
669    }
670}
671
672#[derive(Clone, Debug)]
673pub struct EnumerationValue {
674    pub value: usize, // index in enumeration.values
675    pub enumeration: Rc<Enumeration>,
676}
677
678impl PartialEq for EnumerationValue {
679    fn eq(&self, other: &Self) -> bool {
680        Rc::ptr_eq(&self.enumeration, &other.enumeration) && self.value == other.value
681    }
682}
683
684impl std::fmt::Display for EnumerationValue {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        self.enumeration.values[self.value].fmt(f)
687    }
688}
689
690/// If the `Type::UnitProduct(a)` can be converted to `Type::UnitProduct(a)` by multiplying
691/// by the scale factor, return that scale factor, otherwise, return None
692pub fn unit_product_length_conversion(a: &[(Unit, i8)], b: &[(Unit, i8)]) -> Option<i8> {
693    let mut it1 = a.iter();
694    let mut it2 = b.iter();
695    let (mut v1, mut v2) = (it1.next(), it2.next());
696    let mut ppx = 0;
697    let mut lpx = 0;
698    loop {
699        match (v1, v2) {
700            (None, None) => return (ppx == -lpx && ppx != 0).then(|| ppx),
701            (Some(a), Some(b)) if a == b => (),
702            (Some((Unit::Phx, a)), Some((Unit::Phx, b))) => ppx += a - b,
703            (Some((Unit::Px, a)), Some((Unit::Px, b))) => lpx += a - b,
704            (Some((Unit::Phx, a)), _) => {
705                ppx += *a;
706                v1 = it1.next();
707                continue;
708            }
709            (_, Some((Unit::Phx, b))) => {
710                ppx += -b;
711                v2 = it2.next();
712                continue;
713            }
714            (Some((Unit::Px, a)), _) => {
715                lpx += *a;
716                v1 = it1.next();
717                continue;
718            }
719            (_, Some((Unit::Px, b))) => {
720                lpx += -b;
721                v2 = it2.next();
722                continue;
723            }
724            _ => return None,
725        };
726        v1 = it1.next();
727        v2 = it2.next();
728    }
729}
730
731#[test]
732fn unit_product_length_conversion_test() {
733    use Option::None;
734    use Unit::*;
735    assert_eq!(unit_product_length_conversion(&[(Px, 1)], &[(Phx, 1)]), Some(-1));
736    assert_eq!(unit_product_length_conversion(&[(Phx, -2)], &[(Px, -2)]), Some(-2));
737    assert_eq!(unit_product_length_conversion(&[(Px, 1), (Phx, -2)], &[(Phx, -1)]), Some(-1));
738    assert_eq!(
739        unit_product_length_conversion(
740            &[(Deg, 3), (Phx, 2), (Ms, -1)],
741            &[(Phx, 4), (Deg, 3), (Ms, -1), (Px, -2)]
742        ),
743        Some(-2)
744    );
745    assert_eq!(unit_product_length_conversion(&[(Px, 1)], &[(Phx, -1)]), None);
746    assert_eq!(unit_product_length_conversion(&[(Deg, 1), (Phx, -2)], &[(Px, -2)]), None);
747    assert_eq!(unit_product_length_conversion(&[(Px, 1)], &[(Phx, -1)]), None);
748}