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