Skip to main content

submilli_engine/
shape.rs

1//! Concrete value shapes that need a Wasm subtype emission.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use crate::{ObjectField, Type};
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10pub enum Shape {
11    Object {
12        fields: BTreeMap<String, ObjectField>,
13        index: Option<crate::IndexSignature>,
14    },
15    Array(Box<Type>),
16    /// Lowers to `$Array`; no distinct Wasm type is emitted.
17    Tuple(crate::types::TupleType),
18    Union(Vec<Type>),
19}
20
21impl Shape {
22    pub fn from_type(ty: &Type) -> Option<Shape> {
23        match ty {
24            Type::Object { fields, index } => Some(Shape::Object {
25                index: index.clone(),
26                fields: fields.clone(),
27            }),
28            Type::Array(elem) => Some(Shape::Array(elem.clone())),
29            Type::Tuple(elements) => Some(Shape::Tuple(elements.clone())),
30            Type::Union(members) => Some(Shape::Union(members.clone())),
31            _ => None,
32        }
33    }
34
35    /// Deduplication key — matches `Type`'s display so identical shapes across modules compare equal.
36    pub fn canonical_display(&self) -> String {
37        self.to_string()
38    }
39}
40
41impl fmt::Display for Shape {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Shape::Object { fields, index } => Type::Object {
45                fields: fields.clone(),
46                index: index.clone(),
47            }
48            .fmt(f),
49            // Mirrors `Type`'s array arm — `canonical_display` is a dedup key
50            // compared against it.
51            Shape::Array(elem) => match &**elem {
52                Type::Union(_) | Type::Function { .. } => write!(f, "({elem})[]"),
53                _ => write!(f, "{elem}[]"),
54            },
55            Shape::Tuple(elements) => Type::Tuple(elements.clone()).fmt(f),
56            Shape::Union(members) => {
57                for (i, m) in members.iter().enumerate() {
58                    if i > 0 {
59                        f.write_str(" | ")?;
60                    }
61                    match m {
62                        Type::Function { .. } => write!(f, "({m})")?,
63                        _ => write!(f, "{m}")?,
64                    }
65                }
66                Ok(())
67            }
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use std::collections::BTreeSet;
76
77    #[test]
78    fn object_shape_round_trip() {
79        let mut fields = BTreeMap::new();
80        fields.insert("x".to_string(), ObjectField::required(Type::Number));
81        fields.insert("y".to_string(), ObjectField::required(Type::Number));
82        let ty = Type::Object {
83            index: None,
84            fields: fields.clone(),
85        };
86        let shape = Shape::from_type(&ty).expect("Object is a shape");
87        assert_eq!(
88            shape,
89            Shape::Object {
90                index: None,
91                fields
92            }
93        );
94        assert_eq!(shape.canonical_display(), "{ x: number; y: number }");
95    }
96
97    #[test]
98    fn object_shape_preserves_optional_flag() {
99        let mut fields = BTreeMap::new();
100        fields.insert("x".to_string(), ObjectField::required(Type::Number));
101        fields.insert("y".to_string(), ObjectField::optional(Type::String));
102        let ty = Type::Object {
103            index: None,
104            fields: fields.clone(),
105        };
106        let shape = Shape::from_type(&ty).expect("Object is a shape");
107        assert_eq!(
108            shape,
109            Shape::Object {
110                index: None,
111                fields
112            }
113        );
114        assert_eq!(shape.canonical_display(), "{ x: number; y?: string }");
115    }
116
117    #[test]
118    fn array_shape_round_trip() {
119        let ty = Type::Array(Box::new(Type::String));
120        let shape = Shape::from_type(&ty).expect("Array is a shape");
121        assert_eq!(shape, Shape::Array(Box::new(Type::String)));
122        assert_eq!(shape.canonical_display(), "string[]");
123    }
124
125    #[test]
126    fn tuple_shape_round_trip() {
127        let ty = Type::Tuple(vec![Type::String, Type::Number].into());
128        let shape = Shape::from_type(&ty).expect("Tuple is a shape");
129        assert_eq!(shape, Shape::Tuple(vec![Type::String, Type::Number].into()));
130        assert_eq!(shape.canonical_display(), "[string, number]");
131    }
132
133    #[test]
134    fn union_shape_round_trip() {
135        let ty = Type::union(vec![Type::Number, Type::String]);
136        let shape = Shape::from_type(&ty).expect("Union is a shape");
137        assert!(matches!(shape, Shape::Union(_)));
138        // Canonical sort puts Boolean < Null < Number < String per `Ord`
139        // derive on Type (variant order). Just check display includes
140        // both members.
141        let display = shape.canonical_display();
142        assert!(display.contains("number"));
143        assert!(display.contains("string"));
144        assert!(display.contains(" | "));
145    }
146
147    #[test]
148    fn non_shape_types_return_none() {
149        assert!(Shape::from_type(&Type::Number).is_none());
150        assert!(Shape::from_type(&Type::String).is_none());
151        assert!(Shape::from_type(&Type::Boolean).is_none());
152        assert!(Shape::from_type(&Type::Null).is_none());
153        assert!(Shape::from_type(&Type::Void).is_none());
154        assert!(Shape::from_type(&Type::Error).is_none());
155        assert!(Shape::from_type(&Type::TypeVar("T".to_string())).is_none());
156        assert!(
157            Shape::from_type(&Type::GenericParam {
158                id: 0,
159                name: "T".to_string()
160            })
161            .is_none()
162        );
163        assert!(
164            Shape::from_type(&Type::InterfaceRef {
165                mangled: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, "Foo"),
166                package: crate::Package::user(),
167                name: "Foo".to_string(),
168                args: Vec::new(),
169            })
170            .is_none()
171        );
172        assert!(
173            Shape::from_type(&Type::NumberEnum {
174                mangled: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, "D"),
175                package: crate::Package::user(),
176                name: "D".to_string(),
177                member: None,
178            })
179            .is_none()
180        );
181        assert!(
182            Shape::from_type(&Type::StringEnum {
183                mangled: crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, "C"),
184                package: crate::Package::user(),
185                name: "C".to_string(),
186                member: None,
187            })
188            .is_none()
189        );
190        assert!(
191            Shape::from_type(&Type::Function {
192                params: vec![Type::Number],
193                ret: Box::new(Type::Number),
194                predicate: None,
195                has_rest: false,
196                optional: 0,
197            })
198            .is_none()
199        );
200    }
201
202    #[test]
203    fn btreeset_dedup_by_structural_equality() {
204        let mut set: BTreeSet<Shape> = BTreeSet::new();
205        let mut fields_a = BTreeMap::new();
206        fields_a.insert("x".to_string(), ObjectField::required(Type::Number));
207        let mut fields_b = BTreeMap::new();
208        fields_b.insert("x".to_string(), ObjectField::required(Type::Number));
209        set.insert(Shape::Object {
210            index: None,
211            fields: fields_a,
212        });
213        set.insert(Shape::Object {
214            index: None,
215            fields: fields_b,
216        });
217        set.insert(Shape::Array(Box::new(Type::Number)));
218        assert_eq!(set.len(), 2);
219    }
220
221    #[test]
222    fn canonical_display_matches_type_display_for_equivalent_shape() {
223        let mut fields = BTreeMap::new();
224        fields.insert("x".to_string(), ObjectField::required(Type::Number));
225        let ty = Type::Object {
226            index: None,
227            fields: fields.clone(),
228        };
229        let shape = Shape::Object {
230            index: None,
231            fields,
232        };
233        assert_eq!(shape.canonical_display(), ty.to_string());
234
235        let ty = Type::Array(Box::new(Type::String));
236        let shape = Shape::Array(Box::new(Type::String));
237        assert_eq!(shape.canonical_display(), ty.to_string());
238    }
239}