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. Both compare their
140    /// parameters contravariantly, though `tsc` compares a method's both ways.
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    /// `object`: any value that isn't a primitive, `null` or `undefined`, as in
425    /// TypeScript. Every object, array, function and class instance is one. It
426    /// has `{}`'s members and, like `{}`, lowers as `unknown`.
427    NonPrimitive,
428}
429
430impl Type {
431    pub fn tuple(elements: Vec<Type>) -> Self {
432        Self::Tuple(elements.into())
433    }
434
435    pub fn is_regexp_match_array(&self) -> bool {
436        matches!(self.peel(), Self::InterfaceRef { mangled, .. }
437            if *mangled == crate::mangle::prelude("RegExpMatchArray")
438                || *mangled == crate::mangle::prelude("RegExpExecArray"))
439    }
440
441    pub fn regexp_element() -> Type {
442        Type::union(vec![Type::String, Type::Undefined])
443    }
444
445    /// A receiver whose computed string keys use the object property carrier.
446    pub fn is_structural_object(&self) -> bool {
447        if self.is_regexp_match_array() {
448            return false;
449        }
450        match self.peel() {
451            Self::Object { .. } | Self::InterfaceRef { .. } => true,
452            Self::Union(members) => members.iter().all(Self::is_structural_object),
453            _ => false,
454        }
455    }
456
457    /// Build an [`Type::InterfaceRef`]. `mangled` is the declaring symbol's mangled
458    /// name (its nominal identity) — pass the symbol's own `mangled_name`, never a
459    /// value recomputed from `package`/`name`, so two construction sites for the same
460    /// type always agree.
461    pub fn interface_ref(
462        package: Package,
463        name: impl Into<String>,
464        mangled: MangledName,
465        args: Vec<Type>,
466    ) -> Type {
467        Type::InterfaceRef {
468            mangled,
469            package,
470            name: name.into(),
471            args,
472        }
473    }
474
475    /// Build a [`Type::ClassRef`]. `mangled` is the class symbol's nominal identity;
476    /// pass the symbol's own `mangled_name`, never one recomputed from `package`/`name`.
477    pub fn class_ref(
478        package: Package,
479        name: impl Into<String>,
480        mangled: MangledName,
481        args: Vec<Type>,
482    ) -> Type {
483        Type::ClassRef {
484            mangled,
485            package,
486            name: name.into(),
487            args,
488        }
489    }
490
491    pub fn number_enum(package: Package, name: impl Into<String>, mangled: MangledName) -> Type {
492        Type::NumberEnum {
493            mangled,
494            package,
495            name: name.into(),
496            member: None,
497        }
498    }
499
500    pub fn string_enum(package: Package, name: impl Into<String>, mangled: MangledName) -> Type {
501        Type::StringEnum {
502            mangled,
503            package,
504            name: name.into(),
505            member: None,
506        }
507    }
508
509    /// The member literal type `E.A` of the enum type `self`, which has
510    /// `member_count` members. `self` must be a `NumberEnum` holding a number
511    /// or a `StringEnum` holding a string. As in TypeScript, the one member of
512    /// an enum has the enum's own type.
513    pub fn with_enum_member(
514        &self,
515        member_name: &str,
516        value: EnumValue,
517        member_count: usize,
518    ) -> Type {
519        if member_count < 2 {
520            return self.without_enum_member();
521        }
522        match (self.clone(), value) {
523            (
524                Type::NumberEnum {
525                    mangled,
526                    package,
527                    name,
528                    ..
529                },
530                EnumValue::Number(value),
531            ) => Type::NumberEnum {
532                mangled,
533                package,
534                name,
535                member: Some(EnumMember::new(member_name, value, member_count)),
536            },
537            (
538                Type::StringEnum {
539                    mangled,
540                    package,
541                    name,
542                    ..
543                },
544                EnumValue::String(value),
545            ) => Type::StringEnum {
546                mangled,
547                package,
548                name,
549                member: Some(EnumMember::new(member_name, value, member_count)),
550            },
551            (ty, _) => ty,
552        }
553    }
554
555    /// The enum a member literal type `E.A` belongs to; any other type unchanged.
556    pub fn without_enum_member(&self) -> Type {
557        match self {
558            Type::NumberEnum {
559                mangled,
560                package,
561                name,
562                member: Some(_),
563            } => Type::number_enum(package.clone(), name.clone(), mangled.clone()),
564            Type::StringEnum {
565                mangled,
566                package,
567                name,
568                member: Some(_),
569            } => Type::string_enum(package.clone(), name.clone(), mangled.clone()),
570            _ => self.clone(),
571        }
572    }
573
574    /// Whether this is one literal type, what TypeScript calls a unit type:
575    /// `1`, `"a"`, `true`, `1n` or an enum member `E.A`.
576    pub fn is_literal_type(&self) -> bool {
577        matches!(
578            self,
579            Type::NumberLiteral(_)
580                | Type::StringLiteral(_)
581                | Type::BooleanLiteral(_)
582                | Type::BigIntLiteral(_)
583        ) || self.is_enum_member()
584    }
585
586    /// Whether this is an enum member literal type `E.A`.
587    pub fn is_enum_member(&self) -> bool {
588        self.enum_member_name().is_some()
589    }
590
591    /// The enum, member name and member count of an enum member literal type `E.A`.
592    pub fn enum_member_name(&self) -> Option<(&MangledName, &str, usize)> {
593        match self {
594            Type::NumberEnum {
595                mangled,
596                member: Some(member),
597                ..
598            } => Some((mangled, &member.name, member.member_count)),
599            Type::StringEnum {
600                mangled,
601                member: Some(member),
602                ..
603            } => Some((mangled, &member.name, member.member_count)),
604            _ => None,
605        }
606    }
607
608    /// The value an enum member literal type `E.A` holds.
609    pub fn enum_member_value(&self) -> Option<EnumValue> {
610        match self.peel() {
611            Type::NumberEnum {
612                member: Some(member),
613                ..
614            } => Some(EnumValue::Number(member.value)),
615            Type::StringEnum {
616                member: Some(member),
617                ..
618            } => Some(EnumValue::String(member.value.clone())),
619            _ => None,
620        }
621    }
622
623    pub fn alias_ref(
624        package: Package,
625        name: impl Into<String>,
626        mangled: MangledName,
627        args: Vec<Type>,
628    ) -> Type {
629        Type::AliasRef {
630            mangled,
631            package,
632            name: name.into(),
633            args,
634        }
635    }
636
637    pub fn alias_ty(
638        package: Package,
639        name: impl Into<String>,
640        mangled: MangledName,
641        args: Vec<Type>,
642        ty: Box<Type>,
643    ) -> Type {
644        Type::Alias {
645            mangled,
646            package,
647            name: name.into(),
648            args,
649            ty,
650        }
651    }
652
653    /// Convenience for a non-namespaced prelude interface (`Error`, `Response`, …):
654    /// the prelude registers these with `mangled_name = prelude(name)`, so this is
655    /// the matching identity. Namespaced prelude types (`Temporal.*`) must instead
656    /// use [`Type::interface_ref`] with `extend(prelude("Temporal"), local)`.
657    pub fn prelude_interface(name: impl Into<String>, args: Vec<Type>) -> Type {
658        let name = name.into();
659        let mangled = crate::mangle::prelude(&name);
660        Type::interface_ref(Package::prelude(), name, mangled, args)
661    }
662
663    /// The built-in `Error` class reference — the type of `throw`/`catch` values
664    /// and the root of user error subclasses.
665    pub fn prelude_error_class() -> Type {
666        Type::class_ref(
667            Package::prelude(),
668            "Error",
669            crate::mangle::prelude("Error"),
670            Vec::new(),
671        )
672    }
673
674    /// The built-in `RangeError` class reference — the host-implemented
675    /// `Error` subclass for out-of-range failures.
676    pub fn prelude_range_error_class() -> Type {
677        Type::class_ref(
678            Package::prelude(),
679            "RangeError",
680            crate::mangle::prelude("RangeError"),
681            Vec::new(),
682        )
683    }
684
685    /// Remove display aliases while retaining generic guard identity.
686    pub fn without_aliases(&self) -> &Type {
687        match self {
688            Type::Alias { ty, .. } => ty.without_aliases(),
689            _ => self,
690        }
691    }
692
693    /// Whether this holds every value: `unknown`, or a union of `{}`, `null`
694    /// and `undefined` (and possibly more). Through aliases only: a refinement
695    /// like `T & unknown` holds only `T`'s values.
696    pub fn holds_every_value(&self) -> bool {
697        let members: &[Type] = match self.without_aliases() {
698            Type::Unknown => return true,
699            Type::Union(members) => members,
700            _ => return false,
701        };
702        // Through aliases only: `T & undefined` holds `undefined` only when `T`
703        // does.
704        members.iter().any(Type::is_empty_object)
705            && members
706                .iter()
707                .any(|m| matches!(m.without_aliases(), Type::Null))
708            && members
709                .iter()
710                .any(|m| matches!(m.without_aliases(), Type::Undefined))
711    }
712
713    /// The pieces of a refinement whose shape is a union: `T & ({} | null)`
714    /// is `T & {}` and `null`. `None` for any other type.
715    pub fn refinement_pieces(&self) -> Option<Vec<Type>> {
716        let Type::Refined { original, ty } = self.without_aliases() else {
717            return None;
718        };
719        let Type::Union(parts) = ty.peel() else {
720            return None;
721        };
722        Some(
723            parts
724                .iter()
725                .map(|part| match part.peel() {
726                    Type::Null | Type::Undefined | Type::Void => part.clone(),
727                    _ => Type::Refined {
728                        original: original.clone(),
729                        ty: Box::new(part.clone()),
730                    },
731                })
732                .collect(),
733        )
734    }
735
736    /// Whether this is a narrowed type parameter, like `T & {}`.
737    pub fn refines_type_parameter(&self) -> bool {
738        matches!(self.without_aliases(), Type::Refined { original, .. }
739            if matches!(original.peel(), Type::GenericParam { .. }))
740    }
741
742    pub fn peel(&self) -> &Type {
743        let mut t = self;
744        while let Type::Alias { ty, .. } | Type::Refined { ty, .. } | Type::Readonly(ty) = t {
745            t = ty;
746        }
747        t
748    }
749
750    /// `{ [key: string]: unknown }`: any object, interface or class instance,
751    /// for host declarations that read a value as an object. Not `{}`, which
752    /// also holds primitives that have no object representation.
753    pub fn any_object() -> Type {
754        Type::Object {
755            index: Some(IndexSignature {
756                value: Box::new(Type::Unknown),
757                readonly: false,
758            }),
759            fields: BTreeMap::new(),
760        }
761    }
762
763    /// `{}`, the object type with no fields.
764    pub fn empty_object() -> Type {
765        Type::Object {
766            fields: BTreeMap::new(),
767            index: None,
768        }
769    }
770
771    /// Whether this is `{}`: the object type with no fields and no index
772    /// signature. As in TypeScript, it holds every value but `null` and
773    /// `undefined`, primitives included, so codegen lowers it as `unknown`.
774    /// A refinement such as `T & {}` is not `{}`: it holds only `T`'s values.
775    pub fn is_empty_object(&self) -> bool {
776        let mut t = self;
777        while let Type::Alias { ty, .. } | Type::Readonly(ty) = t {
778            t = ty;
779        }
780        matches!(t, Type::Object { fields, index: None } if fields.is_empty())
781    }
782
783    /// [`peel`](Self::peel), with `{}` and `object` read as `unknown`: the type whose
784    /// representation and run-time operations a value of this type uses.
785    /// Codegen matches on this rather than on `peel`, so a `{}` holding a
786    /// number is never treated as an object struct. `peel` reads a refinement
787    /// as its shape, so `T & {}` lowers as `unknown`, like `T` itself.
788    pub fn lowered(&self) -> &Type {
789        let peeled = self.peel();
790        if peeled.is_empty_object() || matches!(peeled, Type::NonPrimitive) {
791            return &Type::Unknown;
792        }
793        peeled
794    }
795
796    /// [`peel`](Self::peel), but stopping at a [`Type::Readonly`] wrapper, for
797    /// sites that carry a type onward and must not drop its readonly-ness.
798    pub fn peel_preserving_readonly(&self) -> &Type {
799        let mut t = self;
800        while let Type::Alias { ty, .. } | Type::Refined { ty, .. } = t {
801            t = ty;
802        }
803        t
804    }
805
806    /// Whether writes through a value of this type are forbidden because it is a
807    /// `readonly` array or tuple, looking through aliases and refinements.
808    pub fn is_readonly_array(&self) -> bool {
809        matches!(self.peel_preserving_readonly(), Type::Readonly(_))
810    }
811
812    /// The element type of a rest parameter's array: `T` for `T[]` or
813    /// `readonly T[]`. Not peeled through aliases, since rest lowering matches the
814    /// parameter type as written.
815    pub fn rest_element(&self) -> Option<&Type> {
816        match self {
817            Type::Array(element) => Some(element),
818            Type::Readonly(inner) => match inner.as_ref() {
819                Type::Array(element) => Some(element),
820                _ => None,
821            },
822            _ => None,
823        }
824    }
825
826    /// A rest parameter's type with any `readonly` removed. Each call packs a
827    /// fresh array for the rest, so whether the callee may write to it is the
828    /// callee's own concern: two function types relate on their rest elements,
829    /// as in tsc.
830    pub fn rest_array_ignoring_readonly(&self) -> &Type {
831        match self {
832            Type::Readonly(inner) if matches!(inner.as_ref(), Type::Array(_)) => inner,
833            _ => self,
834        }
835    }
836
837    /// Whether this is the type of an array that holds no element at any
838    /// depth: `never[]`, `never[][]`, as an empty literal is typed.
839    pub fn holds_no_element(&self) -> bool {
840        match self.peel() {
841            Type::Array(element) => {
842                matches!(element.peel(), Type::Never) || element.holds_no_element()
843            }
844            _ => false,
845        }
846    }
847
848    /// Whether this is a union of only arrays and tuples, which share the `$Array`
849    /// representation.
850    pub fn is_array_like_union(&self) -> bool {
851        match self.peel() {
852            Type::Union(members) => members
853                .iter()
854                .all(|member| matches!(member.peel(), Type::Array(_) | Type::Tuple(_))),
855            _ => false,
856        }
857    }
858
859    /// The element of a union of arrays and tuples read as one array: any
860    /// member's element. `None` for any other type. Only reads may go through
861    /// it; writing a member's element through the joined type could store
862    /// another member's element type.
863    pub fn array_like_union_element(&self) -> Option<Type> {
864        let Type::Union(members) = self.peel() else {
865            return None;
866        };
867        let elements = members
868            .iter()
869            .map(|member| match member.peel() {
870                Type::Array(element) => Some((**element).clone()),
871                Type::Tuple(positions) => Some(Type::union(positions.elements.clone())),
872                _ => None,
873            })
874            .collect::<Option<Vec<_>>>()?;
875        Some(Type::union(elements))
876    }
877
878    /// The arrays and tuples of a union of strings with arrays or tuples, and
879    /// nothing else, as their own union. Such a union has no shared
880    /// representation, so each use tests `typeof` and takes the string's or the
881    /// array's path.
882    pub fn string_or_array_union_arrays(&self) -> Option<Type> {
883        let Type::Union(members) = self.peel() else {
884            return None;
885        };
886        let (strings, arrays): (Vec<Type>, Vec<Type>) = members
887            .iter()
888            .cloned()
889            .partition(|member| member.peel().is_string_shaped());
890        let all_arrays = arrays
891            .iter()
892            .all(|member| matches!(member.peel(), Type::Array(_) | Type::Tuple(_)));
893        (!strings.is_empty() && !arrays.is_empty() && all_arrays).then(|| Type::union(arrays))
894    }
895
896    /// The array a union of arrays and tuples reads as: see
897    /// [`Self::array_like_union_element`].
898    pub fn array_like_union_view(&self) -> Option<Type> {
899        self.array_like_union_element()
900            .map(|element| Type::Array(Box::new(element)))
901    }
902
903    /// Whether this type is `void`, through any depth of alias.
904    ///
905    /// `void` is the one type with no value slot at all, so a gate that tests
906    /// for it decides between "one Wasm result" and "none". Every such gate —
907    /// in the typechecker *and* in codegen — must answer the same way for
908    /// `type V = void` as for `void`: a bare `matches!(ty, Type::Void)` hands an
909    /// aliased `void` a value slot that
910    /// [`SymbolTable::value_type`](crate::codegen::symbol_table::SymbolTable::value_type)
911    /// then refuses to lower. A typecheck gate must not start peeling ahead of
912    /// its codegen counterpart, which turns a clean diagnostic into a panic.
913    pub fn is_void(&self) -> bool {
914        matches!(self.peel(), Type::Void)
915    }
916
917    /// Whether a function taking `actual` parameters can stand where one taking
918    /// `expected` is called. As in TypeScript, it may declare fewer and ignore
919    /// the rest of the arguments; a closure adapter drops them at runtime. Rest
920    /// functions keep an exact arity, since their packed array has a slot of its
921    /// own.
922    pub fn function_arity_fits(actual: usize, expected: usize, has_rest: bool) -> bool {
923        actual == expected || (!has_rest && actual < expected)
924    }
925
926    /// Whether a value of this type would need a `void` slot at runtime:
927    /// `void` itself, or a union that lists it.
928    ///
929    /// Deliberately one level deep — it does **not** descend into a function's
930    /// return type, where `void` is legitimate (`() => void`). Every gate that
931    /// refuses `void` in a value or comparison position wants this, not the
932    /// bare [`is_void`](Self::is_void). `cond ? f() : 1` and `a ?? f()` are
933    /// plain `void`, so no expression builds the union form; a gate that asks
934    /// this still refuses one that did, rather than letting it reach codegen.
935    pub fn carries_void(&self) -> bool {
936        match self.peel() {
937            Type::Void => true,
938            Type::Union(members) => members.iter().any(|m| matches!(m.peel(), Type::Void)),
939            _ => false,
940        }
941    }
942
943    /// Primitive operations shared by enums and homogeneous literal unions,
944    /// without widening their type identity or changing assignability.
945    pub fn primitive_behavior(&self) -> &Type {
946        match self.peel() {
947            Type::NumberEnum { .. } => &Type::Number,
948            Type::StringEnum { .. } => &Type::String,
949            Type::BooleanLiteral(_) => &Type::Boolean,
950            Type::BigIntLiteral(_) => &Type::BigInt,
951            Type::Union(members)
952                if !members.is_empty()
953                    && members.iter().all(|member| {
954                        matches!(
955                            member.primitive_behavior(),
956                            Type::Number | Type::NumberLiteral(_)
957                        )
958                    }) =>
959            {
960                &Type::Number
961            }
962            Type::Union(members)
963                if !members.is_empty()
964                    && members
965                        .iter()
966                        .all(|member| matches!(member.primitive_behavior(), Type::BigInt)) =>
967            {
968                &Type::BigInt
969            }
970            Type::Union(members)
971                if !members.is_empty() && members.iter().all(Type::is_string_shaped) =>
972            {
973                &Type::String
974            }
975            ty => ty,
976        }
977    }
978
979    /// Whether every value of this type is a bigint: `bigint`, a bigint literal
980    /// type, or a union or alias of those.
981    pub fn is_bigint(&self) -> bool {
982        matches!(self.primitive_behavior(), Type::BigInt)
983    }
984
985    /// This type with literal types replaced by the primitive they are a literal of.
986    ///
987    /// A literal type is only sound where the value cannot change, so inference keeps
988    /// it at a `const` binding and widens here at every position that is mutable or
989    /// whose type is inferred from its contents — a `let` binding, an array element,
990    /// an object-literal property, a generic argument. `const a = 1` is `1`, but
991    /// `let b = a` is `number`, matching TypeScript.
992    ///
993    /// An enum member literal type `E.A` widens to its enum `E`, not to the
994    /// enum's primitive: the enum is a nominal type whose identity its members
995    /// are checked against. Use [`primitive_behavior`](Self::primitive_behavior)
996    /// for the primitive.
997    pub fn widen_literal(&self) -> Type {
998        match self {
999            Type::NumberLiteral(_) => Type::Number,
1000            Type::StringLiteral(_) => Type::String,
1001            Type::BooleanLiteral(_) => Type::Boolean,
1002            Type::BigIntLiteral(_) => Type::BigInt,
1003            _ if self.is_enum_member() => self.without_enum_member(),
1004            // A union widens memberwise, which also collapses it when the members
1005            // share a base: `1 | 2` is `number`, not `number | number`, because
1006            // `Type::union` deduplicates.
1007            Type::Union(members) => Type::union(without_members_of_empty_object(
1008                members.iter().map(Type::widen_literal).collect(),
1009            )),
1010            _ => self.clone(),
1011        }
1012    }
1013
1014    /// The primitive this literal type, or union of literals of one
1015    /// primitive, is a literal of: `number` for `1 | 2`, `None` for
1016    /// `1 | "a"` or a type that is no literal.
1017    pub fn literal_base(&self) -> Option<Type> {
1018        let widened = self.widen_literal();
1019        (widened != *self && matches!(widened, Type::Number | Type::String | Type::Boolean))
1020            .then_some(widened)
1021    }
1022
1023    /// Whether every part of this type is a string — a `string`, a
1024    /// string-literal type, or a union of those.
1025    ///
1026    /// The typechecker, the import collector, and codegen must agree on this
1027    /// exactly: a shape accepted by one and not the others emits a `$string`
1028    /// into an `f64` slot, which fails Wasm validation rather than type
1029    /// checking. Contrast [`contains_string`](Self::contains_string), which
1030    /// asks the `any` question.
1031    pub fn is_string_shaped(&self) -> bool {
1032        match self.peel() {
1033            Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => true,
1034            Type::Union(members) => members.iter().all(Type::is_string_shaped),
1035            _ => false,
1036        }
1037    }
1038
1039    /// Whether *some* part of this type is a string. Diagnostics-only: it is
1040    /// what decides whether a rejected operand still deserves the "convert
1041    /// first" help, so `string | null` gets it.
1042    pub fn contains_string(&self) -> bool {
1043        match self.peel() {
1044            Type::String | Type::StringLiteral(_) | Type::StringEnum { .. } => true,
1045            Type::Union(members) => members.iter().any(Type::contains_string),
1046            _ => false,
1047        }
1048    }
1049
1050    /// Whether any member, through unions, satisfies `leaf`, which sees peeled
1051    /// types. Syntactic: an alias reference is a leaf, not expanded; the
1052    /// typechecker's `any_resolved_member` follows alias bodies through its
1053    /// resolver.
1054    pub fn any_member(&self, leaf: &dyn Fn(&Type) -> bool) -> bool {
1055        match self.peel() {
1056            Type::Union(members) => members.iter().any(|member| member.any_member(leaf)),
1057            member => leaf(member),
1058        }
1059    }
1060
1061    /// Whether every member, through unions, satisfies `leaf`. Syntactic,
1062    /// like [`Type::any_member`].
1063    pub fn all_members(&self, leaf: &dyn Fn(&Type) -> bool) -> bool {
1064        !self.any_member(&|member| !leaf(member))
1065    }
1066
1067    /// Whether `null` is spelled among the members. Syntactic, unlike the
1068    /// typechecker's `type_admits_null`: `unknown` doesn't count.
1069    pub fn spells_null(&self) -> bool {
1070        self.any_member(&|member| matches!(member, Type::Null))
1071    }
1072
1073    /// Whether `undefined`, or an erased `void` result, is spelled among the
1074    /// members. `unknown` and type parameters don't count.
1075    pub fn spells_undefined(&self) -> bool {
1076        self.any_member(&|member| matches!(member, Type::Undefined | Type::Void))
1077    }
1078
1079    /// Whether a value of the member type `leaf` has no JSON text at the
1080    /// document root, so `JSON.stringify` returns `undefined` for it.
1081    /// Objects and arrays holding such values still produce a document.
1082    pub fn stringifies_to_undefined(leaf: &Type) -> bool {
1083        // `{}` may hold a function, as `unknown` may.
1084        matches!(
1085            leaf.lowered(),
1086            Type::Undefined
1087                | Type::Void
1088                | Type::Unknown
1089                | Type::GenericParam { .. }
1090                | Type::TypeVar(_)
1091                | Type::Function { .. }
1092        )
1093    }
1094
1095    /// Normalizes to a flat member list that is unique *by peeled type*.
1096    ///
1097    /// Both halves peel because a nominal key is unsound here: an alias whose
1098    /// body is a union is a nested union in disguise, and every downstream
1099    /// per-member probe (`value_type`'s nullability test, narrowing's member
1100    /// walk) reads it as one opaque member. Deduping nominally leaves
1101    /// `N | number` as a two-member union that no operator is defined on.
1102    ///
1103    /// The alias label a union displays therefore survives only where it is
1104    /// still a single member: an alias of a union is replaced by its members,
1105    /// and among duplicates the alias-labelled spelling wins regardless of
1106    /// source order, so `N | number` and `number | N` both keep `N`.
1107    ///
1108    /// `peel` stops at a *recursive* alias (it is a name, not a body), so a
1109    /// recursive alias never dedups against its own expansion. Both spellings
1110    /// lower the same way, so this costs a redundant member, not correctness.
1111    pub fn union(members: Vec<Type>) -> Type {
1112        if members.iter().any(|m| matches!(m.peel(), Type::Error)) {
1113            return Type::Error;
1114        }
1115        // `unknown | T` → `unknown`; peels aliases so `type Dyn = unknown` still collapses.
1116        if members.iter().any(|m| matches!(m.peel(), Type::Unknown)) {
1117            return Type::Unknown;
1118        }
1119        // `T | never` → `T`. After Error/Unknown checks so cascading-silence wins.
1120        let members: Vec<Type> = members
1121            .into_iter()
1122            .filter(|m| !matches!(m.peel(), Type::Never))
1123            .collect();
1124        let mut flat: Vec<Type> = Vec::new();
1125        for m in members {
1126            match m.without_aliases() {
1127                Type::Union(inner) => flat.extend(inner.iter().cloned()),
1128                _ => flat.push(m),
1129            }
1130        }
1131        // Sorting by the peeled key groups alias-labelled duplicates next to
1132        // their body; aliases rank 0 so they sort first within the group, and
1133        // `dedup_by` keeps the earlier element — the name the user wrote.
1134        let alias_rank = |t: &Type| u8::from(!matches!(t, Type::Alias { .. }));
1135        flat.sort_by(|a, b| {
1136            a.without_aliases()
1137                .cmp(b.without_aliases())
1138                .then_with(|| alias_rank(a).cmp(&alias_rank(b)))
1139        });
1140        flat.dedup_by(|a, b| a.without_aliases() == b.without_aliases());
1141        fold_boolean_literals(&mut flat);
1142        fold_enum_members(&mut flat);
1143        // `T | (T & {})` holds `T`'s values: the refinement adds nothing.
1144        let refines_a_member = |member: &Type| {
1145            matches!(member, Type::Refined { original, .. }
1146                if flat.iter().any(|other| other.without_aliases() == original.without_aliases()))
1147        };
1148        let kept: Vec<Type> = flat
1149            .iter()
1150            .filter(|member| !refines_a_member(member))
1151            .cloned()
1152            .collect();
1153        flat = kept;
1154        // `object | {}` holds `{}`'s values: `object` adds nothing.
1155        if flat.iter().any(Type::is_empty_object) {
1156            flat.retain(|member| !matches!(member.peel(), Type::NonPrimitive));
1157        }
1158        if flat.len() > 1 {
1159            return Type::Union(flat);
1160        }
1161        match flat.into_iter().next() {
1162            Some(member) => member,
1163            None => Type::Never,
1164        }
1165    }
1166
1167    /// The distinct primitive receiver types of a union, if every member is
1168    /// primitive. Literal members share their base type's methods.
1169    pub(crate) fn primitive_union_members(&self) -> Option<Vec<Type>> {
1170        let Type::Union(members) = self.peel() else {
1171            return None;
1172        };
1173        let mut primitives = std::collections::BTreeSet::new();
1174        for member in members {
1175            primitives.insert(match member.primitive_behavior() {
1176                Type::Number | Type::NumberLiteral(_) => Type::Number,
1177                Type::String | Type::StringLiteral(_) => Type::String,
1178                Type::Boolean => Type::Boolean,
1179                Type::BigInt => Type::BigInt,
1180                _ => return None,
1181            });
1182        }
1183        (!primitives.is_empty()).then(|| primitives.into_iter().collect())
1184    }
1185
1186    /// For method dispatch. Erased generics route to `Object`. Aliases peel. Returns `None`
1187    /// for function, void, null, and error. The first tuple element is the owning package
1188    /// for diagnostics; the mangled name is the structural lookup key. Built-in
1189    /// interfaces (`Number`, `Array`, `Object`, …) live in the prelude package.
1190    pub fn interface_routing(&self) -> Option<(MangledName, &str, &str, Vec<Type>)> {
1191        let prelude = crate::mangle::PRELUDE_PACKAGE;
1192        match self.primitive_behavior() {
1193            // A literal type routes to its base's interface: `"abc".at(0)` resolves the
1194            // same members a `string` receiver does. The prelude `Number`/`String`
1195            // interfaces expose no mutating members, so routing a literal there cannot
1196            // invalidate it.
1197            Type::Number | Type::NumberLiteral(_) => Some((
1198                crate::mangle::prelude("Number"),
1199                prelude,
1200                "Number",
1201                Vec::new(),
1202            )),
1203            Type::BigInt => Some((
1204                crate::mangle::prelude("BigInt"),
1205                prelude,
1206                "BigInt",
1207                Vec::new(),
1208            )),
1209            Type::Boolean => Some((
1210                crate::mangle::prelude("Boolean"),
1211                prelude,
1212                "Boolean",
1213                Vec::new(),
1214            )),
1215            Type::String | Type::StringLiteral(_) => Some((
1216                crate::mangle::prelude("String"),
1217                prelude,
1218                "String",
1219                Vec::new(),
1220            )),
1221            Type::Uint8Array => Some((
1222                crate::mangle::prelude("Uint8Array"),
1223                prelude,
1224                "Uint8Array",
1225                Vec::new(),
1226            )),
1227            Type::Array(elem) => Some((
1228                crate::mangle::prelude("Array"),
1229                prelude,
1230                "Array",
1231                vec![(**elem).clone()],
1232            )),
1233            // Tuples are arrays at runtime; route to `Array` with the positions'
1234            // union as the element type so reads (`length`, `map`, `at`, …) flow
1235            // through. Mutating methods are rejected separately at the call site.
1236            Type::Tuple(elements) => Some((
1237                crate::mangle::prelude("Array"),
1238                prelude,
1239                "Array",
1240                vec![Type::union(elements.elements.clone())],
1241            )),
1242            Type::Object { .. }
1243            | Type::NonPrimitive
1244            | Type::TypeVar(_)
1245            | Type::GenericParam { .. } => Some((
1246                crate::mangle::prelude("Object"),
1247                prelude,
1248                "Object",
1249                Vec::new(),
1250            )),
1251            Type::InterfaceRef {
1252                mangled,
1253                package,
1254                name,
1255                args,
1256                ..
1257            }
1258            | Type::ClassRef {
1259                mangled,
1260                package,
1261                name,
1262                args,
1263                ..
1264            } => Some((
1265                mangled.clone(),
1266                package.as_str(),
1267                name.as_str(),
1268                args.clone(),
1269            )),
1270            // Enum values route to Object (not Number/String) to keep the API surface minimal.
1271            Type::NumberEnum { .. } | Type::StringEnum { .. } => Some((
1272                crate::mangle::prelude("Object"),
1273                prelude,
1274                "Object",
1275                Vec::new(),
1276            )),
1277            // `unknown` routes to Object so vtable methods work.
1278            Type::Unknown => Some((
1279                crate::mangle::prelude("Object"),
1280                prelude,
1281                "Object",
1282                Vec::new(),
1283            )),
1284            _ => None,
1285        }
1286    }
1287}
1288
1289/// `ty` as written beside a `?` marker, which already admits `undefined`: without
1290/// its `undefined` member. A type that is only `undefined` stays as it is.
1291/// Every renderer of optional parameters shows them through this, except
1292/// `type_rendering`'s `Frame::Optional`, which applies the same rule to
1293/// borrowed members.
1294pub(crate) fn shown_beside_optional_marker(ty: &Type) -> Type {
1295    match ty.peel() {
1296        Type::Union(members) => Type::union(
1297            members
1298                .iter()
1299                .filter(|member| !matches!(member.peel(), Type::Undefined))
1300                .cloned()
1301                .collect(),
1302        ),
1303        _ => ty.clone(),
1304    }
1305}
1306
1307/// `s` escaped for a double-quoted string the way `tsc` prints a string literal
1308/// type (`escapeString` in TypeScript's `utilities.ts`): `"G\"HI"`, `"a\nb"`.
1309pub(crate) fn escape_string_literal(s: &str) -> String {
1310    let mut out = String::with_capacity(s.len());
1311    let mut chars = crate::literal_units::literal_chars(s).peekable();
1312    while let Some(c) = chars.next() {
1313        let c = match c {
1314            Ok(c) => c,
1315            Err(lone) => {
1316                out.push_str(&format!("\\u{lone:04X}"));
1317                continue;
1318            }
1319        };
1320        match c {
1321            '"' => out.push_str("\\\""),
1322            '\\' => out.push_str("\\\\"),
1323            '\n' => out.push_str("\\n"),
1324            '\r' => out.push_str("\\r"),
1325            '\t' => out.push_str("\\t"),
1326            '\u{8}' => out.push_str("\\b"),
1327            '\u{b}' => out.push_str("\\v"),
1328            '\u{c}' => out.push_str("\\f"),
1329            // `\0` before a digit would read as an octal escape.
1330            '\0' if chars
1331                .peek()
1332                .is_some_and(|next| next.is_ok_and(|d| d.is_ascii_digit())) =>
1333            {
1334                out.push_str("\\x00");
1335            }
1336            '\0' => out.push_str("\\0"),
1337            '\u{0}'..='\u{1f}' | '\u{85}' | '\u{2028}' | '\u{2029}' => {
1338                out.push_str(&format!("\\u{:04X}", u32::from(c)));
1339            }
1340            c => out.push(c),
1341        }
1342    }
1343    out
1344}
1345
1346/// The [`Type::BigIntLiteral`] of a bigint literal's decimal digits, as the
1347/// lexer writes them.
1348pub fn bigint_literal_type(digits: &str) -> Type {
1349    Type::BigIntLiteral(canonical_bigint_digits(digits))
1350}
1351
1352/// `digits` negated, in [`Type::BigIntLiteral`]'s spelling: `-0n` is `0n`.
1353pub fn negate_bigint_digits(digits: &str) -> String {
1354    let canonical = canonical_bigint_digits(digits);
1355    if canonical == "0" {
1356        return canonical;
1357    }
1358    match canonical.strip_prefix('-') {
1359        Some(magnitude) => magnitude.to_string(),
1360        None => format!("-{canonical}"),
1361    }
1362}
1363
1364/// `digits` without leading zeros, keeping a sign, and `0` for zero.
1365fn canonical_bigint_digits(digits: &str) -> String {
1366    let (sign, magnitude) = match digits.strip_prefix('-') {
1367        Some(magnitude) => ("-", magnitude),
1368        None => ("", digits),
1369    };
1370    match magnitude.trim_start_matches('0') {
1371        "" => "0".to_string(),
1372        trimmed => format!("{sign}{trimmed}"),
1373    }
1374}
1375
1376/// Drops the members a `{}` among `members` already holds: every one that
1377/// cannot be `null` or `undefined` and is no bare type parameter. TypeScript
1378/// reduces a union this way when it widens a literal, joins flow types and
1379/// types `a ?? b`.
1380pub fn without_members_of_empty_object(members: Vec<Type>) -> Vec<Type> {
1381    if !members.iter().any(Type::is_empty_object) {
1382        return members;
1383    }
1384    members
1385        .into_iter()
1386        .filter(|member| member.is_empty_object() || may_hold_values_outside_empty_object(member))
1387        .collect()
1388}
1389
1390/// Whether `ty` may hold a value `{}` does not: `null` or `undefined` (as
1391/// `T & ({} | undefined)` may), or any value of a type parameter.
1392fn may_hold_values_outside_empty_object(ty: &Type) -> bool {
1393    match ty.peel() {
1394        Type::Null
1395        | Type::Undefined
1396        | Type::Void
1397        | Type::Unknown
1398        | Type::GenericParam { .. }
1399        | Type::TypeVar(_) => true,
1400        Type::Union(members) => members.iter().any(may_hold_values_outside_empty_object),
1401        _ => false,
1402    }
1403}
1404
1405/// `boolean` is `true | false`: a union holding both literals, or `boolean`
1406/// and either literal, holds exactly `boolean`. `members` is sorted and
1407/// deduplicated, and stays so.
1408fn fold_boolean_literals(members: &mut Vec<Type>) {
1409    let is_boolean = |m: &Type| matches!(m.without_aliases(), Type::Boolean);
1410    let is_literal = |m: &Type| matches!(m.without_aliases(), Type::BooleanLiteral(_));
1411    let literals = members.iter().filter(|m| is_literal(m)).count();
1412    let has_boolean = members.iter().any(is_boolean);
1413    if literals == 0 || (literals == 1 && !has_boolean) {
1414        return;
1415    }
1416    members.retain(|m| !is_literal(m));
1417    if !has_boolean {
1418        let at = members.partition_point(|m| m.without_aliases() < &Type::Boolean);
1419        members.insert(at, Type::Boolean);
1420    }
1421}
1422
1423/// Reduces enum types as TypeScript does: a member beside its enum is absorbed
1424/// by it, members naming every member of their enum are the enum, and an enum
1425/// or member beside its primitive (`E | number`) is absorbed by the primitive.
1426/// Leaves `members` sorted and deduplicated.
1427fn fold_enum_members(members: &mut Vec<Type>) {
1428    absorb_enums_into_primitives(members);
1429    let complete = complete_enums(members);
1430    if complete.is_empty() {
1431        return;
1432    }
1433    for member in members.iter_mut() {
1434        if let Type::NumberEnum { mangled, .. } | Type::StringEnum { mangled, .. } =
1435            member.without_aliases()
1436            && complete.contains(mangled)
1437        {
1438            *member = member.without_aliases().without_enum_member();
1439        }
1440    }
1441    members.sort_by(|a, b| a.without_aliases().cmp(b.without_aliases()));
1442    members.dedup_by(|a, b| a.without_aliases() == b.without_aliases());
1443}
1444
1445/// Drops every enum type and member beside the primitive it holds.
1446fn absorb_enums_into_primitives(members: &mut Vec<Type>) {
1447    let has = |base: &Type| members.iter().any(|m| m.without_aliases() == base);
1448    let (has_number, has_string) = (has(&Type::Number), has(&Type::String));
1449    members.retain(|m| match m.without_aliases() {
1450        Type::NumberEnum { .. } => !has_number,
1451        Type::StringEnum { .. } => !has_string,
1452        _ => true,
1453    });
1454}
1455
1456/// The enums `members` holds whole: as the enum itself, or by naming every
1457/// one of its members.
1458fn complete_enums(members: &[Type]) -> BTreeSet<MangledName> {
1459    struct NamedMembers<'a> {
1460        member_count: usize,
1461        names: BTreeSet<&'a str>,
1462    }
1463    let mut named: BTreeMap<&MangledName, NamedMembers> = BTreeMap::new();
1464    let mut whole: BTreeSet<&MangledName> = BTreeSet::new();
1465    for member in members {
1466        let ty = member.without_aliases();
1467        if let Some((mangled, name, member_count)) = ty.enum_member_name() {
1468            named
1469                .entry(mangled)
1470                .or_insert_with(|| NamedMembers {
1471                    member_count,
1472                    names: BTreeSet::new(),
1473                })
1474                .names
1475                .insert(name);
1476        } else if let Type::NumberEnum { mangled, .. } | Type::StringEnum { mangled, .. } = ty {
1477            whole.insert(mangled);
1478        }
1479    }
1480    named
1481        .into_iter()
1482        .filter(|(mangled, named)| {
1483            whole.contains(mangled) || named.names.len() >= named.member_count
1484        })
1485        .map(|(mangled, _)| mangled.clone())
1486        .collect()
1487}
1488
1489impl Type {
1490    pub fn render_checked(
1491        &self,
1492        limits: crate::rendering::RenderLimits,
1493    ) -> Result<crate::rendering::RenderedText, crate::rendering::RenderError> {
1494        crate::type_rendering::render(self, limits)
1495    }
1496}
1497
1498/// A union's members in the order they are printed. Members are stored in
1499/// canonical order, which puts `null` and `undefined` first; they print last,
1500/// as TypeScript prints them: `string | null | undefined`.
1501pub fn union_display_order(members: &[Type]) -> Vec<&Type> {
1502    let mut ordered: Vec<&Type> = members.iter().collect();
1503    ordered.sort_by_key(|m| match m {
1504        Type::Null => 1,
1505        Type::Undefined => 2,
1506        _ => 0,
1507    });
1508    ordered
1509}
1510
1511impl fmt::Display for Type {
1512    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1513        match self.render_checked(crate::rendering::RenderLimits::default()) {
1514            Ok(rendered) => f.write_str(&rendered.text),
1515            Err(_) => f.write_str("[diagnostic type unavailable]"),
1516        }
1517    }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522    use super::{Package, Type};
1523
1524    #[test]
1525    fn tuple_mapping_preserves_optional_positions() {
1526        let tuple = super::TupleType {
1527            elements: vec![Type::NumberLiteral(super::LiteralF64(1.0)), Type::Undefined],
1528            optional: 1,
1529        };
1530        let mapped = tuple.map(Type::widen_literal);
1531        assert_eq!(mapped.optional, 1);
1532        assert_eq!(mapped.required_len(), 1);
1533        assert_eq!(mapped.elements, vec![Type::Number, Type::Undefined]);
1534    }
1535
1536    #[test]
1537    fn optional_fields_read_as_undefined() {
1538        let field = super::ObjectField::optional(Type::String);
1539        assert_eq!(
1540            field.read_ty(),
1541            Type::union(vec![Type::String, Type::Undefined])
1542        );
1543    }
1544
1545    #[test]
1546    fn optional_function_and_tuple_display_preserve_omission() {
1547        let ty = Type::Function {
1548            params: vec![Type::union(vec![Type::String, Type::Undefined])],
1549            optional: 1,
1550            ret: Box::new(Type::Void),
1551            predicate: None,
1552            has_rest: false,
1553        };
1554        // An optional parameter admits `undefined` already, as `tsc` prints it.
1555        assert_eq!(ty.to_string(), "(arg0?: string) => void");
1556        let tuple = Type::Tuple(super::TupleType {
1557            elements: vec![
1558                Type::Number,
1559                Type::union(vec![Type::String, Type::Undefined]),
1560            ],
1561            optional: 1,
1562        });
1563        assert_eq!(tuple.to_string(), "[number, (string | undefined)?]");
1564    }
1565
1566    #[test]
1567    fn atomic_display() {
1568        assert_eq!(Type::Number.to_string(), "number");
1569        assert_eq!(Type::String.to_string(), "string");
1570        assert_eq!(Type::Boolean.to_string(), "boolean");
1571        assert_eq!(Type::Null.to_string(), "null");
1572        assert_eq!(Type::Void.to_string(), "void");
1573        assert_eq!(Type::Error.to_string(), "<error>");
1574    }
1575
1576    #[test]
1577    fn readonly_display_matches_typescript() {
1578        let numbers = Type::Array(Box::new(Type::Number));
1579        let readonly = Type::Readonly(Box::new(numbers.clone()));
1580        assert_eq!(readonly.to_string(), "readonly number[]");
1581        assert_eq!(
1582            Type::Array(Box::new(readonly.clone())).to_string(),
1583            "(readonly number[])[]"
1584        );
1585        assert_eq!(
1586            Type::Readonly(Box::new(Type::Array(Box::new(numbers)))).to_string(),
1587            "readonly number[][]"
1588        );
1589        assert_eq!(
1590            Type::Readonly(Box::new(Type::Tuple(
1591                vec![Type::Number, Type::String].into()
1592            )))
1593            .to_string(),
1594            "readonly [number, string]"
1595        );
1596        assert!(readonly.is_readonly_array());
1597        assert_eq!(readonly.peel(), &Type::Array(Box::new(Type::Number)));
1598    }
1599
1600    #[test]
1601    fn string_literal_display_escapes_like_typescript() {
1602        // Each expectation is what `tsc` 5.9 prints for the same literal type.
1603        let cases = [
1604            ("G\"HI\\", r#""G\"HI\\""#),
1605            ("a\nb\r\t", r#""a\nb\r\t""#),
1606            ("\u{8}\u{b}\u{c}", r#""\b\v\f""#),
1607            ("\0x", r#""\0x""#),
1608            ("\u{0}1", r#""\x001""#),
1609            ("\u{7}\u{1b}", r#""\u0007\u001B""#),
1610            ("\u{85}\u{2028}\u{2029}", r#""\u0085\u2028\u2029""#),
1611            ("\u{7f}é", "\"\u{7f}é\""),
1612        ];
1613        for (value, printed) in cases {
1614            assert_eq!(Type::StringLiteral(value.into()).to_string(), printed);
1615        }
1616    }
1617
1618    #[test]
1619    fn structural_equality() {
1620        assert_eq!(Type::Number, Type::Number);
1621        assert_ne!(Type::Number, Type::String);
1622
1623        let a = Type::Function {
1624            params: vec![Type::Number, Type::String],
1625            ret: Box::new(Type::Boolean),
1626            predicate: None,
1627            has_rest: false,
1628            optional: 0,
1629        };
1630        let b = Type::Function {
1631            params: vec![Type::Number, Type::String],
1632            ret: Box::new(Type::Boolean),
1633            predicate: None,
1634            has_rest: false,
1635            optional: 0,
1636        };
1637        let c = Type::Function {
1638            params: vec![Type::Number],
1639            ret: Box::new(Type::Boolean),
1640            predicate: None,
1641            has_rest: false,
1642            optional: 0,
1643        };
1644        assert_eq!(a, b);
1645        assert_ne!(a, c);
1646    }
1647
1648    #[test]
1649    fn function_display_no_params() {
1650        let t = Type::Function {
1651            params: vec![],
1652            ret: Box::new(Type::Number),
1653            predicate: None,
1654            has_rest: false,
1655            optional: 0,
1656        };
1657        assert_eq!(t.to_string(), "() => number");
1658    }
1659
1660    #[test]
1661    fn function_display_one_param() {
1662        let t = Type::Function {
1663            params: vec![Type::Number],
1664            ret: Box::new(Type::Boolean),
1665            predicate: None,
1666            has_rest: false,
1667            optional: 0,
1668        };
1669        assert_eq!(t.to_string(), "(arg0: number) => boolean");
1670    }
1671
1672    #[test]
1673    fn function_display_multi_param() {
1674        let t = Type::Function {
1675            params: vec![Type::Number, Type::String],
1676            ret: Box::new(Type::Void),
1677            predicate: None,
1678            has_rest: false,
1679            optional: 0,
1680        };
1681        assert_eq!(t.to_string(), "(arg0: number, arg1: string) => void");
1682    }
1683
1684    #[test]
1685    fn function_display_nested() {
1686        let inner = Type::Function {
1687            params: vec![],
1688            ret: Box::new(Type::Number),
1689            predicate: None,
1690            has_rest: false,
1691            optional: 0,
1692        };
1693        let outer = Type::Function {
1694            params: vec![],
1695            ret: Box::new(inner),
1696            predicate: None,
1697            has_rest: false,
1698            optional: 0,
1699        };
1700        assert_eq!(outer.to_string(), "() => () => number");
1701    }
1702
1703    #[test]
1704    fn clone_roundtrip() {
1705        let t = Type::Function {
1706            params: vec![Type::Number, Type::String],
1707            ret: Box::new(Type::Boolean),
1708            predicate: None,
1709            has_rest: false,
1710            optional: 0,
1711        };
1712        assert_eq!(t.clone(), t);
1713    }
1714
1715    fn point_type() -> Type {
1716        let mut fields = std::collections::BTreeMap::new();
1717        fields.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1718        fields.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1719        Type::Object {
1720            index: None,
1721            fields,
1722        }
1723    }
1724
1725    #[test]
1726    fn object_structural_equality_ignores_field_insertion_order() {
1727        let mut a = std::collections::BTreeMap::new();
1728        a.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1729        a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1730
1731        let mut b = std::collections::BTreeMap::new();
1732        b.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1733        b.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1734
1735        assert_eq!(
1736            Type::Object {
1737                index: None,
1738                fields: a
1739            },
1740            Type::Object {
1741                index: None,
1742                fields: b
1743            }
1744        );
1745    }
1746
1747    #[test]
1748    fn object_inequality_on_different_field_set() {
1749        let mut a = std::collections::BTreeMap::new();
1750        a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1751        let mut b = std::collections::BTreeMap::new();
1752        b.insert("y".to_string(), crate::ObjectField::required(Type::Number));
1753        assert_ne!(
1754            Type::Object {
1755                index: None,
1756                fields: a
1757            },
1758            Type::Object {
1759                index: None,
1760                fields: b
1761            }
1762        );
1763    }
1764
1765    #[test]
1766    fn object_display() {
1767        assert_eq!(point_type().to_string(), "{ x: number; y: number }");
1768        let empty = Type::Object {
1769            index: None,
1770            fields: std::collections::BTreeMap::new(),
1771        };
1772        assert_eq!(empty.to_string(), "{}");
1773    }
1774
1775    #[test]
1776    fn object_display_with_optional_field() {
1777        let mut fields = std::collections::BTreeMap::new();
1778        fields.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1779        fields.insert("y".to_string(), crate::ObjectField::optional(Type::String));
1780        let t = Type::Object {
1781            index: None,
1782            fields,
1783        };
1784        assert_eq!(t.to_string(), "{ x: number; y?: string }");
1785    }
1786
1787    #[test]
1788    fn object_inequality_required_vs_optional() {
1789        let mut a = std::collections::BTreeMap::new();
1790        a.insert("x".to_string(), crate::ObjectField::required(Type::Number));
1791        let mut b = std::collections::BTreeMap::new();
1792        b.insert("x".to_string(), crate::ObjectField::optional(Type::Number));
1793        assert_ne!(
1794            Type::Object {
1795                index: None,
1796                fields: a
1797            },
1798            Type::Object {
1799                index: None,
1800                fields: b
1801            }
1802        );
1803    }
1804
1805    #[test]
1806    fn array_equality() {
1807        assert_eq!(
1808            Type::Array(Box::new(Type::Number)),
1809            Type::Array(Box::new(Type::Number))
1810        );
1811        assert_ne!(
1812            Type::Array(Box::new(Type::Number)),
1813            Type::Array(Box::new(Type::String))
1814        );
1815    }
1816
1817    #[test]
1818    fn array_display() {
1819        assert_eq!(Type::Array(Box::new(Type::Number)).to_string(), "number[]");
1820        assert_eq!(
1821            Type::Array(Box::new(Type::Array(Box::new(Type::Number)))).to_string(),
1822            "number[][]"
1823        );
1824    }
1825
1826    #[test]
1827    fn nested_object_in_array_display() {
1828        let arr = Type::Array(Box::new(point_type()));
1829        assert_eq!(arr.to_string(), "{ x: number; y: number }[]");
1830    }
1831
1832    #[test]
1833    fn type_var_display_uses_source_name() {
1834        assert_eq!(Type::TypeVar("T".to_string()).to_string(), "T");
1835        assert_eq!(Type::TypeVar("Key".to_string()).to_string(), "Key");
1836    }
1837
1838    #[test]
1839    fn type_var_structural_equality_by_name() {
1840        let t = Type::TypeVar("T".to_string());
1841        let t2 = Type::TypeVar("T".to_string());
1842        let u = Type::TypeVar("U".to_string());
1843        assert_eq!(t, t2);
1844        assert_ne!(t, u);
1845        assert_ne!(t, Type::Number);
1846    }
1847
1848    #[test]
1849    fn type_var_inside_array_and_function_displays_through() {
1850        let arr = Type::Array(Box::new(Type::TypeVar("T".to_string())));
1851        assert_eq!(arr.to_string(), "T[]");
1852        let func = Type::Function {
1853            params: vec![Type::TypeVar("T".to_string())],
1854            ret: Box::new(Type::TypeVar("U".to_string())),
1855            predicate: None,
1856            has_rest: false,
1857            optional: 0,
1858        };
1859        assert_eq!(func.to_string(), "(arg0: T) => U");
1860    }
1861
1862    #[test]
1863    fn generic_param_display_uses_name_not_id() {
1864        let gp = Type::GenericParam {
1865            id: 42,
1866            name: "T".to_string(),
1867        };
1868        assert_eq!(gp.to_string(), "T");
1869    }
1870
1871    #[test]
1872    fn generic_param_equality_by_id() {
1873        let a = Type::GenericParam {
1874            id: 5,
1875            name: "T".to_string(),
1876        };
1877        let b = Type::GenericParam {
1878            id: 5,
1879            name: "T".to_string(),
1880        };
1881        assert_eq!(a, b);
1882
1883        let c = Type::GenericParam {
1884            id: 6,
1885            name: "T".to_string(),
1886        };
1887        assert_ne!(a, c);
1888    }
1889
1890    #[test]
1891    fn union_canonical_dedup() {
1892        let t = Type::union(vec![Type::Number, Type::Null, Type::Number]);
1893        let Type::Union(members) = t else {
1894            panic!("expected Union, got {t:?}");
1895        };
1896        assert_eq!(members, vec![Type::Number, Type::Null]);
1897    }
1898
1899    #[test]
1900    fn union_canonical_order_independent() {
1901        let a = Type::union(vec![Type::Number, Type::Null]);
1902        let b = Type::union(vec![Type::Null, Type::Number]);
1903        assert_eq!(a, b);
1904    }
1905
1906    #[test]
1907    fn union_collapses_to_single_member() {
1908        let t = Type::union(vec![Type::Number, Type::Number]);
1909        assert_eq!(t, Type::Number);
1910    }
1911
1912    #[test]
1913    fn union_flattens_nested() {
1914        let inner = Type::union(vec![Type::String, Type::Null]);
1915        let outer = Type::union(vec![Type::Number, inner]);
1916        let expected = Type::union(vec![Type::Number, Type::String, Type::Null]);
1917        assert_eq!(outer, expected);
1918    }
1919
1920    #[test]
1921    fn union_error_member_collapses_to_error() {
1922        let t = Type::union(vec![Type::Number, Type::Error]);
1923        assert_eq!(t, Type::Error);
1924    }
1925
1926    #[test]
1927    fn union_display_basic() {
1928        let t = Type::union(vec![Type::Number, Type::String]);
1929        assert_eq!(t.to_string(), "number | string");
1930    }
1931
1932    #[test]
1933    fn union_display_with_null() {
1934        let t = Type::union(vec![Type::Number, Type::Null]);
1935        assert_eq!(t.to_string(), "number | null");
1936        let t2 = Type::union(vec![Type::Null, Type::Number]);
1937        assert_eq!(t2.to_string(), "number | null");
1938    }
1939
1940    #[test]
1941    fn union_display_wraps_function_member() {
1942        let fn_ty = Type::Function {
1943            params: vec![],
1944            ret: Box::new(Type::String),
1945            predicate: None,
1946            has_rest: false,
1947            optional: 0,
1948        };
1949        let t = Type::union(vec![fn_ty, Type::Boolean]);
1950        assert_eq!(t.to_string(), "boolean | (() => string)");
1951    }
1952
1953    use super::LiteralF64;
1954
1955    #[test]
1956    fn string_literal_display_is_quoted() {
1957        let t = Type::StringLiteral("north".to_string());
1958        assert_eq!(t.to_string(), "\"north\"");
1959    }
1960
1961    #[test]
1962    fn number_literal_display_is_unquoted_with_whole_number_shape() {
1963        let whole = Type::NumberLiteral(LiteralF64(42.0));
1964        assert_eq!(whole.to_string(), "42");
1965        let frac = Type::NumberLiteral(LiteralF64(4.5));
1966        assert_eq!(frac.to_string(), "4.5");
1967    }
1968
1969    #[test]
1970    fn string_literal_equality_by_value() {
1971        let a = Type::StringLiteral("hi".to_string());
1972        let b = Type::StringLiteral("hi".to_string());
1973        let c = Type::StringLiteral("bye".to_string());
1974        assert_eq!(a, b);
1975        assert_ne!(a, c);
1976    }
1977
1978    #[test]
1979    fn number_literal_equality_by_value() {
1980        let a = Type::NumberLiteral(LiteralF64(42.0));
1981        let b = Type::NumberLiteral(LiteralF64(42.0));
1982        let c = Type::NumberLiteral(LiteralF64(43.0));
1983        assert_eq!(a, b);
1984        assert_ne!(a, c);
1985    }
1986
1987    #[test]
1988    fn union_sorts_literals_next_to_base_types() {
1989        // NumberLiteral sorts after Number, StringLiteral after String per variant order.
1990        let t = Type::union(vec![
1991            Type::String,
1992            Type::StringLiteral("hi".to_string()),
1993            Type::Number,
1994            Type::NumberLiteral(LiteralF64(42.0)),
1995        ]);
1996        assert_eq!(t.to_string(), "number | 42 | string | \"hi\"");
1997    }
1998
1999    #[test]
2000    fn widen_literal_replaces_a_literal_with_its_base() {
2001        assert_eq!(
2002            Type::NumberLiteral(LiteralF64(1.0)).widen_literal(),
2003            Type::Number
2004        );
2005        assert_eq!(
2006            Type::StringLiteral("hi".to_string()).widen_literal(),
2007            Type::String
2008        );
2009    }
2010
2011    /// A union widens memberwise, and `Type::union` then deduplicates — so a union of
2012    /// literals over one base collapses to that base rather than repeating it.
2013    #[test]
2014    fn widen_literal_collapses_a_union_of_literals() {
2015        let t = Type::union(vec![
2016            Type::NumberLiteral(LiteralF64(1.0)),
2017            Type::NumberLiteral(LiteralF64(2.0)),
2018        ]);
2019        assert_eq!(t.widen_literal(), Type::Number);
2020    }
2021
2022    /// Enums are nominal, not literal: widening one would discard the identity its
2023    /// members are checked against. `primitive_behavior` is the enum-aware accessor.
2024    #[test]
2025    fn widen_literal_leaves_everything_else_alone() {
2026        assert_eq!(Type::Number.widen_literal(), Type::Number);
2027        assert_eq!(Type::Boolean.widen_literal(), Type::Boolean);
2028        let arr = Type::Array(Box::new(Type::NumberLiteral(LiteralF64(1.0))));
2029        assert_eq!(arr.widen_literal(), arr);
2030    }
2031
2032    #[test]
2033    fn union_of_two_string_literals_sorts_by_value() {
2034        let t = Type::union(vec![
2035            Type::StringLiteral("south".to_string()),
2036            Type::StringLiteral("north".to_string()),
2037        ]);
2038        assert_eq!(t.to_string(), "\"north\" | \"south\"");
2039    }
2040
2041    #[test]
2042    fn literal_f64_equality_distinguishes_negative_zero_at_bit_level() {
2043        // Bit-pattern equality distinguishes -0.0 and +0.0; parser canonicalizes -0.0→0.0 so
2044        // this case won't appear in practice.
2045        let pos = LiteralF64(0.0);
2046        let neg = LiteralF64(-0.0);
2047        assert_ne!(pos, neg);
2048    }
2049
2050    #[test]
2051    fn type_var_and_generic_param_are_distinct_types() {
2052        // TypeVar is signature form; GenericParam is body form.
2053        // Even with the same name they're different variants.
2054        let tv = Type::TypeVar("T".to_string());
2055        let gp = Type::GenericParam {
2056            id: 0,
2057            name: "T".to_string(),
2058        };
2059        assert_ne!(tv, gp);
2060    }
2061
2062    #[test]
2063    fn alias_display_renders_name_not_body() {
2064        let ty = user_alias("Circle", point_type());
2065        assert_eq!(ty.to_string(), "Circle");
2066    }
2067
2068    #[test]
2069    fn peel_walks_through_alias_to_underlying() {
2070        let inner = Type::Number;
2071        let aliased = user_alias("ID", inner.clone());
2072        assert_eq!(aliased.peel(), &inner);
2073    }
2074
2075    #[test]
2076    fn peel_is_idempotent_through_nested_aliases() {
2077        let nested = user_alias("A", user_alias("B", Type::Number));
2078        assert_eq!(nested.peel(), &Type::Number);
2079        assert_eq!(nested.peel().peel(), &Type::Number);
2080    }
2081
2082    #[test]
2083    fn alias_is_not_structurally_equal_to_underlying() {
2084        let aliased = user_alias("ID", Type::Number);
2085        assert_ne!(aliased, Type::Number);
2086        assert_eq!(aliased.peel(), &Type::Number);
2087    }
2088
2089    #[test]
2090    fn alias_routes_to_underlying_interface() {
2091        let aliased = user_alias("ID", Type::Number);
2092        let (_mangled, _pkg, iface, _args) = aliased.interface_routing().expect("number routes");
2093        assert_eq!(iface, "Number");
2094    }
2095
2096    #[test]
2097    fn union_of_error_alias_collapses_to_error() {
2098        let alias_err = user_alias("Bad", Type::Error);
2099        let t = Type::union(vec![Type::Number, alias_err]);
2100        assert_eq!(t, Type::Error);
2101    }
2102
2103    #[test]
2104    fn union_preserves_distinct_alias_members_for_display() {
2105        let circle = user_alias("Circle", point_type());
2106        let rect = user_alias("Rectangle", Type::Number);
2107        let t = Type::union(vec![circle, rect]);
2108        // Ordered by the peeled bodies, which puts `Number` before the object
2109        // shape — not by the order the members were written.
2110        assert_eq!(t.to_string(), "Rectangle | Circle");
2111    }
2112
2113    #[test]
2114    fn union_dedups_an_alias_against_its_own_body_keeping_the_label() {
2115        let n = user_alias("N", Type::Number);
2116        assert_eq!(Type::union(vec![n.clone(), Type::Number]), n);
2117        assert_eq!(Type::union(vec![Type::Number, n.clone()]), n);
2118    }
2119
2120    #[test]
2121    fn union_dedups_aliases_that_share_one_body() {
2122        let circle = user_alias("Circle", point_type());
2123        let rect = user_alias("Rectangle", point_type());
2124        assert_eq!(Type::union(vec![circle.clone(), rect]), circle);
2125    }
2126
2127    #[test]
2128    fn union_flattens_an_alias_whose_body_is_a_union() {
2129        let nullable = user_alias("MN", Type::union(vec![Type::Number, Type::Null]));
2130        assert_eq!(
2131            Type::union(vec![nullable, Type::Number]),
2132            Type::union(vec![Type::Number, Type::Null])
2133        );
2134    }
2135
2136    #[test]
2137    fn generic_alias_display_includes_args() {
2138        let ty = user_alias_args("Box", vec![Type::Number], Type::Number);
2139        assert_eq!(ty.to_string(), "Box<number>");
2140    }
2141
2142    fn iface(package: &str, name: &str) -> Type {
2143        Type::interface_ref(
2144            Package(package.to_string()),
2145            name,
2146            crate::mangle::package_symbol(package, name),
2147            Vec::new(),
2148        )
2149    }
2150
2151    fn user_alias(name: &str, ty: Type) -> Type {
2152        user_alias_args(name, Vec::new(), ty)
2153    }
2154
2155    fn user_alias_args(name: &str, args: Vec<Type>, ty: Type) -> Type {
2156        Type::alias_ty(
2157            Package::user(),
2158            name,
2159            crate::mangle::package_symbol(crate::mangle::USER_PACKAGE, name),
2160            args,
2161            Box::new(ty),
2162        )
2163    }
2164
2165    #[test]
2166    fn mangled_name_keys_type_identity() {
2167        // Identity is the mangled name. Same `(package, name)` ⇒ same mangled ⇒ equal;
2168        // a different owning package ⇒ different mangled ⇒ distinct (the multi-file
2169        // property: two same-named interfaces in different packages/modules are not
2170        // conflated).
2171        assert_eq!(iface("main", "Response"), iface("main", "Response"));
2172        let http = iface("submilli:http", "Response");
2173        let main = iface("main", "Response");
2174        assert_ne!(
2175            http, main,
2176            "different package ⇒ different mangled ⇒ distinct"
2177        );
2178        assert_ne!(
2179            http.cmp(&main),
2180            std::cmp::Ordering::Equal,
2181            "Ord must distinguish distinct mangled names"
2182        );
2183        assert_ne!(
2184            iface("main", "Response"),
2185            iface("main", "Other"),
2186            "different names stay distinct"
2187        );
2188    }
2189
2190    #[test]
2191    fn same_name_different_module_not_equal_nor_assignable() {
2192        // The core multi-file guarantee: a sibling module's `Logger`
2193        // (`mod:main#util#Logger`) is a distinct type from the root's `Logger`
2194        // (`main#Logger`), even though the bare name matches.
2195        let root = iface("main", "Logger");
2196        let sibling = Type::InterfaceRef {
2197            mangled: crate::mangle::package_module_symbol("main", "util", "Logger"),
2198            package: Package::user(),
2199            name: "Logger".to_string(),
2200            args: Vec::new(),
2201        };
2202        assert_ne!(root, sibling);
2203        // Same mangled ⇒ equal.
2204        let root2 = iface("main", "Logger");
2205        assert_eq!(root, root2);
2206    }
2207
2208    #[test]
2209    fn union_dedups_interface_refs_by_mangled() {
2210        // Same mangled collapses to one member; different mangled stays a 2-member union.
2211        let same = Type::union(vec![iface("main", "Response"), iface("main", "Response")]);
2212        assert!(
2213            !matches!(same, Type::Union(_)),
2214            "identical refs collapse to one member, got {same:?}"
2215        );
2216        let distinct = Type::union(vec![
2217            iface("submilli:http", "Response"),
2218            iface("main", "Response"),
2219        ]);
2220        assert!(
2221            matches!(distinct, Type::Union(ref ms) if ms.len() == 2),
2222            "distinct-package refs stay a 2-member union, got {distinct:?}"
2223        );
2224    }
2225
2226    #[test]
2227    fn generic_alias_display_multiple_args() {
2228        let ty = user_alias_args("Pair", vec![Type::String, Type::Number], Type::Number);
2229        assert_eq!(ty.to_string(), "Pair<string, number>");
2230    }
2231}