Skip to main content

submilli_engine/
types.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use crate::mangle::MangledName;
5use serde::{Deserialize, Serialize};
6
7/// One member of an enum, as the member literal type `E.A` names it.
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9pub struct EnumMember<V> {
10    pub name: String,
11    pub value: V,
12    /// How many members the enum has, so a union naming them all folds into
13    /// the enum, as TypeScript reduces `E.A | E.B` to `E`.
14    pub member_count: usize,
15}
16
17impl<V> EnumMember<V> {
18    pub fn new(name: &str, value: V, member_count: usize) -> Self {
19        EnumMember {
20            name: name.to_string(),
21            value,
22            member_count,
23        }
24    }
25}
26
27/// The value an enum member holds.
28#[derive(Clone, Debug, PartialEq)]
29pub enum EnumValue {
30    Number(LiteralF64),
31    String(String),
32}
33
34impl EnumValue {
35    /// The plain literal type of this value: `1` or `"a"`.
36    pub fn literal_type(&self) -> Type {
37        match self {
38            EnumValue::Number(value) => Type::NumberLiteral(*value),
39            EnumValue::String(value) => Type::StringLiteral(value.clone()),
40        }
41    }
42}
43
44/// `f64` wrapper for total ordering and bit-pattern equality — bare `f64` lacks `Eq`/`Ord`,
45/// which would break the derived impls on [`Type`]. Construction must canonicalize `-0.0 → 0.0`;
46/// NaN can't appear from source literals so bit-pattern equality is safe.
47#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
48pub struct LiteralF64(#[serde(with = "crate::artifact_f64")] pub f64);
49
50impl PartialEq for LiteralF64 {
51    fn eq(&self, other: &Self) -> bool {
52        self.0.to_bits() == other.0.to_bits()
53    }
54}
55
56impl Eq for LiteralF64 {}
57
58impl PartialOrd for LiteralF64 {
59    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
60        Some(self.cmp(other))
61    }
62}
63
64impl Ord for LiteralF64 {
65    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
66        self.0.total_cmp(&other.0)
67    }
68}
69
70impl std::hash::Hash for LiteralF64 {
71    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
72        self.0.to_bits().hash(state);
73    }
74}
75
76/// Owning package of a by-name [`Type`] reference (`InterfaceRef`, `AliasRef`, `Alias`,
77/// `NumberEnum`, `StringEnum`). Carried so *structural* resolution of an already-typed
78/// value can find the symbol by FQN in a global registry, independent of what the current
79/// module imported.
80///
81/// **Excluded from identity:** `Eq`/`Ord`/`Hash` treat every `Package` as equal, so the
82/// owning package never drives a by-name type's identity. Identity is the sibling
83/// `mangled` field (the declaring symbol's mangled name, which already encodes the
84/// package and module); `package` only keeps *structural* registry lookup robust to
85/// which construction site stamped it.
86#[derive(Clone, Debug, Serialize, Deserialize)]
87pub struct Package(pub String);
88
89impl Package {
90    pub fn prelude() -> Self {
91        Self(crate::mangle::PRELUDE_PACKAGE.to_string())
92    }
93
94    pub fn user() -> Self {
95        Self(crate::mangle::USER_PACKAGE.to_string())
96    }
97
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101}
102
103impl PartialEq for Package {
104    fn eq(&self, _: &Self) -> bool {
105        true
106    }
107}
108
109impl Eq for Package {}
110
111impl PartialOrd for Package {
112    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
113        Some(self.cmp(other))
114    }
115}
116
117impl Ord for Package {
118    fn cmp(&self, _: &Self) -> std::cmp::Ordering {
119        std::cmp::Ordering::Equal
120    }
121}
122
123impl std::hash::Hash for Package {
124    fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
125}
126
127/// `optional: true` means reads widen to `ty | undefined` and construction may omit the field.
128/// Distinct from a value-nullable field (`ty: T | null, optional: false`): must be present
129/// but can be null.
130#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
131pub struct ObjectField {
132    pub ty: Type,
133    pub optional: bool,
134    /// `readonly` forbids writes through the field; assignability treats a writable
135    /// target field invariantly (see `assignable.rs`). Inferred object literals and
136    /// freshly-synthesized shapes are writable (`false`); only an explicit `readonly`
137    /// modifier on an object-type/interface property sets this.
138    pub readonly: bool,
139    /// A method rather than a function-typed property. `tsc` compares a method's
140    /// parameters bivariantly and a property's contravariantly.
141    #[serde(default)]
142    pub method: bool,
143}
144
145impl ObjectField {
146    pub fn required(ty: Type) -> Self {
147        Self {
148            ty,
149            optional: false,
150            readonly: false,
151            method: false,
152        }
153    }
154    pub fn optional(ty: Type) -> Self {
155        Self {
156            ty,
157            optional: true,
158            readonly: false,
159            method: false,
160        }
161    }
162
163    /// The type a *read* of this field yields: an optional field widens to
164    /// `T | undefined`, since an absent property reads as undefined. Every field
165    /// read — object type, interface property, class field, union member, and
166    /// codegen's mirror of all four — goes through this, so the widening rule
167    /// has one definition.
168    pub fn read_ty(&self) -> Type {
169        Self::widen_optional(self.optional, self.ty.clone())
170    }
171
172    /// [`read_ty`](Self::read_ty) for callers holding the two facts separately
173    /// (an interface `PropertySig`, a class `FieldSig`).
174    pub fn widen_optional(optional: bool, ty: Type) -> Type {
175        if optional {
176            Type::union(vec![ty, Type::Undefined])
177        } else {
178            ty
179        }
180    }
181}
182
183/// Values available under arbitrary string property names.
184#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
185pub struct IndexSignature {
186    pub value: Box<Type>,
187    pub readonly: bool,
188}
189
190impl IndexSignature {
191    pub fn map_value(&self, transform: impl FnOnce(&Type) -> Type) -> Self {
192        Self {
193            value: Box::new(transform(&self.value)),
194            readonly: self.readonly,
195        }
196    }
197
198    /// [`map_value`](Self::map_value) with a transform that can fail.
199    pub fn try_map_value<E>(
200        &self,
201        transform: impl FnOnce(&Type) -> Result<Type, E>,
202    ) -> Result<Self, E> {
203        Ok(Self {
204            value: Box::new(transform(&self.value)?),
205            readonly: self.readonly,
206        })
207    }
208
209    pub fn read_ty(&self) -> Type {
210        Type::union(vec![(*self.value).clone(), Type::Undefined])
211    }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
215pub struct TypePredicate {
216    pub parameter_index: u32,
217    pub asserted_type: Type,
218}
219
220/// Tuple positions and the number of trailing positions that may be omitted.
221/// Optional positions include `Undefined` in their element type.
222#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
223pub struct TupleType {
224    pub elements: Vec<Type>,
225    pub optional: usize,
226}
227
228impl TupleType {
229    pub fn required_len(&self) -> usize {
230        self.elements.len().saturating_sub(self.optional)
231    }
232
233    pub fn map(&self, transform: impl FnMut(&Type) -> Type) -> Self {
234        Self {
235            elements: self.elements.iter().map(transform).collect(),
236            optional: self.optional,
237        }
238    }
239
240    pub fn try_map<E>(&self, transform: impl FnMut(&Type) -> Result<Type, E>) -> Result<Self, E> {
241        Ok(Self {
242            elements: self
243                .elements
244                .iter()
245                .map(transform)
246                .collect::<Result<_, _>>()?,
247            optional: self.optional,
248        })
249    }
250}
251
252impl From<Vec<Type>> for TupleType {
253    fn from(elements: Vec<Type>) -> Self {
254        Self {
255            elements,
256            optional: 0,
257        }
258    }
259}
260
261impl std::ops::Deref for TupleType {
262    type Target = [Type];
263    fn deref(&self) -> &Self::Target {
264        &self.elements
265    }
266}
267
268impl IntoIterator for TupleType {
269    type Item = Type;
270    type IntoIter = std::vec::IntoIter<Type>;
271    fn into_iter(self) -> Self::IntoIter {
272        self.elements.into_iter()
273    }
274}
275
276impl<'a> IntoIterator for &'a TupleType {
277    type Item = &'a Type;
278    type IntoIter = std::slice::Iter<'a, Type>;
279    fn into_iter(self) -> Self::IntoIter {
280        self.elements.iter()
281    }
282}
283
284#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
285pub enum Type {
286    Number,
287    /// No distinct runtime representation — codegen widens to `f64` at every emission site.
288    NumberLiteral(LiteralF64),
289    BigInt,
290    /// A bigint literal type: `123n`, `-1n`. Holds the value in decimal, with a
291    /// leading `-` when negative, so each value has one spelling. Like the other
292    /// literal types it lowers exactly as its base, `bigint`.
293    BigIntLiteral(String),
294    String,
295    StringLiteral(String),
296    Uint8Array,
297    Boolean,
298    /// `true` or `false`. Like the other literal types it has no runtime
299    /// representation of its own: it lowers exactly as `boolean`. `Type::union`
300    /// folds `true | false` into `boolean`, which is what `boolean` means.
301    BooleanLiteral(bool),
302    Null,
303    /// The single undefined value, distinct from language null and Wasm null.
304    Undefined,
305    Void,
306    /// Unlike TypeScript's `any`, requires explicit narrowing before use.
307    /// `Unknown | T` collapses to `Unknown`.
308    Unknown,
309    Function {
310        params: Vec<Type>,
311        /// Number of trailing fixed parameters callers may omit.
312        optional: usize,
313        ret: Box<Type>,
314        /// Boxed to break the `Type → Function → TypePredicate → Type` cycle. Guards are
315        /// assignable to plain functions but not vice versa.
316        predicate: Option<Box<TypePredicate>>,
317        /// When true, the last `params` entry holds the rest array type.
318        has_rest: bool,
319    },
320    /// `BTreeMap` gives structural `==`, deterministic iteration, and canonical field order for codegen.
321    Object {
322        fields: BTreeMap<String, ObjectField>,
323        index: Option<IndexSignature>,
324    },
325    Array(Box<Type>),
326    /// Parser rejects empty tuples. Index access requires an integer literal; out-of-range and
327    /// non-literal indices are rejected at typecheck time. Lowers to `(ref $Array)` at runtime.
328    Tuple(TupleType),
329    /// `readonly T[]` (also spelled `ReadonlyArray<T>`) or `readonly [A, B]`. The inner
330    /// type is always a [`Type::Array`] or [`Type::Tuple`]: the wrapper only forbids
331    /// writes, so [`Type::peel`] strips it and every read path sees the plain array.
332    /// Write paths (index assignment, mutating methods) must ask
333    /// [`Type::is_readonly_array`] before peeling. `Eq`/`Ord` keep it distinct from the
334    /// mutable type because a readonly array is not assignable to a mutable one.
335    Readonly(Box<Type>),
336    /// Diagnostic already reported; downstream code must not emit cascading errors.
337    Error,
338    /// No Wasm representation — never values never reach codegen.
339    Never,
340    /// Signature-form generic parameter. Never appears in function bodies — body inference
341    /// replaces each TypeVar with a fresh [`GenericParam`](Type::GenericParam) at body entry.
342    TypeVar(String),
343    /// Body-internal generic placeholder. Identity by `id` — two GPs with different ids are
344    /// distinct even when they share a name, preventing collisions across nested generic scopes.
345    GenericParam {
346        id: u32,
347        name: String,
348    },
349    /// A generic value with a proven runtime shape. Member access and lowering
350    /// use `ty`; assignability also preserves the original generic identity.
351    Refined {
352        original: Box<Type>,
353        ty: Box<Type>,
354    },
355    /// `args` carries already-substituted type arguments; empty for non-generic interfaces.
356    /// `mangled` is the declaring symbol's mangled name — the **nominal identity** (two
357    /// modules' same-named interfaces have distinct mangled names). `package`/`name` are
358    /// kept for diagnostics and import-independent *structural* resolution (see [`Package`]);
359    /// they do not drive identity. `mangled` is listed first so derived `Ord` keys on it.
360    InterfaceRef {
361        mangled: MangledName,
362        package: Package,
363        name: String,
364        args: Vec<Type>,
365    },
366    /// Nominal reference to a `class` declaration. Identity is `mangled` (two modules'
367    /// same-named classes are distinct); `package`/`name` are for diagnostics and
368    /// import-independent structural resolution, mirroring [`Type::InterfaceRef`].
369    /// `args` instantiates the class's type parameters; empty for a
370    /// non-generic class.
371    ClassRef {
372        mangled: MangledName,
373        package: Package,
374        name: String,
375        args: Vec<Type>,
376    },
377    /// Distinct from `StringEnum` so codegen knows the `i32` representation without consulting the type namespace.
378    ///
379    /// `member` narrows the enum to one member's literal type, `E.A`, which is
380    /// what a member read has, as in TypeScript. It shares the enum's runtime
381    /// representation; [`Type::widen_literal`] drops it back to the enum.
382    NumberEnum {
383        mangled: MangledName,
384        package: Package,
385        name: String,
386        member: Option<EnumMember<LiteralF64>>,
387    },
388    StringEnum {
389        mangled: MangledName,
390        package: Package,
391        name: String,
392        member: Option<EnumMember<String>>,
393    },
394    /// Canonical union: sorted, deduplicated, no nested unions, no `Error` members, always ≥2
395    /// members. Build *only* via [`Type::union`].
396    Union(Vec<Type>),
397    /// Every pattern-match site must call [`Type::peel`] first; sole exception is
398    /// [`Display`](fmt::Display). `Eq`/`Ord`/`Hash` are nominal: `Alias { "ID", Number }` ≠
399    /// `Number` at the raw level. `args` carries already-substituted type arguments.
400    Alias {
401        mangled: MangledName,
402        package: Package,
403        name: String,
404        args: Vec<Type>,
405        ty: Box<Type>,
406    },
407    /// Lazy by-name reference to a type alias, used **only at a
408    /// recursion back-edge** — the inner `Json` in
409    /// `type Json = number | string | Json[]`, or the `Node` in
410    /// `type Node = { next: Node | null }`. Unlike [`Type::Alias`] it
411    /// carries no inline body; the body lives in the type namespace and
412    /// is resolved by name on demand (mirroring [`Type::InterfaceRef`]).
413    /// This keeps a recursive alias a *finite* `Type` value — a cyclic
414    /// inline body would infinitely recurse the derived `Eq`/`Ord` and
415    /// [`Type::peel`]. `peel` does **not** expand it (it stops here, the
416    /// same as `InterfaceRef`); the few sites that need the alias's
417    /// structure resolve it by name with cycle-safety.
418    AliasRef {
419        mangled: MangledName,
420        package: Package,
421        name: String,
422        args: Vec<Type>,
423    },
424}
425
426impl Type {
427    pub fn tuple(elements: Vec<Type>) -> Self {
428        Self::Tuple(elements.into())
429    }
430
431    /// A receiver whose computed string keys use the object property carrier.
432    pub fn is_structural_object(&self) -> bool {
433        match self.peel() {
434            Self::Object { .. } | Self::InterfaceRef { .. } => true,
435            Self::Union(members) => members.iter().all(Self::is_structural_object),
436            _ => false,
437        }
438    }
439
440    /// Build an [`Type::InterfaceRef`]. `mangled` is the declaring symbol's mangled
441    /// name (its nominal identity) — pass the symbol's own `mangled_name`, never a
442    /// value recomputed from `package`/`name`, so two construction sites for the same
443    /// type always agree.
444    pub fn interface_ref(
445        package: Package,
446        name: impl Into<String>,
447        mangled: MangledName,
448        args: Vec<Type>,
449    ) -> Type {
450        Type::InterfaceRef {
451            mangled,
452            package,
453            name: name.into(),
454            args,
455        }
456    }
457
458    /// Build a [`Type::ClassRef`]. `mangled` is the class symbol's nominal identity;
459    /// pass the symbol's own `mangled_name`, never one recomputed from `package`/`name`.
460    pub fn class_ref(
461        package: Package,
462        name: impl Into<String>,
463        mangled: MangledName,
464        args: Vec<Type>,
465    ) -> Type {
466        Type::ClassRef {
467            mangled,
468            package,
469            name: name.into(),
470            args,
471        }
472    }
473
474    pub fn number_enum(package: Package, name: impl Into<String>, mangled: MangledName) -> Type {
475        Type::NumberEnum {
476            mangled,
477            package,
478            name: name.into(),
479            member: None,
480        }
481    }
482
483    pub fn string_enum(package: Package, name: impl Into<String>, mangled: MangledName) -> Type {
484        Type::StringEnum {
485            mangled,
486            package,
487            name: name.into(),
488            member: None,
489        }
490    }
491
492    /// The member literal type `E.A` of the enum type `self`, which has
493    /// `member_count` members. `self` must be a `NumberEnum` holding a number
494    /// or a `StringEnum` holding a string. As in TypeScript, the one member of
495    /// an enum has the enum's own type.
496    pub fn with_enum_member(
497        &self,
498        member_name: &str,
499        value: EnumValue,
500        member_count: usize,
501    ) -> Type {
502        if member_count < 2 {
503            return self.without_enum_member();
504        }
505        match (self.clone(), value) {
506            (
507                Type::NumberEnum {
508                    mangled,
509                    package,
510                    name,
511                    ..
512                },
513                EnumValue::Number(value),
514            ) => Type::NumberEnum {
515                mangled,
516                package,
517                name,
518                member: Some(EnumMember::new(member_name, value, member_count)),
519            },
520            (
521                Type::StringEnum {
522                    mangled,
523                    package,
524                    name,
525                    ..
526                },
527                EnumValue::String(value),
528            ) => Type::StringEnum {
529                mangled,
530                package,
531                name,
532                member: Some(EnumMember::new(member_name, value, member_count)),
533            },
534            (ty, _) => ty,
535        }
536    }
537
538    /// The enum a member literal type `E.A` belongs to; any other type unchanged.
539    pub fn without_enum_member(&self) -> Type {
540        match self {
541            Type::NumberEnum {
542                mangled,
543                package,
544                name,
545                member: Some(_),
546            } => Type::number_enum(package.clone(), name.clone(), mangled.clone()),
547            Type::StringEnum {
548                mangled,
549                package,
550                name,
551                member: Some(_),
552            } => Type::string_enum(package.clone(), name.clone(), mangled.clone()),
553            _ => self.clone(),
554        }
555    }
556
557    /// Whether this is one literal type, what TypeScript calls a unit type:
558    /// `1`, `"a"`, `true`, `1n` or an enum member `E.A`.
559    pub fn is_literal_type(&self) -> bool {
560        matches!(
561            self,
562            Type::NumberLiteral(_)
563                | Type::StringLiteral(_)
564                | Type::BooleanLiteral(_)
565                | Type::BigIntLiteral(_)
566        ) || self.is_enum_member()
567    }
568
569    /// Whether this is an enum member literal type `E.A`.
570    pub fn is_enum_member(&self) -> bool {
571        self.enum_member_name().is_some()
572    }
573
574    /// The enum, member name and member count of an enum member literal type `E.A`.
575    pub fn enum_member_name(&self) -> Option<(&MangledName, &str, usize)> {
576        match self {
577            Type::NumberEnum {
578                mangled,
579                member: Some(member),
580                ..
581            } => Some((mangled, &member.name, member.member_count)),
582            Type::StringEnum {
583                mangled,
584                member: Some(member),
585                ..
586            } => Some((mangled, &member.name, member.member_count)),
587            _ => None,
588        }
589    }
590
591    /// The value an enum member literal type `E.A` holds.
592    pub fn enum_member_value(&self) -> Option<EnumValue> {
593        match self.peel() {
594            Type::NumberEnum {
595                member: Some(member),
596                ..
597            } => Some(EnumValue::Number(member.value)),
598            Type::StringEnum {
599                member: Some(member),
600                ..
601            } => Some(EnumValue::String(member.value.clone())),
602            _ => None,
603        }
604    }
605
606    pub fn alias_ref(
607        package: Package,
608        name: impl Into<String>,
609        mangled: MangledName,
610        args: Vec<Type>,
611    ) -> Type {
612        Type::AliasRef {
613            mangled,
614            package,
615            name: name.into(),
616            args,
617        }
618    }
619
620    pub fn alias_ty(
621        package: Package,
622        name: impl Into<String>,
623        mangled: MangledName,
624        args: Vec<Type>,
625        ty: Box<Type>,
626    ) -> Type {
627        Type::Alias {
628            mangled,
629            package,
630            name: name.into(),
631            args,
632            ty,
633        }
634    }
635
636    /// Convenience for a non-namespaced prelude interface (`Error`, `Response`, …):
637    /// the prelude registers these with `mangled_name = prelude(name)`, so this is
638    /// the matching identity. Namespaced prelude types (`Temporal.*`) must instead
639    /// use [`Type::interface_ref`] with `extend(prelude("Temporal"), local)`.
640    pub fn prelude_interface(name: impl Into<String>, args: Vec<Type>) -> Type {
641        let name = name.into();
642        let mangled = crate::mangle::prelude(&name);
643        Type::interface_ref(Package::prelude(), name, mangled, args)
644    }
645
646    /// The built-in `Error` class reference — the type of `throw`/`catch` values
647    /// and the root of user error subclasses.
648    pub fn prelude_error_class() -> Type {
649        Type::class_ref(
650            Package::prelude(),
651            "Error",
652            crate::mangle::prelude("Error"),
653            Vec::new(),
654        )
655    }
656
657    /// The built-in `RangeError` class reference — the host-implemented
658    /// `Error` subclass for out-of-range failures.
659    pub fn prelude_range_error_class() -> Type {
660        Type::class_ref(
661            Package::prelude(),
662            "RangeError",
663            crate::mangle::prelude("RangeError"),
664            Vec::new(),
665        )
666    }
667
668    /// Remove display aliases while retaining generic guard identity.
669    pub fn without_aliases(&self) -> &Type {
670        match self {
671            Type::Alias { ty, .. } => ty.without_aliases(),
672            _ => self,
673        }
674    }
675
676    pub fn peel(&self) -> &Type {
677        let mut t = self;
678        while let Type::Alias { ty, .. } | Type::Refined { ty, .. } | Type::Readonly(ty) = t {
679            t = ty;
680        }
681        t
682    }
683
684    /// [`peel`](Self::peel), but stopping at a [`Type::Readonly`] wrapper, for
685    /// sites that carry a type onward and must not drop its readonly-ness.
686    pub fn peel_preserving_readonly(&self) -> &Type {
687        let mut t = self;
688        while let Type::Alias { ty, .. } | Type::Refined { ty, .. } = t {
689            t = ty;
690        }
691        t
692    }
693
694    /// Whether writes through a value of this type are forbidden because it is a
695    /// `readonly` array or tuple, looking through aliases and refinements.
696    pub fn is_readonly_array(&self) -> bool {
697        matches!(self.peel_preserving_readonly(), Type::Readonly(_))
698    }
699
700    /// The element type of a rest parameter's array: `T` for `T[]` or
701    /// `readonly T[]`. Not peeled through aliases, since rest lowering matches the
702    /// parameter type as written.
703    pub fn rest_element(&self) -> Option<&Type> {
704        match self {
705            Type::Array(element) => Some(element),
706            Type::Readonly(inner) => match inner.as_ref() {
707                Type::Array(element) => Some(element),
708                _ => None,
709            },
710            _ => None,
711        }
712    }
713
714    /// A rest parameter's type with any `readonly` removed. Each call packs a
715    /// fresh array for the rest, so whether the callee may write to it is the
716    /// callee's own concern: two function types relate on their rest elements,
717    /// as in tsc.
718    pub fn rest_array_ignoring_readonly(&self) -> &Type {
719        match self {
720            Type::Readonly(inner) if matches!(inner.as_ref(), Type::Array(_)) => inner,
721            _ => self,
722        }
723    }
724
725    /// Whether this is a union of only arrays and tuples, which share the `$Array`
726    /// representation.
727    pub fn is_array_like_union(&self) -> bool {
728        match self.peel() {
729            Type::Union(members) => members
730                .iter()
731                .all(|member| matches!(member.peel(), Type::Array(_) | Type::Tuple(_))),
732            _ => false,
733        }
734    }
735
736    /// The element of a union of arrays and tuples read as one array: any
737    /// member's element. `None` for any other type. Only reads may go through
738    /// it; writing a member's element through the joined type could store
739    /// another member's element type.
740    pub fn array_like_union_element(&self) -> Option<Type> {
741        let Type::Union(members) = self.peel() else {
742            return None;
743        };
744        let elements = members
745            .iter()
746            .map(|member| match member.peel() {
747                Type::Array(element) => Some((**element).clone()),
748                Type::Tuple(positions) => Some(Type::union(positions.elements.clone())),
749                _ => None,
750            })
751            .collect::<Option<Vec<_>>>()?;
752        Some(Type::union(elements))
753    }
754
755    /// The arrays and tuples of a union of strings with arrays or tuples, and
756    /// nothing else, as their own union. Such a union has no shared
757    /// representation, so each use tests `typeof` and takes the string's or the
758    /// array's path.
759    pub fn string_or_array_union_arrays(&self) -> Option<Type> {
760        let Type::Union(members) = self.peel() else {
761            return None;
762        };
763        let (strings, arrays): (Vec<Type>, Vec<Type>) = members
764            .iter()
765            .cloned()
766            .partition(|member| member.peel().is_string_shaped());
767        let all_arrays = arrays
768            .iter()
769            .all(|member| matches!(member.peel(), Type::Array(_) | Type::Tuple(_)));
770        (!strings.is_empty() && !arrays.is_empty() && all_arrays).then(|| Type::union(arrays))
771    }
772
773    /// The array a union of arrays and tuples reads as: see
774    /// [`Self::array_like_union_element`].
775    pub fn array_like_union_view(&self) -> Option<Type> {
776        self.array_like_union_element()
777            .map(|element| Type::Array(Box::new(element)))
778    }
779
780    /// Whether this type is `void`, through any depth of alias.
781    ///
782    /// `void` is the one type with no value slot at all, so a gate that tests
783    /// for it decides between "one Wasm result" and "none". Every such gate —
784    /// in the typechecker *and* in codegen — must answer the same way for
785    /// `type V = void` as for `void`: a bare `matches!(ty, Type::Void)` hands an
786    /// aliased `void` a value slot that
787    /// [`SymbolTable::value_type`](crate::codegen::symbol_table::SymbolTable::value_type)
788    /// then refuses to lower. A typecheck gate must not start peeling ahead of
789    /// its codegen counterpart, which turns a clean diagnostic into a panic.
790    pub fn is_void(&self) -> bool {
791        matches!(self.peel(), Type::Void)
792    }
793
794    /// Whether a function taking `actual` parameters can stand where one taking
795    /// `expected` is called. As in TypeScript, it may declare fewer and ignore
796    /// the rest of the arguments; a closure adapter drops them at runtime. Rest
797    /// functions keep an exact arity, since their packed array has a slot of its
798    /// own.
799    pub fn function_arity_fits(actual: usize, expected: usize, has_rest: bool) -> bool {
800        actual == expected || (!has_rest && actual < expected)
801    }
802
803    /// Whether a value of this type would need a `void` slot at runtime:
804    /// `void` itself, or a union that lists it.
805    ///
806    /// Deliberately one level deep — it does **not** descend into a function's
807    /// return type, where `void` is legitimate (`() => void`). Every gate that
808    /// refuses `void` in a value or comparison position wants this, not the
809    /// bare [`is_void`](Self::is_void). `cond ? f() : 1` and `a ?? f()` are
810    /// plain `void`, so no expression builds the union form; a gate that asks
811    /// this still refuses one that did, rather than letting it reach codegen.
812    pub fn carries_void(&self) -> bool {
813        match self.peel() {
814            Type::Void => true,
815            Type::Union(members) => members.iter().any(|m| matches!(m.peel(), Type::Void)),
816            _ => false,
817        }
818    }
819
820    /// Primitive operations shared by enums and homogeneous literal unions,
821    /// without widening their type identity or changing assignability.
822    pub fn primitive_behavior(&self) -> &Type {
823        match self.peel() {
824            Type::NumberEnum { .. } => &Type::Number,
825            Type::StringEnum { .. } => &Type::String,
826            Type::BooleanLiteral(_) => &Type::Boolean,
827            Type::BigIntLiteral(_) => &Type::BigInt,
828            Type::Union(members)
829                if !members.is_empty()
830                    && members.iter().all(|member| {
831                        matches!(
832                            member.primitive_behavior(),
833                            Type::Number | Type::NumberLiteral(_)
834                        )
835                    }) =>
836            {
837                &Type::Number
838            }
839            Type::Union(members)
840                if !members.is_empty()
841                    && members
842                        .iter()
843                        .all(|member| matches!(member.primitive_behavior(), Type::BigInt)) =>
844            {
845                &Type::BigInt
846            }
847            Type::Union(members)
848                if !members.is_empty() && members.iter().all(Type::is_string_shaped) =>
849            {
850                &Type::String
851            }
852            ty => ty,
853        }
854    }
855
856    /// Whether every value of this type is a bigint: `bigint`, a bigint literal
857    /// type, or a union or alias of those.
858    pub fn is_bigint(&self) -> bool {
859        matches!(self.primitive_behavior(), Type::BigInt)
860    }
861
862    /// This type with literal types replaced by the primitive they are a literal of.
863    ///
864    /// A literal type is only sound where the value cannot change, so inference keeps
865    /// it at a `const` binding and widens here at every position that is mutable or
866    /// whose type is inferred from its contents — a `let` binding, an array element,
867    /// an object-literal property, a generic argument. `const a = 1` is `1`, but
868    /// `let b = a` is `number`, matching TypeScript.
869    ///
870    /// An enum member literal type `E.A` widens to its enum `E`, not to the
871    /// enum's primitive: the enum is a nominal type whose identity its members
872    /// are checked against. Use [`primitive_behavior`](Self::primitive_behavior)
873    /// for the primitive.
874    pub fn widen_literal(&self) -> Type {
875        match self {
876            Type::NumberLiteral(_) => Type::Number,
877            Type::StringLiteral(_) => Type::String,
878            Type::BooleanLiteral(_) => Type::Boolean,
879            Type::BigIntLiteral(_) => Type::BigInt,
880            _ if self.is_enum_member() => self.without_enum_member(),
881            // A union widens memberwise, which also collapses it when the members
882            // share a base: `1 | 2` is `number`, not `number | number`, because
883            // `Type::union` deduplicates.
884            Type::Union(members) => Type::union(members.iter().map(Type::widen_literal).collect()),
885            _ => self.clone(),
886        }
887    }
888
889    /// The primitive this literal type, or union of literals of one
890    /// primitive, is a literal of: `number` for `1 | 2`, `None` for
891    /// `1 | "a"` or a type that is no literal.
892    pub fn literal_base(&self) -> Option<Type> {
893        let widened = self.widen_literal();
894        (widened != *self && matches!(widened, Type::Number | Type::String | Type::Boolean))
895            .then_some(widened)
896    }
897
898    /// Whether every part of this type is a string — a `string`, a
899    /// string-literal type, or a union of those.
900    ///
901    /// The typechecker, the import collector, and codegen must agree on this
902    /// exactly: a shape accepted by one and not the others emits a `$string`
903    /// into an `f64` slot, which fails Wasm validation rather than type
904    /// checking. Contrast [`contains_string`](Self::contains_string), which
905    /// asks the `any` question.
906    pub fn is_string_shaped(&self) -> bool {
907        match self.peel() {
908            Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => true,
909            Type::Union(members) => members.iter().all(Type::is_string_shaped),
910            _ => false,
911        }
912    }
913
914    /// Whether *some* part of this type is a string. Diagnostics-only: it is
915    /// what decides whether a rejected operand still deserves the "convert
916    /// first" help, so `string | null` gets it.
917    pub fn contains_string(&self) -> bool {
918        match self.peel() {
919            Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => true,
920            Type::Union(members) => members.iter().any(Type::contains_string),
921            _ => false,
922        }
923    }
924
925    /// Whether any member, through unions, satisfies `leaf`, which sees peeled
926    /// types. Syntactic: an alias reference is a leaf, not expanded; the
927    /// typechecker's `any_resolved_member` follows alias bodies through its
928    /// resolver.
929    pub fn any_member(&self, leaf: &dyn Fn(&Type) -> bool) -> bool {
930        match self.peel() {
931            Type::Union(members) => members.iter().any(|member| member.any_member(leaf)),
932            member => leaf(member),
933        }
934    }
935
936    /// Whether every member, through unions, satisfies `leaf`. Syntactic,
937    /// like [`Type::any_member`].
938    pub fn all_members(&self, leaf: &dyn Fn(&Type) -> bool) -> bool {
939        !self.any_member(&|member| !leaf(member))
940    }
941
942    /// Whether `null` is spelled among the members. Syntactic, unlike the
943    /// typechecker's `type_admits_null`: `unknown` doesn't count.
944    pub fn spells_null(&self) -> bool {
945        self.any_member(&|member| matches!(member, Type::Null))
946    }
947
948    /// Whether `undefined`, or an erased `void` result, is spelled among the
949    /// members. `unknown` and type parameters don't count.
950    pub fn spells_undefined(&self) -> bool {
951        self.any_member(&|member| matches!(member, Type::Undefined | Type::Void))
952    }
953
954    /// Whether a value of the member type `leaf` has no JSON text at the
955    /// document root, so `JSON.stringify` returns `undefined` for it.
956    /// Objects and arrays holding such values still produce a document.
957    pub fn stringifies_to_undefined(leaf: &Type) -> bool {
958        matches!(
959            leaf,
960            Type::Undefined
961                | Type::Void
962                | Type::Unknown
963                | Type::GenericParam { .. }
964                | Type::TypeVar(_)
965                | Type::Function { .. }
966        )
967    }
968
969    /// Normalizes to a flat member list that is unique *by peeled type*.
970    ///
971    /// Both halves peel because a nominal key is unsound here: an alias whose
972    /// body is a union is a nested union in disguise, and every downstream
973    /// per-member probe (`value_type`'s nullability test, narrowing's member
974    /// walk) reads it as one opaque member. Deduping nominally leaves
975    /// `N | number` as a two-member union that no operator is defined on.
976    ///
977    /// The alias label a union displays therefore survives only where it is
978    /// still a single member: an alias of a union is replaced by its members,
979    /// and among duplicates the alias-labelled spelling wins regardless of
980    /// source order, so `N | number` and `number | N` both keep `N`.
981    ///
982    /// `peel` stops at a *recursive* alias (it is a name, not a body), so a
983    /// recursive alias never dedups against its own expansion. Both spellings
984    /// lower the same way, so this costs a redundant member, not correctness.
985    pub fn union(members: Vec<Type>) -> Type {
986        if members.iter().any(|m| matches!(m.peel(), Type::Error)) {
987            return Type::Error;
988        }
989        // `unknown | T` → `unknown`; peels aliases so `type Dyn = unknown` still collapses.
990        if members.iter().any(|m| matches!(m.peel(), Type::Unknown)) {
991            return Type::Unknown;
992        }
993        // `T | never` → `T`. After Error/Unknown checks so cascading-silence wins.
994        let members: Vec<Type> = members
995            .into_iter()
996            .filter(|m| !matches!(m.peel(), Type::Never))
997            .collect();
998        let mut flat: Vec<Type> = Vec::new();
999        for m in members {
1000            match m.without_aliases() {
1001                Type::Union(inner) => flat.extend(inner.iter().cloned()),
1002                _ => flat.push(m),
1003            }
1004        }
1005        // Sorting by the peeled key groups alias-labelled duplicates next to
1006        // their body; aliases rank 0 so they sort first within the group, and
1007        // `dedup_by` keeps the earlier element — the name the user wrote.
1008        let alias_rank = |t: &Type| u8::from(!matches!(t, Type::Alias { .. }));
1009        flat.sort_by(|a, b| {
1010            a.without_aliases()
1011                .cmp(b.without_aliases())
1012                .then_with(|| alias_rank(a).cmp(&alias_rank(b)))
1013        });
1014        flat.dedup_by(|a, b| a.without_aliases() == b.without_aliases());
1015        fold_boolean_literals(&mut flat);
1016        fold_enum_members(&mut flat);
1017        if flat.len() > 1 {
1018            return Type::Union(flat);
1019        }
1020        match flat.into_iter().next() {
1021            Some(member) => member,
1022            None => Type::Never,
1023        }
1024    }
1025
1026    /// For method dispatch. Erased generics route to `Object`. Aliases peel. Returns `None`
1027    /// for function, void, null, and error. The first tuple element is the owning package
1028    /// for diagnostics; the mangled name is the structural lookup key. Built-in
1029    /// interfaces (`Number`, `Array`, `Object`, …) live in the prelude package.
1030    pub fn interface_routing(&self) -> Option<(MangledName, &str, &str, Vec<Type>)> {
1031        let prelude = crate::mangle::PRELUDE_PACKAGE;
1032        match self.primitive_behavior() {
1033            // A literal type routes to its base's interface: `"abc".at(0)` resolves the
1034            // same members a `string` receiver does. The prelude `Number`/`String`
1035            // interfaces expose no mutating members, so routing a literal there cannot
1036            // invalidate it.
1037            Type::Number | Type::NumberLiteral(_) => Some((
1038                crate::mangle::prelude("Number"),
1039                prelude,
1040                "Number",
1041                Vec::new(),
1042            )),
1043            Type::BigInt => Some((
1044                crate::mangle::prelude("BigInt"),
1045                prelude,
1046                "BigInt",
1047                Vec::new(),
1048            )),
1049            Type::Boolean => Some((
1050                crate::mangle::prelude("Boolean"),
1051                prelude,
1052                "Boolean",
1053                Vec::new(),
1054            )),
1055            Type::String | Type::StringLiteral(_) => Some((
1056                crate::mangle::prelude("String"),
1057                prelude,
1058                "String",
1059                Vec::new(),
1060            )),
1061            Type::Uint8Array => Some((
1062                crate::mangle::prelude("Uint8Array"),
1063                prelude,
1064                "Uint8Array",
1065                Vec::new(),
1066            )),
1067            Type::Array(elem) => Some((
1068                crate::mangle::prelude("Array"),
1069                prelude,
1070                "Array",
1071                vec![(**elem).clone()],
1072            )),
1073            // Tuples are arrays at runtime; route to `Array` with the positions'
1074            // union as the element type so reads (`length`, `map`, `at`, …) flow
1075            // through. Mutating methods are rejected separately at the call site.
1076            Type::Tuple(elements) => Some((
1077                crate::mangle::prelude("Array"),
1078                prelude,
1079                "Array",
1080                vec![Type::union(elements.elements.clone())],
1081            )),
1082            Type::Object { .. } | Type::TypeVar(_) | Type::GenericParam { .. } => Some((
1083                crate::mangle::prelude("Object"),
1084                prelude,
1085                "Object",
1086                Vec::new(),
1087            )),
1088            Type::InterfaceRef {
1089                mangled,
1090                package,
1091                name,
1092                args,
1093                ..
1094            }
1095            | Type::ClassRef {
1096                mangled,
1097                package,
1098                name,
1099                args,
1100                ..
1101            } => Some((
1102                mangled.clone(),
1103                package.as_str(),
1104                name.as_str(),
1105                args.clone(),
1106            )),
1107            // Enum values route to Object (not Number/String) to keep the API surface minimal.
1108            Type::NumberEnum { .. } | Type::StringEnum { .. } => Some((
1109                crate::mangle::prelude("Object"),
1110                prelude,
1111                "Object",
1112                Vec::new(),
1113            )),
1114            // `unknown` routes to Object so vtable methods work.
1115            Type::Unknown => Some((
1116                crate::mangle::prelude("Object"),
1117                prelude,
1118                "Object",
1119                Vec::new(),
1120            )),
1121            _ => None,
1122        }
1123    }
1124}
1125
1126/// `ty` as written beside a `?` marker, which already admits `undefined`: without
1127/// its `undefined` member. A type that is only `undefined` stays as it is.
1128/// Every renderer of optional parameters shows them through this, except
1129/// `type_rendering`'s `Frame::Optional`, which applies the same rule to
1130/// borrowed members.
1131pub(crate) fn shown_beside_optional_marker(ty: &Type) -> Type {
1132    match ty.peel() {
1133        Type::Union(members) => Type::union(
1134            members
1135                .iter()
1136                .filter(|member| !matches!(member.peel(), Type::Undefined))
1137                .cloned()
1138                .collect(),
1139        ),
1140        _ => ty.clone(),
1141    }
1142}
1143
1144/// `s` escaped for a double-quoted string the way `tsc` prints a string literal
1145/// type (`escapeString` in TypeScript's `utilities.ts`): `"G\"HI"`, `"a\nb"`.
1146pub(crate) fn escape_string_literal(s: &str) -> String {
1147    let mut out = String::with_capacity(s.len());
1148    let mut chars = crate::literal_units::literal_chars(s).peekable();
1149    while let Some(c) = chars.next() {
1150        let c = match c {
1151            Ok(c) => c,
1152            Err(lone) => {
1153                out.push_str(&format!("\\u{lone:04X}"));
1154                continue;
1155            }
1156        };
1157        match c {
1158            '"' => out.push_str("\\\""),
1159            '\\' => out.push_str("\\\\"),
1160            '\n' => out.push_str("\\n"),
1161            '\r' => out.push_str("\\r"),
1162            '\t' => out.push_str("\\t"),
1163            '\u{8}' => out.push_str("\\b"),
1164            '\u{b}' => out.push_str("\\v"),
1165            '\u{c}' => out.push_str("\\f"),
1166            // `\0` before a digit would read as an octal escape.
1167            '\0' if chars
1168                .peek()
1169                .is_some_and(|next| next.is_ok_and(|d| d.is_ascii_digit())) =>
1170            {
1171                out.push_str("\\x00");
1172            }
1173            '\0' => out.push_str("\\0"),
1174            '\u{0}'..='\u{1f}' | '\u{85}' | '\u{2028}' | '\u{2029}' => {
1175                out.push_str(&format!("\\u{:04X}", u32::from(c)));
1176            }
1177            c => out.push(c),
1178        }
1179    }
1180    out
1181}
1182
1183/// The [`Type::BigIntLiteral`] of a bigint literal's decimal digits, as the
1184/// lexer writes them.
1185pub fn bigint_literal_type(digits: &str) -> Type {
1186    Type::BigIntLiteral(canonical_bigint_digits(digits))
1187}
1188
1189/// `digits` negated, in [`Type::BigIntLiteral`]'s spelling: `-0n` is `0n`.
1190pub fn negate_bigint_digits(digits: &str) -> String {
1191    let canonical = canonical_bigint_digits(digits);
1192    if canonical == "0" {
1193        return canonical;
1194    }
1195    match canonical.strip_prefix('-') {
1196        Some(magnitude) => magnitude.to_string(),
1197        None => format!("-{canonical}"),
1198    }
1199}
1200
1201/// `digits` without leading zeros, keeping a sign, and `0` for zero.
1202fn canonical_bigint_digits(digits: &str) -> String {
1203    let (sign, magnitude) = match digits.strip_prefix('-') {
1204        Some(magnitude) => ("-", magnitude),
1205        None => ("", digits),
1206    };
1207    match magnitude.trim_start_matches('0') {
1208        "" => "0".to_string(),
1209        trimmed => format!("{sign}{trimmed}"),
1210    }
1211}
1212
1213/// `boolean` is `true | false`: a union holding both literals, or `boolean`
1214/// and either literal, holds exactly `boolean`. `members` is sorted and
1215/// deduplicated, and stays so.
1216fn fold_boolean_literals(members: &mut Vec<Type>) {
1217    let is_boolean = |m: &Type| matches!(m.without_aliases(), Type::Boolean);
1218    let is_literal = |m: &Type| matches!(m.without_aliases(), Type::BooleanLiteral(_));
1219    let literals = members.iter().filter(|m| is_literal(m)).count();
1220    let has_boolean = members.iter().any(is_boolean);
1221    if literals == 0 || (literals == 1 && !has_boolean) {
1222        return;
1223    }
1224    members.retain(|m| !is_literal(m));
1225    if !has_boolean {
1226        let at = members.partition_point(|m| m.without_aliases() < &Type::Boolean);
1227        members.insert(at, Type::Boolean);
1228    }
1229}
1230
1231/// Reduces enum types as TypeScript does: a member beside its enum is absorbed
1232/// by it, members naming every member of their enum are the enum, and an enum
1233/// or member beside its primitive (`E | number`) is absorbed by the primitive.
1234/// Leaves `members` sorted and deduplicated.
1235fn fold_enum_members(members: &mut Vec<Type>) {
1236    absorb_enums_into_primitives(members);
1237    let complete = complete_enums(members);
1238    if complete.is_empty() {
1239        return;
1240    }
1241    for member in members.iter_mut() {
1242        if let Type::NumberEnum { mangled, .. } | Type::StringEnum { mangled, .. } =
1243            member.without_aliases()
1244            && complete.contains(mangled)
1245        {
1246            *member = member.without_aliases().without_enum_member();
1247        }
1248    }
1249    members.sort_by(|a, b| a.without_aliases().cmp(b.without_aliases()));
1250    members.dedup_by(|a, b| a.without_aliases() == b.without_aliases());
1251}
1252
1253/// Drops every enum type and member beside the primitive it holds.
1254fn absorb_enums_into_primitives(members: &mut Vec<Type>) {
1255    let has = |base: &Type| members.iter().any(|m| m.without_aliases() == base);
1256    let (has_number, has_string) = (has(&Type::Number), has(&Type::String));
1257    members.retain(|m| match m.without_aliases() {
1258        Type::NumberEnum { .. } => !has_number,
1259        Type::StringEnum { .. } => !has_string,
1260        _ => true,
1261    });
1262}
1263
1264/// The enums `members` holds whole: as the enum itself, or by naming every
1265/// one of its members.
1266fn complete_enums(members: &[Type]) -> BTreeSet<MangledName> {
1267    struct NamedMembers<'a> {
1268        member_count: usize,
1269        names: BTreeSet<&'a str>,
1270    }
1271    let mut named: BTreeMap<&MangledName, NamedMembers> = BTreeMap::new();
1272    let mut whole: BTreeSet<&MangledName> = BTreeSet::new();
1273    for member in members {
1274        let ty = member.without_aliases();
1275        if let Some((mangled, name, member_count)) = ty.enum_member_name() {
1276            named
1277                .entry(mangled)
1278                .or_insert_with(|| NamedMembers {
1279                    member_count,
1280                    names: BTreeSet::new(),
1281                })
1282                .names
1283                .insert(name);
1284        } else if let Type::NumberEnum { mangled, .. } | Type::StringEnum { mangled, .. } = ty {
1285            whole.insert(mangled);
1286        }
1287    }
1288    named
1289        .into_iter()
1290        .filter(|(mangled, named)| {
1291            whole.contains(mangled) || named.names.len() >= named.member_count
1292        })
1293        .map(|(mangled, _)| mangled.clone())
1294        .collect()
1295}
1296
1297impl Type {
1298    pub fn render_checked(
1299        &self,
1300        limits: crate::rendering::RenderLimits,
1301    ) -> Result<crate::rendering::RenderedText, crate::rendering::RenderError> {
1302        crate::type_rendering::render(self, limits)
1303    }
1304}
1305
1306/// A union's members in the order they are printed. Members are stored in
1307/// canonical order, which puts `null` and `undefined` first; they print last,
1308/// as TypeScript prints them: `string | null | undefined`.
1309pub fn union_display_order(members: &[Type]) -> Vec<&Type> {
1310    let mut ordered: Vec<&Type> = members.iter().collect();
1311    ordered.sort_by_key(|m| match m {
1312        Type::Null => 1,
1313        Type::Undefined => 2,
1314        _ => 0,
1315    });
1316    ordered
1317}
1318
1319impl fmt::Display for Type {
1320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1321        match self.render_checked(crate::rendering::RenderLimits::default()) {
1322            Ok(rendered) => f.write_str(&rendered.text),
1323            Err(_) => f.write_str("[diagnostic type unavailable]"),
1324        }
1325    }
1326}
1327
1328#[cfg(test)]
1329mod tests {
1330    use super::{Package, Type};
1331
1332    #[test]
1333    fn tuple_mapping_preserves_optional_positions() {
1334        let tuple = super::TupleType {
1335            elements: vec![Type::NumberLiteral(super::LiteralF64(1.0)), Type::Undefined],
1336            optional: 1,
1337        };
1338        let mapped = tuple.map(Type::widen_literal);
1339        assert_eq!(mapped.optional, 1);
1340        assert_eq!(mapped.required_len(), 1);
1341        assert_eq!(mapped.elements, vec![Type::Number, Type::Undefined]);
1342    }
1343
1344    #[test]
1345    fn optional_fields_read_as_undefined() {
1346        let field = super::ObjectField::optional(Type::String);
1347        assert_eq!(
1348            field.read_ty(),
1349            Type::union(vec![Type::String, Type::Undefined])
1350        );
1351    }
1352
1353    #[test]
1354    fn optional_function_and_tuple_display_preserve_omission() {
1355        let ty = Type::Function {
1356            params: vec![Type::union(vec![Type::String, Type::Undefined])],
1357            optional: 1,
1358            ret: Box::new(Type::Void),
1359            predicate: None,
1360            has_rest: false,
1361        };
1362        // An optional parameter admits `undefined` already, as `tsc` prints it.
1363        assert_eq!(ty.to_string(), "(arg0?: string) => void");
1364        let tuple = Type::Tuple(super::TupleType {
1365            elements: vec![
1366                Type::Number,
1367                Type::union(vec![Type::String, Type::Undefined]),
1368            ],
1369            optional: 1,
1370        });
1371        assert_eq!(tuple.to_string(), "[number, (string | undefined)?]");
1372    }
1373
1374    #[test]
1375    fn atomic_display() {
1376        assert_eq!(Type::Number.to_string(), "number");
1377        assert_eq!(Type::String.to_string(), "string");
1378        assert_eq!(Type::Boolean.to_string(), "boolean");
1379        assert_eq!(Type::Null.to_string(), "null");
1380        assert_eq!(Type::Void.to_string(), "void");
1381        assert_eq!(Type::Error.to_string(), "<error>");
1382    }
1383
1384    #[test]
1385    fn readonly_display_matches_typescript() {
1386        let numbers = Type::Array(Box::new(Type::Number));
1387        let readonly = Type::Readonly(Box::new(numbers.clone()));
1388        assert_eq!(readonly.to_string(), "readonly number[]");
1389        assert_eq!(
1390            Type::Array(Box::new(readonly.clone())).to_string(),
1391            "(readonly number[])[]"
1392        );
1393        assert_eq!(
1394            Type::Readonly(Box::new(Type::Array(Box::new(numbers)))).to_string(),
1395            "readonly number[][]"
1396        );
1397        assert_eq!(
1398            Type::Readonly(Box::new(Type::Tuple(
1399                vec![Type::Number, Type::String].into()
1400            )))
1401            .to_string(),
1402            "readonly [number, string]"
1403        );
1404        assert!(readonly.is_readonly_array());
1405        assert_eq!(readonly.peel(), &Type::Array(Box::new(Type::Number)));
1406    }
1407
1408    #[test]
1409    fn string_literal_display_escapes_like_typescript() {
1410        // Each expectation is what `tsc` 5.9 prints for the same literal type.
1411        let cases = [
1412            ("G\"HI\\", r#""G\"HI\\""#),
1413            ("a\nb\r\t", r#""a\nb\r\t""#),
1414            ("\u{8}\u{b}\u{c}", r#""\b\v\f""#),
1415            ("\0x", r#""\0x""#),
1416            ("\u{0}1", r#""\x001""#),
1417            ("\u{7}\u{1b}", r#""\u0007\u001B""#),
1418            ("\u{85}\u{2028}\u{2029}", r#""\u0085\u2028\u2029""#),
1419            ("\u{7f}é", "\"\u{7f}é\""),
1420        ];
1421        for (value, printed) in cases {
1422            assert_eq!(Type::StringLiteral(value.into()).to_string(), printed);
1423        }
1424    }
1425
1426    #[test]
1427    fn structural_equality() {
1428        assert_eq!(Type::Number, Type::Number);
1429        assert_ne!(Type::Number, Type::String);
1430
1431        let a = Type::Function {
1432            params: vec![Type::Number, Type::String],
1433            ret: Box::new(Type::Boolean),
1434            predicate: None,
1435            has_rest: false,
1436            optional: 0,
1437        };
1438        let b = Type::Function {
1439            params: vec![Type::Number, Type::String],
1440            ret: Box::new(Type::Boolean),
1441            predicate: None,
1442            has_rest: false,
1443            optional: 0,
1444        };
1445        let c = Type::Function {
1446            params: vec![Type::Number],
1447            ret: Box::new(Type::Boolean),
1448            predicate: None,
1449            has_rest: false,
1450            optional: 0,
1451        };
1452        assert_eq!(a, b);
1453        assert_ne!(a, c);
1454    }
1455
1456    #[test]
1457    fn function_display_no_params() {
1458        let t = Type::Function {
1459            params: vec![],
1460            ret: Box::new(Type::Number),
1461            predicate: None,
1462            has_rest: false,
1463            optional: 0,
1464        };
1465        assert_eq!(t.to_string(), "() => number");
1466    }
1467
1468    #[test]
1469    fn function_display_one_param() {
1470        let t = Type::Function {
1471            params: vec![Type::Number],
1472            ret: Box::new(Type::Boolean),
1473            predicate: None,
1474            has_rest: false,
1475            optional: 0,
1476        };
1477        assert_eq!(t.to_string(), "(arg0: number) => boolean");
1478    }
1479
1480    #[test]
1481    fn function_display_multi_param() {
1482        let t = Type::Function {
1483            params: vec![Type::Number, Type::String],
1484            ret: Box::new(Type::Void),
1485            predicate: None,
1486            has_rest: false,
1487            optional: 0,
1488        };
1489        assert_eq!(t.to_string(), "(arg0: number, arg1: string) => void");
1490    }
1491
1492    #[test]
1493    fn function_display_nested() {
1494        let inner = Type::Function {
1495            params: vec![],
1496            ret: Box::new(Type::Number),
1497            predicate: None,
1498            has_rest: false,
1499            optional: 0,
1500        };
1501        let outer = Type::Function {
1502            params: vec![],
1503            ret: Box::new(inner),
1504            predicate: None,
1505            has_rest: false,
1506            optional: 0,
1507        };
1508        assert_eq!(outer.to_string(), "() => () => number");
1509    }
1510
1511    #[test]
1512    fn clone_roundtrip() {
1513        let t = Type::Function {
1514            params: vec![Type::Number, Type::String],
1515            ret: Box::new(Type::Boolean),
1516            predicate: None,
1517            has_rest: false,
1518            optional: 0,
1519        };
1520        assert_eq!(t.clone(), t);
1521    }
1522
1523    fn point_type() -> Type {
1524        let mut fields = std::collections::BTreeMap::new();
1525        fields.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1526        fields.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1527        Type::Object {
1528            index: None,
1529            fields,
1530        }
1531    }
1532
1533    #[test]
1534    fn object_structural_equality_ignores_field_insertion_order() {
1535        let mut a = std::collections::BTreeMap::new();
1536        a.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1537        a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1538
1539        let mut b = std::collections::BTreeMap::new();
1540        b.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1541        b.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1542
1543        assert_eq!(
1544            Type::Object {
1545                index: None,
1546                fields: a
1547            },
1548            Type::Object {
1549                index: None,
1550                fields: b
1551            }
1552        );
1553    }
1554
1555    #[test]
1556    fn object_inequality_on_different_field_set() {
1557        let mut a = std::collections::BTreeMap::new();
1558        a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1559        let mut b = std::collections::BTreeMap::new();
1560        b.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1561        assert_ne!(
1562            Type::Object {
1563                index: None,
1564                fields: a
1565            },
1566            Type::Object {
1567                index: None,
1568                fields: b
1569            }
1570        );
1571    }
1572
1573    #[test]
1574    fn object_display() {
1575        assert_eq!(point_type().to_string(), "{ x: number; y: number }");
1576        let empty = Type::Object {
1577            index: None,
1578            fields: std::collections::BTreeMap::new(),
1579        };
1580        assert_eq!(empty.to_string(), "{}");
1581    }
1582
1583    #[test]
1584    fn object_display_with_optional_field() {
1585        let mut fields = std::collections::BTreeMap::new();
1586        fields.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1587        fields.insert("y".to_string(), crate::ObjectField::optional(Type::String));
1588        let t = Type::Object {
1589            index: None,
1590            fields,
1591        };
1592        assert_eq!(t.to_string(), "{ x: number; y?: string }");
1593    }
1594
1595    #[test]
1596    fn object_inequality_required_vs_optional() {
1597        let mut a = std::collections::BTreeMap::new();
1598        a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1599        let mut b = std::collections::BTreeMap::new();
1600        b.insert("x".to_string(), crate::ObjectField::optional(Type::Number));
1601        assert_ne!(
1602            Type::Object {
1603                index: None,
1604                fields: a
1605            },
1606            Type::Object {
1607                index: None,
1608                fields: b
1609            }
1610        );
1611    }
1612
1613    #[test]
1614    fn array_equality() {
1615        assert_eq!(
1616            Type::Array(Box::new(Type::Number)),
1617            Type::Array(Box::new(Type::Number))
1618        );
1619        assert_ne!(
1620            Type::Array(Box::new(Type::Number)),
1621            Type::Array(Box::new(Type::String))
1622        );
1623    }
1624
1625    #[test]
1626    fn array_display() {
1627        assert_eq!(Type::Array(Box::new(Type::Number)).to_string(), "number[]");
1628        assert_eq!(
1629            Type::Array(Box::new(Type::Array(Box::new(Type::Number)))).to_string(),
1630            "number[][]"
1631        );
1632    }
1633
1634    #[test]
1635    fn nested_object_in_array_display() {
1636        let arr = Type::Array(Box::new(point_type()));
1637        assert_eq!(arr.to_string(), "{ x: number; y: number }[]");
1638    }
1639
1640    #[test]
1641    fn type_var_display_uses_source_name() {
1642        assert_eq!(Type::TypeVar("T".to_string()).to_string(), "T");
1643        assert_eq!(Type::TypeVar("Key".to_string()).to_string(), "Key");
1644    }
1645
1646    #[test]
1647    fn type_var_structural_equality_by_name() {
1648        let t = Type::TypeVar("T".to_string());
1649        let t2 = Type::TypeVar("T".to_string());
1650        let u = Type::TypeVar("U".to_string());
1651        assert_eq!(t, t2);
1652        assert_ne!(t, u);
1653        assert_ne!(t, Type::Number);
1654    }
1655
1656    #[test]
1657    fn type_var_inside_array_and_function_displays_through() {
1658        let arr = Type::Array(Box::new(Type::TypeVar("T".to_string())));
1659        assert_eq!(arr.to_string(), "T[]");
1660        let func = Type::Function {
1661            params: vec![Type::TypeVar("T".to_string())],
1662            ret: Box::new(Type::TypeVar("U".to_string())),
1663            predicate: None,
1664            has_rest: false,
1665            optional: 0,
1666        };
1667        assert_eq!(func.to_string(), "(arg0: T) => U");
1668    }
1669
1670    #[test]
1671    fn generic_param_display_uses_name_not_id() {
1672        let gp = Type::GenericParam {
1673            id: 42,
1674            name: "T".to_string(),
1675        };
1676        assert_eq!(gp.to_string(), "T");
1677    }
1678
1679    #[test]
1680    fn generic_param_equality_by_id() {
1681        let a = Type::GenericParam {
1682            id: 5,
1683            name: "T".to_string(),
1684        };
1685        let b = Type::GenericParam {
1686            id: 5,
1687            name: "T".to_string(),
1688        };
1689        assert_eq!(a, b);
1690
1691        let c = Type::GenericParam {
1692            id: 6,
1693            name: "T".to_string(),
1694        };
1695        assert_ne!(a, c);
1696    }
1697
1698    #[test]
1699    fn union_canonical_dedup() {
1700        let t = Type::union(vec![Type::Number, Type::Null, Type::Number]);
1701        let Type::Union(members) = t else {
1702            panic!("expected Union, got {t:?}");
1703        };
1704        assert_eq!(members, vec![Type::Number, Type::Null]);
1705    }
1706
1707    #[test]
1708    fn union_canonical_order_independent() {
1709        let a = Type::union(vec![Type::Number, Type::Null]);
1710        let b = Type::union(vec![Type::Null, Type::Number]);
1711        assert_eq!(a, b);
1712    }
1713
1714    #[test]
1715    fn union_collapses_to_single_member() {
1716        let t = Type::union(vec![Type::Number, Type::Number]);
1717        assert_eq!(t, Type::Number);
1718    }
1719
1720    #[test]
1721    fn union_flattens_nested() {
1722        let inner = Type::union(vec![Type::String, Type::Null]);
1723        let outer = Type::union(vec![Type::Number, inner]);
1724        let expected = Type::union(vec![Type::Number, Type::String, Type::Null]);
1725        assert_eq!(outer, expected);
1726    }
1727
1728    #[test]
1729    fn union_error_member_collapses_to_error() {
1730        let t = Type::union(vec![Type::Number, Type::Error]);
1731        assert_eq!(t, Type::Error);
1732    }
1733
1734    #[test]
1735    fn union_display_basic() {
1736        let t = Type::union(vec![Type::Number, Type::String]);
1737        assert_eq!(t.to_string(), "number | string");
1738    }
1739
1740    #[test]
1741    fn union_display_with_null() {
1742        let t = Type::union(vec![Type::Number, Type::Null]);
1743        assert_eq!(t.to_string(), "number | null");
1744        let t2 = Type::union(vec![Type::Null, Type::Number]);
1745        assert_eq!(t2.to_string(), "number | null");
1746    }
1747
1748    #[test]
1749    fn union_display_wraps_function_member() {
1750        let fn_ty = Type::Function {
1751            params: vec![],
1752            ret: Box::new(Type::String),
1753            predicate: None,
1754            has_rest: false,
1755            optional: 0,
1756        };
1757        let t = Type::union(vec![fn_ty, Type::Boolean]);
1758        assert_eq!(t.to_string(), "boolean | (() => string)");
1759    }
1760
1761    use super::LiteralF64;
1762
1763    #[test]
1764    fn string_literal_display_is_quoted() {
1765        let t = Type::StringLiteral("north".to_string());
1766        assert_eq!(t.to_string(), "\"north\"");
1767    }
1768
1769    #[test]
1770    fn number_literal_display_is_unquoted_with_whole_number_shape() {
1771        let whole = Type::NumberLiteral(LiteralF64(42.0));
1772        assert_eq!(whole.to_string(), "42");
1773        let frac = Type::NumberLiteral(LiteralF64(4.5));
1774        assert_eq!(frac.to_string(), "4.5");
1775    }
1776
1777    #[test]
1778    fn string_literal_equality_by_value() {
1779        let a = Type::StringLiteral("hi".to_string());
1780        let b = Type::StringLiteral("hi".to_string());
1781        let c = Type::StringLiteral("bye".to_string());
1782        assert_eq!(a, b);
1783        assert_ne!(a, c);
1784    }
1785
1786    #[test]
1787    fn number_literal_equality_by_value() {
1788        let a = Type::NumberLiteral(LiteralF64(42.0));
1789        let b = Type::NumberLiteral(LiteralF64(42.0));
1790        let c = Type::NumberLiteral(LiteralF64(43.0));
1791        assert_eq!(a, b);
1792        assert_ne!(a, c);
1793    }
1794
1795    #[test]
1796    fn union_sorts_literals_next_to_base_types() {
1797        // NumberLiteral sorts after Number, StringLiteral after String per variant order.
1798        let t = Type::union(vec![
1799            Type::String,
1800            Type::StringLiteral("hi".to_string()),
1801            Type::Number,
1802            Type::NumberLiteral(LiteralF64(42.0)),
1803        ]);
1804        assert_eq!(t.to_string(), "number | 42 | string | \"hi\"");
1805    }
1806
1807    #[test]
1808    fn widen_literal_replaces_a_literal_with_its_base() {
1809        assert_eq!(
1810            Type::NumberLiteral(LiteralF64(1.0)).widen_literal(),
1811            Type::Number
1812        );
1813        assert_eq!(
1814            Type::StringLiteral("hi".to_string()).widen_literal(),
1815            Type::String
1816        );
1817    }
1818
1819    /// A union widens memberwise, and `Type::union` then deduplicates — so a union of
1820    /// literals over one base collapses to that base rather than repeating it.
1821    #[test]
1822    fn widen_literal_collapses_a_union_of_literals() {
1823        let t = Type::union(vec![
1824            Type::NumberLiteral(LiteralF64(1.0)),
1825            Type::NumberLiteral(LiteralF64(2.0)),
1826        ]);
1827        assert_eq!(t.widen_literal(), Type::Number);
1828    }
1829
1830    /// Enums are nominal, not literal: widening one would discard the identity its
1831    /// members are checked against. `primitive_behavior` is the enum-aware accessor.
1832    #[test]
1833    fn widen_literal_leaves_everything_else_alone() {
1834        assert_eq!(Type::Number.widen_literal(), Type::Number);
1835        assert_eq!(Type::Boolean.widen_literal(), Type::Boolean);
1836        let arr = Type::Array(Box::new(Type::NumberLiteral(LiteralF64(1.0))));
1837        assert_eq!(arr.widen_literal(), arr);
1838    }
1839
1840    #[test]
1841    fn union_of_two_string_literals_sorts_by_value() {
1842        let t = Type::union(vec![
1843            Type::StringLiteral("south".to_string()),
1844            Type::StringLiteral("north".to_string()),
1845        ]);
1846        assert_eq!(t.to_string(), "\"north\" | \"south\"");
1847    }
1848
1849    #[test]
1850    fn literal_f64_equality_distinguishes_negative_zero_at_bit_level() {
1851        // Bit-pattern equality distinguishes -0.0 and +0.0; parser canonicalizes -0.0→0.0 so
1852        // this case won't appear in practice.
1853        let pos = LiteralF64(0.0);
1854        let neg = LiteralF64(-0.0);
1855        assert_ne!(pos, neg);
1856    }
1857
1858    #[test]
1859    fn type_var_and_generic_param_are_distinct_types() {
1860        // TypeVar is signature form; GenericParam is body form.
1861        // Even with the same name they're different variants.
1862        let tv = Type::TypeVar("T".to_string());
1863        let gp = Type::GenericParam {
1864            id: 0,
1865            name: "T".to_string(),
1866        };
1867        assert_ne!(tv, gp);
1868    }
1869
1870    #[test]
1871    fn alias_display_renders_name_not_body() {
1872        let ty = user_alias("Circle", point_type());
1873        assert_eq!(ty.to_string(), "Circle");
1874    }
1875
1876    #[test]
1877    fn peel_walks_through_alias_to_underlying() {
1878        let inner = Type::Number;
1879        let aliased = user_alias("ID", inner.clone());
1880        assert_eq!(aliased.peel(), &inner);
1881    }
1882
1883    #[test]
1884    fn peel_is_idempotent_through_nested_aliases() {
1885        let nested = user_alias("A", user_alias("B", Type::Number));
1886        assert_eq!(nested.peel(), &Type::Number);
1887        assert_eq!(nested.peel().peel(), &Type::Number);
1888    }
1889
1890    #[test]
1891    fn alias_is_not_structurally_equal_to_underlying() {
1892        let aliased = user_alias("ID", Type::Number);
1893        assert_ne!(aliased, Type::Number);
1894        assert_eq!(aliased.peel(), &Type::Number);
1895    }
1896
1897    #[test]
1898    fn alias_routes_to_underlying_interface() {
1899        let aliased = user_alias("ID", Type::Number);
1900        let (_mangled, _pkg, iface, _args) = aliased.interface_routing().expect("number routes");
1901        assert_eq!(iface, "Number");
1902    }
1903
1904    #[test]
1905    fn union_of_error_alias_collapses_to_error() {
1906        let alias_err = user_alias("Bad", Type::Error);
1907        let t = Type::union(vec![Type::Number, alias_err]);
1908        assert_eq!(t, Type::Error);
1909    }
1910
1911    #[test]
1912    fn union_preserves_distinct_alias_members_for_display() {
1913        let circle = user_alias("Circle", point_type());
1914        let rect = user_alias("Rectangle", Type::Number);
1915        let t = Type::union(vec![circle, rect]);
1916        // Ordered by the peeled bodies, which puts `Number` before the object
1917        // shape — not by the order the members were written.
1918        assert_eq!(t.to_string(), "Rectangle | Circle");
1919    }
1920
1921    #[test]
1922    fn union_dedups_an_alias_against_its_own_body_keeping_the_label() {
1923        let n = user_alias("N", Type::Number);
1924        assert_eq!(Type::union(vec![n.clone(), Type::Number]), n);
1925        assert_eq!(Type::union(vec![Type::Number, n.clone()]), n);
1926    }
1927
1928    #[test]
1929    fn union_dedups_aliases_that_share_one_body() {
1930        let circle = user_alias("Circle", point_type());
1931        let rect = user_alias("Rectangle", point_type());
1932        assert_eq!(Type::union(vec![circle.clone(), rect]), circle);
1933    }
1934
1935    #[test]
1936    fn union_flattens_an_alias_whose_body_is_a_union() {
1937        let nullable = user_alias("MN", Type::union(vec![Type::Number, Type::Null]));
1938        assert_eq!(
1939            Type::union(vec![nullable, Type::Number]),
1940            Type::union(vec![Type::Number, Type::Null])
1941        );
1942    }
1943
1944    #[test]
1945    fn generic_alias_display_includes_args() {
1946        let ty = user_alias_args("Box", vec![Type::Number], Type::Number);
1947        assert_eq!(ty.to_string(), "Box<number>");
1948    }
1949
1950    fn iface(package: &str, name: &str) -> Type {
1951        Type::interface_ref(
1952            Package(package.to_string()),
1953            name,
1954            crate::mangle::package_symbol(package, name),
1955            Vec::new(),
1956        )
1957    }
1958
1959    fn user_alias(name: &str, ty: Type) -> Type {
1960        user_alias_args(name, Vec::new(), ty)
1961    }
1962
1963    fn user_alias_args(name: &str, args: Vec<Type>, ty: Type) -> Type {
1964        Type::alias_ty(
1965            Package::user(),
1966            name,
1967            crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, name),
1968            args,
1969            Box::new(ty),
1970        )
1971    }
1972
1973    #[test]
1974    fn mangled_name_keys_type_identity() {
1975        // Identity is the mangled name. Same `(package, name)` ⇒ same mangled ⇒ equal;
1976        // a different owning package ⇒ different mangled ⇒ distinct (the multi-file
1977        // property: two same-named interfaces in different packages/modules are not
1978        // conflated).
1979        assert_eq!(iface("main", "Response"), iface("main", "Response"));
1980        let http = iface("submilli:http", "Response");
1981        let main = iface("main", "Response");
1982        assert_ne!(
1983            http, main,
1984            "different package ⇒ different mangled ⇒ distinct"
1985        );
1986        assert_ne!(
1987            http.cmp(&main),
1988            std::cmp::Ordering::Equal,
1989            "Ord must distinguish distinct mangled names"
1990        );
1991        assert_ne!(
1992            iface("main", "Response"),
1993            iface("main", "Other"),
1994            "different names stay distinct"
1995        );
1996    }
1997
1998    #[test]
1999    fn same_name_different_module_not_equal_nor_assignable() {
2000        // The core multi-file guarantee: a sibling module's `Logger`
2001        // (`mod:main#util#Logger`) is a distinct type from the root's `Logger`
2002        // (`main#Logger`), even though the bare name matches.
2003        let root = iface("main", "Logger");
2004        let sibling = Type::InterfaceRef {
2005            mangled: crate::mangle::package_module_symbol("main", "util", "Logger"),
2006            package: Package::user(),
2007            name: "Logger".to_string(),
2008            args: Vec::new(),
2009        };
2010        assert_ne!(root, sibling);
2011        // Same mangled ⇒ equal.
2012        let root2 = iface("main", "Logger");
2013        assert_eq!(root, root2);
2014    }
2015
2016    #[test]
2017    fn union_dedups_interface_refs_by_mangled() {
2018        // Same mangled collapses to one member; different mangled stays a 2-member union.
2019        let same = Type::union(vec![iface("main", "Response"), iface("main", "Response")]);
2020        assert!(
2021            !matches!(same, Type::Union(_)),
2022            "identical refs collapse to one member, got {same:?}"
2023        );
2024        let distinct = Type::union(vec![
2025            iface("submilli:http", "Response"),
2026            iface("main", "Response"),
2027        ]);
2028        assert!(
2029            matches!(distinct, Type::Union(ref ms) if ms.len() == 2),
2030            "distinct-package refs stay a 2-member union, got {distinct:?}"
2031        );
2032    }
2033
2034    #[test]
2035    fn generic_alias_display_multiple_args() {
2036        let ty = user_alias_args("Pair", vec![Type::String, Type::Number], Type::Number);
2037        assert_eq!(ty.to_string(), "Pair<string, number>");
2038    }
2039}