Skip to main content

lisette_syntax/
types.rs

1use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
2use std::borrow::Borrow;
3use std::cell::OnceCell;
4
5use ecow::EcoString;
6
7use crate::ast::Generic;
8
9/// Dot-qualified identifier for a named type, method, value, or variant.
10///
11/// Wraps the qualified name (`"main.Point.sum"`, `"prelude.Option"`,
12/// `"go:net/http.Handler"`) as a single `EcoString` and exposes structured
13/// accessors. Centralizes the join/split logic that used to live in ad-hoc
14/// `format!("{}.{}", ..)` and `split_once('.')` call sites.
15#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct Symbol(EcoString);
18
19impl Symbol {
20    /// Joins a module id and a local (possibly multi-segment) name.
21    ///
22    /// `Symbol::from_parts("main", "Point.sum")` → `"main.Point.sum"`.
23    pub fn from_parts(module: &str, local: &str) -> Self {
24        let mut s = String::with_capacity(module.len() + 1 + local.len());
25        s.push_str(module);
26        s.push('.');
27        s.push_str(local);
28        Self(EcoString::from(s))
29    }
30
31    /// Appends an additional dot-segment to an already-qualified symbol.
32    ///
33    /// `Symbol::from_raw("main.Shape").with_segment("Circle")` →
34    /// `"main.Shape.Circle"`.
35    pub fn with_segment(&self, segment: &str) -> Self {
36        let mut s = String::with_capacity(self.0.len() + 1 + segment.len());
37        s.push_str(&self.0);
38        s.push('.');
39        s.push_str(segment);
40        Self(EcoString::from(s))
41    }
42
43    /// Wraps an already-constructed qualified string. Prefer `from_parts`
44    /// when the module id and local name are available separately.
45    pub fn from_raw(qualified: impl Into<EcoString>) -> Self {
46        Self(qualified.into())
47    }
48
49    pub fn as_str(&self) -> &str {
50        &self.0
51    }
52
53    pub fn as_eco(&self) -> &EcoString {
54        &self.0
55    }
56
57    /// Last dot-separated segment. `"main.Point.sum"` → `"sum"`.
58    pub fn last_segment(&self) -> &str {
59        self.0.rsplit('.').next().unwrap_or(&self.0)
60    }
61
62    /// Strips the last dot-separated segment. `"main.Point.sum"` → `"main.Point"`.
63    /// Returns `None` if the symbol has no dot.
64    pub fn without_last_segment(&self) -> Option<&str> {
65        self.0.rsplit_once('.').map(|(rest, _)| rest)
66    }
67}
68
69impl Borrow<str> for Symbol {
70    fn borrow(&self) -> &str {
71        &self.0
72    }
73}
74
75impl AsRef<str> for Symbol {
76    fn as_ref(&self) -> &str {
77        &self.0
78    }
79}
80
81impl std::ops::Deref for Symbol {
82    type Target = str;
83
84    fn deref(&self) -> &str {
85        &self.0
86    }
87}
88
89impl From<&Symbol> for EcoString {
90    fn from(s: &Symbol) -> Self {
91        s.0.clone()
92    }
93}
94
95impl std::fmt::Display for Symbol {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        self.0.fmt(f)
98    }
99}
100
101impl From<EcoString> for Symbol {
102    fn from(s: EcoString) -> Self {
103        Self(s)
104    }
105}
106
107impl From<Symbol> for EcoString {
108    fn from(s: Symbol) -> Self {
109        s.0
110    }
111}
112
113impl From<&str> for Symbol {
114    fn from(s: &str) -> Self {
115        Self(EcoString::from(s))
116    }
117}
118
119impl From<String> for Symbol {
120    fn from(s: String) -> Self {
121        Self(EcoString::from(s))
122    }
123}
124
125impl PartialEq<str> for Symbol {
126    fn eq(&self, other: &str) -> bool {
127        self.0.as_str() == other
128    }
129}
130
131impl PartialEq<&str> for Symbol {
132    fn eq(&self, other: &&str) -> bool {
133        self.0.as_str() == *other
134    }
135}
136
137/// Extract the unqualified name from a dot-qualified identifier.
138///
139/// `"prelude.Option"` → `"Option"`, `"**nominal.int"` → `"int"`, `"foo"` → `"foo"`
140pub fn unqualified_name(id: &str) -> &str {
141    id.rsplit('.').next().unwrap_or(id)
142}
143
144pub const GO_IMPORT_PREFIX: &str = "go:";
145
146/// Resolve the module of a qualified ID. For `go:` IDs containing `/`,
147/// does a longest-prefix match against `module_ids` to disambiguate paths
148/// whose module segment contains dots (e.g. `gopkg.in/yaml.v3`). Otherwise
149/// splits on the first dot. Returns `None` when the id has no dot and is
150/// not a registered `go:` module.
151pub fn module_for_qualified_name<'a, I>(id: &'a str, module_ids: I) -> Option<&'a str>
152where
153    I: IntoIterator<Item = &'a str>,
154{
155    if !id.starts_with(GO_IMPORT_PREFIX) || !id.contains('/') {
156        return id.split_once('.').map(|(m, _)| m);
157    }
158    let mut best: Option<&str> = None;
159    for module_id in module_ids {
160        if id.starts_with(module_id)
161            && id.as_bytes().get(module_id.len()) == Some(&b'.')
162            && best.is_none_or(|prev| module_id.len() > prev.len())
163        {
164            best = Some(module_id);
165        }
166    }
167    best
168}
169
170pub fn is_range_type_name(name: &str) -> bool {
171    matches!(
172        name,
173        "Range" | "RangeInclusive" | "RangeFrom" | "RangeTo" | "RangeToInclusive"
174    )
175}
176
177pub fn peel_to_range_type(ty: &Type) -> Option<&Type> {
178    std::iter::successors(Some(ty), |t| match t {
179        Type::Nominal {
180            underlying_ty: Some(u),
181            ..
182        } => Some(u.as_ref()),
183        _ => None,
184    })
185    .find(|t| t.get_name().is_some_and(is_range_type_name))
186}
187
188/// type param name -> type variable
189pub type SubstitutionMap = HashMap<EcoString, Type>;
190
191/// Build a substitution map from a list of generics and their type arguments,
192/// pairing each generic's name with the type at the same position.
193pub fn build_substitution_map(generics: &[Generic], type_args: &[Type]) -> SubstitutionMap {
194    generics
195        .iter()
196        .zip(type_args.iter())
197        .map(|(g, t)| (g.name.clone(), t.clone()))
198        .collect()
199}
200
201pub fn substitute(ty: &Type, map: &HashMap<EcoString, Type>) -> Type {
202    if map.is_empty() {
203        return ty.clone();
204    }
205    match ty {
206        Type::Parameter(name) => map.get(name).cloned().unwrap_or_else(|| ty.clone()),
207        Type::Nominal {
208            id,
209            params,
210            underlying_ty: underlying,
211        } => Type::Nominal {
212            id: id.clone(),
213            params: params.iter().map(|p| substitute(p, map)).collect(),
214            underlying_ty: underlying.as_ref().map(|u| Box::new(substitute(u, map))),
215        },
216        Type::Function {
217            params,
218            param_mutability,
219            bounds,
220            return_type,
221        } => Type::Function {
222            params: params.iter().map(|p| substitute(p, map)).collect(),
223            param_mutability: param_mutability.clone(),
224            bounds: bounds
225                .iter()
226                .map(|b| Bound {
227                    param_name: b.param_name.clone(),
228                    generic: substitute(&b.generic, map),
229                    ty: substitute(&b.ty, map),
230                })
231                .collect(),
232            return_type: Box::new(substitute(return_type, map)),
233        },
234        Type::Var { .. } | Type::Error => ty.clone(),
235        Type::Forall { vars, body } => {
236            let has_overlap = map.keys().any(|k| vars.contains(k));
237            let substituted_body = if has_overlap {
238                let filtered_map: HashMap<EcoString, Type> = map
239                    .iter()
240                    .filter(|(k, _)| !vars.contains(*k))
241                    .map(|(k, v)| (k.clone(), v.clone()))
242                    .collect();
243                substitute(body, &filtered_map)
244            } else {
245                substitute(body, map)
246            };
247            Type::Forall {
248                vars: vars.clone(),
249                body: Box::new(substituted_body),
250            }
251        }
252        Type::Tuple(elements) => Type::Tuple(elements.iter().map(|e| substitute(e, map)).collect()),
253        Type::Compound { kind, args } => Type::Compound {
254            kind: *kind,
255            args: args.iter().map(|a| substitute(a, map)).collect(),
256        },
257        Type::Simple(_) | Type::Never | Type::ImportNamespace(_) | Type::ReceiverPlaceholder => {
258            ty.clone()
259        }
260    }
261}
262
263#[derive(Debug, Clone, PartialEq)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
265pub struct Bound {
266    pub param_name: EcoString,
267    pub generic: Type,
268    pub ty: Type,
269}
270
271/// A unique handle identifying a type variable. The binding state (Unbound /
272/// Bound-to-a-Type) lives in a `TypeEnv` owned by the checker; the handle is
273/// a plain id so `Type` stays a pure value (Clone, Eq, Hash, Serialize).
274#[derive(Clone, Copy, PartialEq, Eq, Hash)]
275#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
276pub struct TypeVarId(pub u32);
277
278impl TypeVarId {
279    pub const IGNORED: TypeVarId = TypeVarId(u32::MAX);
280    pub const UNINFERRED: TypeVarId = TypeVarId(u32::MAX - 1);
281
282    pub fn is_reserved(self) -> bool {
283        self == Self::IGNORED || self == Self::UNINFERRED
284    }
285
286    pub fn as_u32(self) -> u32 {
287        self.0
288    }
289}
290
291impl std::fmt::Debug for TypeVarId {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        match *self {
294            Self::IGNORED => write!(f, "ignored"),
295            Self::UNINFERRED => write!(f, "uninferred"),
296            TypeVarId(n) => write!(f, "#{}", n),
297        }
298    }
299}
300
301#[derive(Clone)]
302#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
303pub enum Type {
304    Simple(SimpleKind),
305
306    Compound {
307        kind: CompoundKind,
308        args: Vec<Type>,
309    },
310
311    Nominal {
312        id: Symbol,
313        params: Vec<Type>,
314        underlying_ty: Option<Box<Type>>,
315    },
316
317    /// Module namespace handle. Produced by imports (e.g. `import http "net/http"`
318    /// produces an `ImportNamespace("go:net/http")` on the local identifier).
319    /// Dot-access on this type resolves to the module's exports.
320    ImportNamespace(EcoString),
321
322    Function {
323        params: Vec<Type>,
324        param_mutability: Vec<bool>,
325        bounds: Vec<Bound>,
326        return_type: Box<Type>,
327    },
328
329    /// Type variable handle. Binding state lives in a `TypeEnv` owned by the
330    /// checker; the inline `hint` is display metadata set at allocation time
331    /// so `Display`/`Debug` work without env access.
332    Var {
333        id: TypeVarId,
334        hint: Option<EcoString>,
335    },
336
337    Forall {
338        vars: Vec<EcoString>,
339        body: Box<Type>,
340    },
341
342    Parameter(EcoString),
343
344    Never,
345
346    Tuple(Vec<Type>),
347
348    /// Poison type returned after an error has been reported.
349    /// Unifies with everything silently, preventing cascading diagnostics.
350    Error,
351
352    /// Sentinel occupying the `self` slot of an interface method type.
353    /// Unifies silently so an implementing type's receiver does not conflict
354    /// with the abstract method shape. Previously encoded as
355    /// `Constructor { id: "**nominal.__receiver__" }`.
356    ReceiverPlaceholder,
357}
358
359impl std::fmt::Debug for Type {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        match self {
362            Type::Nominal { id, params, .. } => f
363                .debug_struct("Nominal")
364                .field("id", id)
365                .field("params", params)
366                .finish(),
367            Type::Function {
368                params,
369                param_mutability,
370                bounds,
371                return_type,
372            } => {
373                let mut s = f.debug_struct("Function");
374                s.field("params", params);
375                if param_mutability.iter().any(|m| *m) {
376                    s.field("param_mutability", param_mutability);
377                }
378                s.field("bounds", bounds)
379                    .field("return_type", return_type)
380                    .finish()
381            }
382            Type::Var { id, hint } => {
383                let mut s = f.debug_struct("Var");
384                s.field("id", id);
385                if let Some(h) = hint {
386                    s.field("hint", h);
387                }
388                s.finish()
389            }
390            Type::Forall { vars, body } => f
391                .debug_struct("Forall")
392                .field("vars", vars)
393                .field("body", body)
394                .finish(),
395            Type::Parameter(name) => f.debug_tuple("Parameter").field(name).finish(),
396            Type::Never => write!(f, "Never"),
397            Type::Tuple(elements) => f.debug_tuple("Tuple").field(elements).finish(),
398            Type::Error => write!(f, "Error"),
399            Type::ImportNamespace(module_id) => {
400                f.debug_tuple("ImportNamespace").field(module_id).finish()
401            }
402            Type::ReceiverPlaceholder => write!(f, "ReceiverPlaceholder"),
403            Type::Simple(kind) => f.debug_tuple("Simple").field(kind).finish(),
404            Type::Compound { kind, args } => f
405                .debug_struct("Compound")
406                .field("kind", kind)
407                .field("args", args)
408                .finish(),
409        }
410    }
411}
412
413impl PartialEq for Type {
414    fn eq(&self, other: &Self) -> bool {
415        match (self, other) {
416            (
417                Type::Nominal {
418                    id: id1,
419                    params: params1,
420                    ..
421                },
422                Type::Nominal {
423                    id: id2,
424                    params: params2,
425                    ..
426                },
427            ) => id1 == id2 && params1 == params2,
428            (
429                Type::Function {
430                    params: p1,
431                    param_mutability: m1,
432                    bounds: b1,
433                    return_type: r1,
434                },
435                Type::Function {
436                    params: p2,
437                    param_mutability: m2,
438                    bounds: b2,
439                    return_type: r2,
440                },
441            ) => p1 == p2 && m1 == m2 && b1 == b2 && r1 == r2,
442            (Type::Var { id: id1, .. }, Type::Var { id: id2, .. }) => id1 == id2,
443            (
444                Type::Forall {
445                    vars: vars1,
446                    body: body1,
447                },
448                Type::Forall {
449                    vars: vars2,
450                    body: body2,
451                },
452            ) => vars1 == vars2 && body1 == body2,
453            (Type::Parameter(name1), Type::Parameter(name2)) => name1 == name2,
454            (Type::Never, Type::Never) => true,
455            (Type::Tuple(elems1), Type::Tuple(elems2)) => elems1 == elems2,
456            (Type::ImportNamespace(m1), Type::ImportNamespace(m2)) => m1 == m2,
457            (Type::ReceiverPlaceholder, Type::ReceiverPlaceholder) => true,
458            (Type::Simple(k1), Type::Simple(k2)) => k1 == k2,
459            (Type::Compound { kind: k1, args: a1 }, Type::Compound { kind: k2, args: a2 }) => {
460                k1 == k2 && a1 == a2
461            }
462            _ => false,
463        }
464    }
465}
466
467thread_local! {
468    static INTERNED_INT: OnceCell<Type> = const { OnceCell::new() };
469    static INTERNED_STRING: OnceCell<Type> = const { OnceCell::new() };
470    static INTERNED_BOOL: OnceCell<Type> = const { OnceCell::new() };
471    static INTERNED_UNIT: OnceCell<Type> = const { OnceCell::new() };
472    static INTERNED_FLOAT64: OnceCell<Type> = const { OnceCell::new() };
473    static INTERNED_RUNE: OnceCell<Type> = const { OnceCell::new() };
474    static INTERNED_BYTE: OnceCell<Type> = const { OnceCell::new() };
475}
476
477impl Type {
478    pub fn simple(kind: SimpleKind) -> Type {
479        Self::Simple(kind)
480    }
481
482    pub fn compound(kind: CompoundKind, args: Vec<Type>) -> Type {
483        Self::Compound { kind, args }
484    }
485
486    pub fn int() -> Type {
487        INTERNED_INT.with(|cell| cell.get_or_init(|| Self::simple(SimpleKind::Int)).clone())
488    }
489
490    pub fn string() -> Type {
491        INTERNED_STRING.with(|cell| {
492            cell.get_or_init(|| Self::simple(SimpleKind::String))
493                .clone()
494        })
495    }
496
497    pub fn bool() -> Type {
498        INTERNED_BOOL.with(|cell| cell.get_or_init(|| Self::simple(SimpleKind::Bool)).clone())
499    }
500
501    pub fn unit() -> Type {
502        INTERNED_UNIT.with(|cell| cell.get_or_init(|| Self::simple(SimpleKind::Unit)).clone())
503    }
504
505    pub fn float64() -> Type {
506        INTERNED_FLOAT64.with(|cell| {
507            cell.get_or_init(|| Self::simple(SimpleKind::Float64))
508                .clone()
509        })
510    }
511
512    pub fn rune() -> Type {
513        INTERNED_RUNE.with(|cell| cell.get_or_init(|| Self::simple(SimpleKind::Rune)).clone())
514    }
515
516    pub fn byte() -> Type {
517        INTERNED_BYTE.with(|cell| cell.get_or_init(|| Self::simple(SimpleKind::Byte)).clone())
518    }
519}
520
521impl Type {
522    pub fn uninferred() -> Self {
523        Self::Var {
524            id: TypeVarId::UNINFERRED,
525            hint: None,
526        }
527    }
528
529    pub fn ignored() -> Self {
530        Self::Var {
531            id: TypeVarId::IGNORED,
532            hint: None,
533        }
534    }
535
536    pub fn get_type_params(&self) -> Option<&[Type]> {
537        match self {
538            Type::Nominal { params, .. } => Some(params),
539            Type::Compound { args, .. } => Some(args),
540            _ => None,
541        }
542    }
543
544    /// Direct child types, for read-only walks. Excludes `Function.bounds`.
545    pub fn children(&self) -> Vec<&Type> {
546        match self {
547            Type::Nominal {
548                params,
549                underlying_ty,
550                ..
551            } => {
552                let mut c: Vec<&Type> = params.iter().collect();
553                if let Some(u) = underlying_ty {
554                    c.push(u);
555                }
556                c
557            }
558            Type::Compound { args, .. } => args.iter().collect(),
559            Type::Function {
560                params,
561                return_type,
562                ..
563            } => {
564                let mut c: Vec<&Type> = params.iter().collect();
565                c.push(return_type);
566                c
567            }
568            Type::Tuple(elements) => elements.iter().collect(),
569            Type::Forall { body, .. } => vec![body],
570            _ => vec![],
571        }
572    }
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
576pub enum NumericFamily {
577    SignedInt,
578    UnsignedInt,
579    Float,
580}
581
582#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
583#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
584pub enum CompoundKind {
585    Ref,
586    Slice,
587    EnumeratedSlice,
588    Map,
589    Channel,
590    Sender,
591    Receiver,
592    VarArgs,
593}
594
595impl CompoundKind {
596    pub fn leaf_name(self) -> &'static str {
597        match self {
598            CompoundKind::Ref => "Ref",
599            CompoundKind::Slice => "Slice",
600            CompoundKind::EnumeratedSlice => "EnumeratedSlice",
601            CompoundKind::Map => "Map",
602            CompoundKind::Channel => "Channel",
603            CompoundKind::Sender => "Sender",
604            CompoundKind::Receiver => "Receiver",
605            CompoundKind::VarArgs => "VarArgs",
606        }
607    }
608
609    pub fn from_name(name: &str) -> Option<CompoundKind> {
610        Some(match name {
611            "Ref" => CompoundKind::Ref,
612            "Slice" => CompoundKind::Slice,
613            "EnumeratedSlice" => CompoundKind::EnumeratedSlice,
614            "Map" => CompoundKind::Map,
615            "Channel" => CompoundKind::Channel,
616            "Sender" => CompoundKind::Sender,
617            "Receiver" => CompoundKind::Receiver,
618            "VarArgs" => CompoundKind::VarArgs,
619            _ => return None,
620        })
621    }
622}
623
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
625#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
626pub enum SimpleKind {
627    Int,
628    Int8,
629    Int16,
630    Int32,
631    Int64,
632    Uint,
633    Uint8,
634    Uint16,
635    Uint32,
636    Uint64,
637    Uintptr,
638    Byte,
639    Float32,
640    Float64,
641    Complex64,
642    Complex128,
643    Rune,
644    Bool,
645    String,
646    Unit,
647}
648
649impl SimpleKind {
650    pub fn leaf_name(self) -> &'static str {
651        match self {
652            SimpleKind::Int => "int",
653            SimpleKind::Int8 => "int8",
654            SimpleKind::Int16 => "int16",
655            SimpleKind::Int32 => "int32",
656            SimpleKind::Int64 => "int64",
657            SimpleKind::Uint => "uint",
658            SimpleKind::Uint8 => "uint8",
659            SimpleKind::Uint16 => "uint16",
660            SimpleKind::Uint32 => "uint32",
661            SimpleKind::Uint64 => "uint64",
662            SimpleKind::Uintptr => "uintptr",
663            SimpleKind::Byte => "byte",
664            SimpleKind::Float32 => "float32",
665            SimpleKind::Float64 => "float64",
666            SimpleKind::Complex64 => "complex64",
667            SimpleKind::Complex128 => "complex128",
668            SimpleKind::Rune => "rune",
669            SimpleKind::Bool => "bool",
670            SimpleKind::String => "string",
671            SimpleKind::Unit => "Unit",
672        }
673    }
674
675    pub fn from_name(name: &str) -> Option<SimpleKind> {
676        Some(match name {
677            "int" => SimpleKind::Int,
678            "int8" => SimpleKind::Int8,
679            "int16" => SimpleKind::Int16,
680            "int32" => SimpleKind::Int32,
681            "int64" => SimpleKind::Int64,
682            "uint" => SimpleKind::Uint,
683            "uint8" => SimpleKind::Uint8,
684            "uint16" => SimpleKind::Uint16,
685            "uint32" => SimpleKind::Uint32,
686            "uint64" => SimpleKind::Uint64,
687            "uintptr" => SimpleKind::Uintptr,
688            "byte" => SimpleKind::Byte,
689            "float32" => SimpleKind::Float32,
690            "float64" => SimpleKind::Float64,
691            "complex64" => SimpleKind::Complex64,
692            "complex128" => SimpleKind::Complex128,
693            "rune" => SimpleKind::Rune,
694            "bool" => SimpleKind::Bool,
695            "string" => SimpleKind::String,
696            "Unit" => SimpleKind::Unit,
697            _ => return None,
698        })
699    }
700
701    pub fn is_arithmetic(self) -> bool {
702        !matches!(
703            self,
704            SimpleKind::Bool | SimpleKind::String | SimpleKind::Unit | SimpleKind::Uintptr
705        )
706    }
707
708    pub fn is_ordered(self) -> bool {
709        self.is_arithmetic() && !matches!(self, SimpleKind::Complex64 | SimpleKind::Complex128)
710    }
711
712    pub fn is_unsigned_int(self) -> bool {
713        matches!(
714            self,
715            SimpleKind::Byte
716                | SimpleKind::Uint
717                | SimpleKind::Uint8
718                | SimpleKind::Uint16
719                | SimpleKind::Uint32
720                | SimpleKind::Uint64
721        )
722    }
723
724    pub fn is_signed_int(self) -> bool {
725        matches!(
726            self,
727            SimpleKind::Int
728                | SimpleKind::Int8
729                | SimpleKind::Int16
730                | SimpleKind::Int32
731                | SimpleKind::Int64
732                | SimpleKind::Rune
733        )
734    }
735
736    pub fn is_float(self) -> bool {
737        matches!(self, SimpleKind::Float32 | SimpleKind::Float64)
738    }
739
740    pub fn is_complex(self) -> bool {
741        matches!(self, SimpleKind::Complex64 | SimpleKind::Complex128)
742    }
743
744    pub fn numeric_family(self) -> Option<NumericFamily> {
745        if self.is_signed_int() {
746            Some(NumericFamily::SignedInt)
747        } else if self.is_unsigned_int() {
748            Some(NumericFamily::UnsignedInt)
749        } else if self.is_float() {
750            Some(NumericFamily::Float)
751        } else {
752            None
753        }
754    }
755}
756
757impl Type {
758    pub fn get_function_ret(&self) -> Option<&Type> {
759        match self {
760            Type::Function { return_type, .. } => Some(return_type),
761            _ => None,
762        }
763    }
764
765    pub fn has_name(&self, name: &str) -> bool {
766        match self {
767            Type::Nominal { id, .. } => id.last_segment() == name,
768            Type::Simple(kind) => kind.leaf_name() == name,
769            Type::Compound { kind, .. } => kind.leaf_name() == name,
770            _ => false,
771        }
772    }
773
774    pub fn get_qualified_id(&self) -> Option<&str> {
775        match self {
776            Type::Nominal { id, .. } => Some(id.as_str()),
777            _ => None,
778        }
779    }
780
781    pub fn get_underlying(&self) -> Option<&Type> {
782        match self {
783            Type::Nominal {
784                underlying_ty: underlying,
785                ..
786            } => underlying.as_deref(),
787            _ => None,
788        }
789    }
790
791    pub fn is_result(&self) -> bool {
792        self.has_qualified_id("prelude.Result")
793    }
794
795    pub fn is_option(&self) -> bool {
796        self.has_qualified_id("prelude.Option")
797    }
798
799    pub fn is_partial(&self) -> bool {
800        self.has_qualified_id("prelude.Partial")
801    }
802
803    fn has_qualified_id(&self, qualified_id: &str) -> bool {
804        matches!(self, Type::Nominal { id, .. } if id.as_str() == qualified_id)
805    }
806
807    pub fn is_unit(&self) -> bool {
808        self.is_simple(SimpleKind::Unit)
809    }
810
811    pub fn tuple_arity(&self) -> Option<usize> {
812        match self {
813            Type::Tuple(elements) => Some(elements.len()),
814            _ => None,
815        }
816    }
817
818    pub fn is_tuple(&self) -> bool {
819        matches!(self, Type::Tuple(_))
820    }
821
822    pub fn as_import_namespace(&self) -> Option<&str> {
823        match self {
824            Type::ImportNamespace(module_id) => Some(module_id),
825            _ => None,
826        }
827    }
828
829    pub fn as_compound(&self) -> Option<(CompoundKind, &[Type])> {
830        match self {
831            Type::Compound { kind, args } => Some((*kind, args.as_slice())),
832            Type::Nominal { id, params, .. } => {
833                CompoundKind::from_name(id.last_segment()).map(|k| (k, params.as_slice()))
834            }
835            _ => None,
836        }
837    }
838
839    pub fn is_native(&self, kind: CompoundKind) -> bool {
840        self.as_compound().is_some_and(|(k, _)| k == kind)
841    }
842
843    pub fn is_ref(&self) -> bool {
844        self.is_native(CompoundKind::Ref)
845    }
846
847    pub fn is_slice(&self) -> bool {
848        self.is_native(CompoundKind::Slice)
849    }
850
851    pub fn is_map(&self) -> bool {
852        self.is_native(CompoundKind::Map)
853    }
854
855    pub fn is_channel(&self) -> bool {
856        self.is_native(CompoundKind::Channel)
857    }
858
859    pub fn is_receiver_placeholder(&self) -> bool {
860        matches!(self, Type::ReceiverPlaceholder)
861    }
862
863    pub fn is_unknown(&self) -> bool {
864        self.has_name("Unknown")
865    }
866
867    pub fn resolves_to_unknown(&self) -> bool {
868        peel_alias(self, |_| true).is_unknown()
869    }
870
871    pub fn contains_unknown(&self) -> bool {
872        let peeled = peel_alias(self, |_| true);
873        if peeled.is_unknown() {
874            return true;
875        }
876        match &peeled {
877            Type::Compound { args, .. } => args.iter().any(|a| a.contains_unknown()),
878            Type::Function {
879                params,
880                return_type,
881                ..
882            } => params.iter().any(|p| p.contains_unknown()) || return_type.contains_unknown(),
883            Type::Tuple(elements) => elements.iter().any(|e| e.contains_unknown()),
884            Type::Nominal { params, .. } => params.iter().any(|p| p.contains_unknown()),
885            Type::Forall { body, .. } => body.contains_unknown(),
886            _ => false,
887        }
888    }
889
890    pub fn is_receiver(&self) -> bool {
891        self.is_native(CompoundKind::Receiver)
892    }
893
894    pub fn is_ignored(&self) -> bool {
895        matches!(self, Type::Var { id, .. } if *id == TypeVarId::IGNORED)
896    }
897
898    pub fn is_variadic(&self) -> Option<Type> {
899        let last = self.get_function_params()?.last()?;
900        match last.as_compound()? {
901            (CompoundKind::VarArgs, _) => last.inner(),
902            _ => None,
903        }
904    }
905
906    pub fn is_string(&self) -> bool {
907        self.is_simple(SimpleKind::String)
908    }
909
910    pub fn is_slice_of_simple(&self, element: SimpleKind) -> bool {
911        match self.as_compound() {
912            Some((CompoundKind::Slice, [elem])) => elem.is_simple(element),
913            _ => false,
914        }
915    }
916
917    pub fn is_slice_of(&self, element_name: &str) -> bool {
918        match self.as_compound() {
919            Some((CompoundKind::Slice, [elem])) => elem.has_name(element_name),
920            _ => false,
921        }
922    }
923
924    pub fn is_byte_slice(&self) -> bool {
925        self.is_slice_of_simple(SimpleKind::Byte) || self.is_slice_of_simple(SimpleKind::Uint8)
926    }
927
928    pub fn is_rune_slice(&self) -> bool {
929        self.is_slice_of_simple(SimpleKind::Rune)
930    }
931
932    pub fn is_byte_or_rune_slice(&self) -> bool {
933        self.is_byte_slice() || self.is_rune_slice()
934    }
935
936    pub fn has_underlying_rune(&self) -> bool {
937        self.underlying_numeric_type().is_some_and(|t| t.is_rune())
938    }
939
940    pub fn has_underlying_byte(&self) -> bool {
941        self.underlying_numeric_type()
942            .is_some_and(|t| t.is_simple(SimpleKind::Byte) || t.is_simple(SimpleKind::Uint8))
943    }
944
945    pub fn has_byte_or_rune_slice_underlying(&self) -> bool {
946        if self.is_byte_or_rune_slice() {
947            return true;
948        }
949        match self {
950            Type::Nominal { underlying_ty, .. } => underlying_ty
951                .as_deref()
952                .is_some_and(|u| u.has_byte_or_rune_slice_underlying()),
953            _ => false,
954        }
955    }
956
957    pub fn as_simple(&self) -> Option<SimpleKind> {
958        match self {
959            Type::Simple(kind) => Some(*kind),
960            Type::Nominal { id, .. } => SimpleKind::from_name(id.last_segment()),
961            _ => None,
962        }
963    }
964
965    pub fn is_simple(&self, kind: SimpleKind) -> bool {
966        self.as_simple() == Some(kind)
967    }
968
969    pub fn is_boolean(&self) -> bool {
970        self.is_simple(SimpleKind::Bool)
971    }
972
973    pub fn is_rune(&self) -> bool {
974        self.is_simple(SimpleKind::Rune)
975    }
976
977    pub fn is_float64(&self) -> bool {
978        self.is_simple(SimpleKind::Float64)
979    }
980
981    pub fn is_float32(&self) -> bool {
982        self.is_simple(SimpleKind::Float32)
983    }
984
985    pub fn is_float(&self) -> bool {
986        self.as_simple().is_some_and(SimpleKind::is_float)
987    }
988
989    pub fn is_variable(&self) -> bool {
990        matches!(self, Type::Var { .. })
991    }
992
993    pub fn is_type_var(&self) -> bool {
994        matches!(self, Type::Var { .. })
995    }
996
997    pub fn is_numeric(&self) -> bool {
998        self.as_simple().is_some_and(SimpleKind::is_arithmetic)
999    }
1000
1001    pub fn is_ordered(&self) -> bool {
1002        self.as_simple().is_some_and(SimpleKind::is_ordered)
1003    }
1004
1005    /// True for Go's `cmp.Ordered` set: ints, floats, strings, and named aliases over them.
1006    pub fn satisfies_ordered_constraint(&self) -> bool {
1007        if let Some(kind) = self.as_simple() {
1008            return matches!(
1009                kind,
1010                SimpleKind::Int
1011                    | SimpleKind::Int8
1012                    | SimpleKind::Int16
1013                    | SimpleKind::Int32
1014                    | SimpleKind::Int64
1015                    | SimpleKind::Uint
1016                    | SimpleKind::Uint8
1017                    | SimpleKind::Uint16
1018                    | SimpleKind::Uint32
1019                    | SimpleKind::Uint64
1020                    | SimpleKind::Uintptr
1021                    | SimpleKind::Byte
1022                    | SimpleKind::Rune
1023                    | SimpleKind::Float32
1024                    | SimpleKind::Float64
1025                    | SimpleKind::String
1026            );
1027        }
1028        match self {
1029            Type::Nominal { underlying_ty, .. } => underlying_ty
1030                .as_deref()
1031                .is_some_and(Type::satisfies_ordered_constraint),
1032            Type::Parameter(_) => true,
1033            _ => false,
1034        }
1035    }
1036
1037    pub fn is_complex(&self) -> bool {
1038        self.as_simple().is_some_and(SimpleKind::is_complex)
1039    }
1040
1041    pub fn is_unsigned_int(&self) -> bool {
1042        self.as_simple().is_some_and(SimpleKind::is_unsigned_int)
1043    }
1044
1045    pub fn is_never(&self) -> bool {
1046        matches!(self, Type::Never)
1047    }
1048
1049    pub fn is_error(&self) -> bool {
1050        matches!(self, Type::Error)
1051    }
1052
1053    pub fn contains_error(&self) -> bool {
1054        match self {
1055            Type::Error => true,
1056            Type::Nominal {
1057                params,
1058                underlying_ty,
1059                ..
1060            } => {
1061                params.iter().any(Type::contains_error)
1062                    || underlying_ty.as_deref().is_some_and(Type::contains_error)
1063            }
1064            Type::Compound { args, .. } => args.iter().any(Type::contains_error),
1065            Type::Function {
1066                params,
1067                return_type,
1068                ..
1069            } => params.iter().any(Type::contains_error) || return_type.contains_error(),
1070            Type::Tuple(elements) => elements.iter().any(Type::contains_error),
1071            Type::Forall { body, .. } => body.contains_error(),
1072            _ => false,
1073        }
1074    }
1075
1076    pub fn has_unbound_variables(&self) -> bool {
1077        match self {
1078            Type::Var { hint, .. } => hint.is_some(),
1079            Type::Nominal { params, .. } => params.iter().any(|p| p.has_unbound_variables()),
1080            Type::Function {
1081                params,
1082                return_type,
1083                ..
1084            } => {
1085                params.iter().any(|p| p.has_unbound_variables())
1086                    || return_type.has_unbound_variables()
1087            }
1088            Type::Forall { body, .. } => body.has_unbound_variables(),
1089            Type::Tuple(elements) => elements.iter().any(|e| e.has_unbound_variables()),
1090            Type::Compound { args, .. } => args.iter().any(|a| a.has_unbound_variables()),
1091            Type::Simple(_)
1092            | Type::Parameter(_)
1093            | Type::Never
1094            | Type::Error
1095            | Type::ImportNamespace(_)
1096            | Type::ReceiverPlaceholder => false,
1097        }
1098    }
1099
1100    pub fn remove_found_type_names(&self, names: &mut HashSet<EcoString>) {
1101        if names.is_empty() {
1102            return;
1103        }
1104
1105        match self {
1106            Type::Nominal { id, params, .. } => {
1107                names.remove(id.last_segment());
1108                for param in params {
1109                    param.remove_found_type_names(names);
1110                }
1111            }
1112            Type::Function {
1113                params,
1114                return_type,
1115                bounds,
1116                ..
1117            } => {
1118                for param in params {
1119                    param.remove_found_type_names(names);
1120                }
1121                return_type.remove_found_type_names(names);
1122                for bound in bounds {
1123                    bound.generic.remove_found_type_names(names);
1124                    bound.ty.remove_found_type_names(names);
1125                }
1126            }
1127            Type::Forall { body, .. } => {
1128                body.remove_found_type_names(names);
1129            }
1130            Type::Var { .. } => {}
1131            Type::Parameter(name) => {
1132                names.remove(name);
1133            }
1134            Type::Tuple(elements) => {
1135                for element in elements {
1136                    element.remove_found_type_names(names);
1137                }
1138            }
1139            Type::Compound { kind, args } => {
1140                names.remove(kind.leaf_name());
1141                for arg in args {
1142                    arg.remove_found_type_names(names);
1143                }
1144            }
1145            Type::Simple(kind) => {
1146                names.remove(kind.leaf_name());
1147            }
1148            Type::Never | Type::Error | Type::ImportNamespace(_) | Type::ReceiverPlaceholder => {}
1149        }
1150    }
1151}
1152
1153impl Type {
1154    pub fn get_name(&self) -> Option<&str> {
1155        match self {
1156            Type::Simple(kind) => Some(kind.leaf_name()),
1157            Type::Compound { kind, args } => match kind {
1158                CompoundKind::Ref => args.first().and_then(|inner| inner.get_name()),
1159                _ => Some(kind.leaf_name()),
1160            },
1161            Type::Nominal { id, params, .. } => {
1162                let name = id.last_segment();
1163                if CompoundKind::from_name(name) == Some(CompoundKind::Ref) {
1164                    return params.first().and_then(|inner| inner.get_name());
1165                }
1166                Some(name)
1167            }
1168            Type::ImportNamespace(module_id) => {
1169                let path = module_id.strip_prefix("go:").unwrap_or(module_id);
1170                path.rsplit('/').next()
1171            }
1172            _ => None,
1173        }
1174    }
1175
1176    pub fn wraps(&self, name: &str, inner: &Type) -> bool {
1177        self.get_name().is_some_and(|n| n == name)
1178            && self
1179                .get_type_params()
1180                .and_then(|p| p.first())
1181                .is_some_and(|first| *first == *inner)
1182    }
1183
1184    pub fn get_function_params(&self) -> Option<&[Type]> {
1185        match self {
1186            Type::Function { params, .. } => Some(params),
1187            Type::Nominal {
1188                underlying_ty: Some(inner),
1189                ..
1190            } => inner.get_function_params(),
1191            _ => None,
1192        }
1193    }
1194
1195    pub fn param_count(&self) -> usize {
1196        match self {
1197            Type::Function { params, .. } => params.len(),
1198            _ => 0,
1199        }
1200    }
1201
1202    pub fn get_param_mutability(&self) -> &[bool] {
1203        match self {
1204            Type::Function {
1205                param_mutability, ..
1206            } => param_mutability,
1207            _ => &[],
1208        }
1209    }
1210
1211    pub fn with_replaced_first_param(&self, new_first: &Type) -> Type {
1212        match self {
1213            Type::Function {
1214                params,
1215                param_mutability,
1216                bounds,
1217                return_type,
1218            } => {
1219                if params.is_empty() {
1220                    return self.clone();
1221                }
1222                let mut new_params = params.clone();
1223                new_params[0] = new_first.clone();
1224                Type::Function {
1225                    params: new_params,
1226                    param_mutability: param_mutability.clone(),
1227                    bounds: bounds.clone(),
1228                    return_type: return_type.clone(),
1229                }
1230            }
1231            Type::Forall { vars, body } => Type::Forall {
1232                vars: vars.clone(),
1233                body: Box::new(body.with_replaced_first_param(new_first)),
1234            },
1235            _ => self.clone(),
1236        }
1237    }
1238
1239    pub fn get_bounds(&self) -> &[Bound] {
1240        match self {
1241            Type::Function { bounds, .. } => bounds,
1242            Type::Forall { body, .. } => body.get_bounds(),
1243            _ => &[],
1244        }
1245    }
1246
1247    pub fn get_qualified_name(&self) -> Symbol {
1248        match self.strip_refs() {
1249            Type::Nominal { id, .. } => id,
1250            Type::Simple(kind) => Symbol::from_parts("prelude", kind.leaf_name()),
1251            Type::Compound { kind, .. } => Symbol::from_parts("prelude", kind.leaf_name()),
1252            _ => panic!("called get_qualified_name on {:#?}", self),
1253        }
1254    }
1255
1256    pub fn inner(&self) -> Option<Type> {
1257        self.get_type_params()
1258            .and_then(|args| args.first().cloned())
1259    }
1260
1261    pub fn ok_type(&self) -> Type {
1262        debug_assert!(
1263            self.is_result() || self.is_option() || self.is_partial(),
1264            "ok_type called on non-Result/Option/Partial type"
1265        );
1266        self.inner()
1267            .expect("Result/Option/Partial should have inner type")
1268    }
1269
1270    pub fn err_type(&self) -> Type {
1271        debug_assert!(
1272            self.is_result() || self.is_partial(),
1273            "err_type called on non-Result/Partial type"
1274        );
1275        self.get_type_params()
1276            .and_then(|args| args.get(1).cloned())
1277            .expect("Result/Partial should have error type")
1278    }
1279}
1280
1281/// Walk an alias chain via `underlying_ty` (preserves substitution); cycle
1282/// guard defends against chains that slip past `circular_type_alias`.
1283pub fn peel_alias<F>(ty: &Type, is_alias: F) -> Type
1284where
1285    F: Fn(&str) -> bool,
1286{
1287    let mut current = ty.unwrap_forall().clone();
1288    let mut seen: Vec<String> = Vec::new();
1289    while let Type::Nominal {
1290        id,
1291        underlying_ty: Some(u),
1292        ..
1293    } = &current
1294    {
1295        if !is_alias(id.as_str()) {
1296            break;
1297        }
1298        if seen.iter().any(|s| s == id.as_str()) {
1299            break;
1300        }
1301        seen.push(id.to_string());
1302        current = u.unwrap_forall().clone();
1303    }
1304    current
1305}
1306
1307/// Walk an alias chain by id alone; used when no `Type` with
1308/// `underlying_ty` is available (e.g. Go-name resolution).
1309pub fn peel_alias_id<F>(id: &str, next_alias: F) -> String
1310where
1311    F: Fn(&str) -> Option<String>,
1312{
1313    let mut current = id.to_string();
1314    let mut seen: Vec<String> = Vec::new();
1315    loop {
1316        if seen.iter().any(|s| s == &current) {
1317            return current;
1318        }
1319        let Some(next) = next_alias(&current) else {
1320            return current;
1321        };
1322        seen.push(current);
1323        current = next;
1324    }
1325}
1326
1327impl Type {
1328    pub fn unwrap_forall(&self) -> &Type {
1329        match self {
1330            Type::Forall { body, .. } => body.as_ref(),
1331            other => other,
1332        }
1333    }
1334
1335    pub fn strip_refs(&self) -> Type {
1336        if self.is_ref() {
1337            return self.inner().expect("ref type must have inner").strip_refs();
1338        }
1339
1340        self.clone()
1341    }
1342
1343    pub fn with_receiver_placeholder(self) -> Type {
1344        match self {
1345            Type::Function {
1346                params,
1347                param_mutability,
1348                bounds,
1349                return_type,
1350            } => {
1351                let mut new_params = vec![Type::ReceiverPlaceholder];
1352                new_params.extend(params);
1353
1354                let mut new_mutability = vec![false];
1355                new_mutability.extend(param_mutability);
1356
1357                Type::Function {
1358                    params: new_params,
1359                    param_mutability: new_mutability,
1360                    bounds,
1361                    return_type,
1362                }
1363            }
1364            _ => unreachable!(
1365                "with_receiver_placeholder called on non-function type: {:?}",
1366                self
1367            ),
1368        }
1369    }
1370
1371    pub fn remove_vars(types: &[&Type]) -> (Vec<Type>, Vec<EcoString>) {
1372        let mut vars = HashMap::default();
1373        let types = types
1374            .iter()
1375            .map(|v| Self::remove_vars_impl(v, &mut vars))
1376            .collect();
1377
1378        (types, vars.into_values().collect())
1379    }
1380
1381    fn remove_vars_impl(ty: &Type, vars: &mut HashMap<u32, EcoString>) -> Type {
1382        match ty {
1383            Type::Nominal {
1384                id: name,
1385                params: args,
1386                underlying_ty: underlying,
1387            } => Type::Nominal {
1388                id: name.clone(),
1389                params: args
1390                    .iter()
1391                    .map(|a| Self::remove_vars_impl(a, vars))
1392                    .collect(),
1393                underlying_ty: underlying
1394                    .as_ref()
1395                    .map(|u| Box::new(Self::remove_vars_impl(u, vars))),
1396            },
1397
1398            Type::Function {
1399                params: args,
1400                param_mutability,
1401                bounds,
1402                return_type,
1403            } => Type::Function {
1404                params: args
1405                    .iter()
1406                    .map(|a| Self::remove_vars_impl(a, vars))
1407                    .collect(),
1408                param_mutability: param_mutability.clone(),
1409                bounds: bounds
1410                    .iter()
1411                    .map(|b| Bound {
1412                        param_name: b.param_name.clone(),
1413                        generic: Self::remove_vars_impl(&b.generic, vars),
1414                        ty: Self::remove_vars_impl(&b.ty, vars),
1415                    })
1416                    .collect(),
1417                return_type: Self::remove_vars_impl(return_type, vars).into(),
1418            },
1419
1420            Type::Var { id, hint } => match vars.get(&id.0) {
1421                Some(g) => Type::Parameter(g.clone()),
1422                None => {
1423                    let name: EcoString = hint
1424                        .clone()
1425                        .unwrap_or_else(|| alpha_index(vars.len()).into());
1426
1427                    vars.insert(id.0, name.clone());
1428                    Type::Parameter(name)
1429                }
1430            },
1431
1432            Type::Forall { body, .. } => Self::remove_vars_impl(body, vars),
1433            Type::Tuple(elements) => Type::Tuple(
1434                elements
1435                    .iter()
1436                    .map(|e| Self::remove_vars_impl(e, vars))
1437                    .collect(),
1438            ),
1439            Type::Compound { kind, args } => Type::Compound {
1440                kind: *kind,
1441                args: args
1442                    .iter()
1443                    .map(|a| Self::remove_vars_impl(a, vars))
1444                    .collect(),
1445            },
1446            Type::Simple(_) | Type::Parameter(_) => ty.clone(),
1447            Type::Never | Type::Error | Type::ImportNamespace(_) | Type::ReceiverPlaceholder => {
1448                ty.clone()
1449            }
1450        }
1451    }
1452
1453    pub fn contains_type(&self, target: &Type) -> bool {
1454        if *self == *target {
1455            return true;
1456        }
1457        match self {
1458            Type::Nominal { params, .. } => params.iter().any(|p| p.contains_type(target)),
1459            Type::Function {
1460                params,
1461                return_type,
1462                ..
1463            } => {
1464                params.iter().any(|p| p.contains_type(target)) || return_type.contains_type(target)
1465            }
1466            Type::Var { .. } => false,
1467            Type::Forall { body, .. } => body.contains_type(target),
1468            Type::Tuple(elements) => elements.iter().any(|e| e.contains_type(target)),
1469            Type::Compound { args, .. } => args.iter().any(|a| a.contains_type(target)),
1470            Type::Simple(_)
1471            | Type::Parameter(_)
1472            | Type::Never
1473            | Type::Error
1474            | Type::ImportNamespace(_)
1475            | Type::ReceiverPlaceholder => false,
1476        }
1477    }
1478}
1479
1480impl Type {
1481    pub fn underlying_numeric_type(&self) -> Option<Type> {
1482        self.underlying_numeric_type_recursive(&mut HashSet::default())
1483    }
1484
1485    pub fn has_underlying_numeric_type(&self) -> bool {
1486        self.underlying_numeric_type().is_some()
1487    }
1488
1489    fn underlying_numeric_type_recursive(&self, visited: &mut HashSet<Symbol>) -> Option<Type> {
1490        match self {
1491            Type::Simple(_) if self.is_numeric() => Some(self.clone()),
1492            Type::Nominal {
1493                id,
1494                underlying_ty: underlying,
1495                ..
1496            } => {
1497                if self.is_numeric() {
1498                    return Some(self.clone());
1499                }
1500
1501                if !visited.insert(id.clone()) {
1502                    return None;
1503                }
1504
1505                underlying
1506                    .as_ref()?
1507                    .underlying_numeric_type_recursive(visited)
1508            }
1509            _ => None,
1510        }
1511    }
1512
1513    pub fn numeric_family(&self) -> Option<NumericFamily> {
1514        self.as_simple()?.numeric_family()
1515    }
1516
1517    pub fn is_numeric_compatible_with(&self, other: &Type) -> bool {
1518        let self_underlying_ty = self.underlying_numeric_type();
1519        let other_underlying_ty = other.underlying_numeric_type();
1520
1521        match (self_underlying_ty, other_underlying_ty) {
1522            (Some(s), Some(o)) => s.numeric_family() == o.numeric_family(),
1523            _ => false,
1524        }
1525    }
1526
1527    pub fn is_aliased_numeric_type(&self) -> bool {
1528        match self {
1529            Type::Nominal { underlying_ty, .. } => {
1530                underlying_ty.is_some() && !self.is_numeric() && self.has_underlying_numeric_type()
1531            }
1532            _ => false,
1533        }
1534    }
1535}
1536
1537/// 0 → "A", 25 → "Z", 26 → "AA", 27 → "AB", ... (bijective base-26 over A-Z).
1538fn alpha_index(idx: usize) -> String {
1539    let mut s = String::new();
1540    let mut n = idx + 1;
1541    while n > 0 {
1542        n -= 1;
1543        s.insert(0, (b'A' + (n % 26) as u8) as char);
1544        n /= 26;
1545    }
1546    s
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551    use super::*;
1552
1553    #[test]
1554    fn alpha_index_single() {
1555        assert_eq!(alpha_index(0), "A");
1556        assert_eq!(alpha_index(5), "F");
1557        assert_eq!(alpha_index(25), "Z");
1558    }
1559
1560    #[test]
1561    fn alpha_index_double() {
1562        assert_eq!(alpha_index(26), "AA");
1563        assert_eq!(alpha_index(27), "AB");
1564        assert_eq!(alpha_index(51), "AZ");
1565        assert_eq!(alpha_index(52), "BA");
1566        assert_eq!(alpha_index(701), "ZZ");
1567    }
1568
1569    #[test]
1570    fn alpha_index_triple() {
1571        assert_eq!(alpha_index(702), "AAA");
1572    }
1573
1574    fn unhinted_var(id: u32) -> Type {
1575        Type::Var {
1576            id: TypeVarId(id),
1577            hint: None,
1578        }
1579    }
1580
1581    #[test]
1582    fn remove_vars_handles_more_than_six_unhinted_vars() {
1583        let func = Type::Function {
1584            params: (0..6).map(unhinted_var).collect(),
1585            param_mutability: vec![false; 6],
1586            bounds: vec![],
1587            return_type: Box::new(unhinted_var(6)),
1588        };
1589
1590        let (resolved, generics) = Type::remove_vars(&[&func]);
1591
1592        assert_eq!(generics.len(), 7);
1593        let Type::Function {
1594            params,
1595            return_type,
1596            ..
1597        } = &resolved[0]
1598        else {
1599            panic!("expected function type");
1600        };
1601        let names: Vec<_> = params
1602            .iter()
1603            .chain(std::iter::once(return_type.as_ref()))
1604            .map(|p| match p {
1605                Type::Parameter(name) => name.to_string(),
1606                other => panic!("expected parameter, got {:?}", other),
1607            })
1608            .collect();
1609        assert_eq!(names, vec!["A", "B", "C", "D", "E", "F", "G"]);
1610    }
1611
1612    #[test]
1613    fn remove_vars_handles_dozens_of_unhinted_vars() {
1614        let params: Vec<Type> = (0..30).map(unhinted_var).collect();
1615        let func = Type::Function {
1616            params: params.clone(),
1617            param_mutability: vec![false; params.len()],
1618            bounds: vec![],
1619            return_type: Box::new(Type::Simple(SimpleKind::Unit)),
1620        };
1621        let (_, generics) = Type::remove_vars(&[&func]);
1622        assert_eq!(generics.len(), 30);
1623    }
1624}