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