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