Skip to main content

mir_types/
atomic.rs

1use std::hash::Hash;
2use std::sync::Arc;
3
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6
7use crate::symbol::Name;
8use crate::Type;
9
10// ---------------------------------------------------------------------------
11// FnParam — used inside callable/closure atomics
12// ---------------------------------------------------------------------------
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct FnParam {
16    pub name: Name,
17    /// Parameter type stored as SimpleType for compact representation.
18    /// Most params are simple scalars (string, int, etc.) and fit inline.
19    pub ty: Option<crate::compact::SimpleType>,
20    /// `@param-out` / `@psalm-param-out` writeback type. When `Some`, the
21    /// variable passed for this by-ref param is set to this type after the call
22    /// instead of the declared in-type (`ty`). Preserved through `TClosure` so
23    /// first-class callables honour out-param annotations.
24    #[serde(default)]
25    pub out_ty: Option<crate::compact::SimpleType>,
26    /// Default value stored as SimpleType. Usually None or a simple scalar.
27    pub default: Option<crate::compact::SimpleType>,
28    pub is_variadic: bool,
29    pub is_byref: bool,
30    pub is_optional: bool,
31}
32
33// ---------------------------------------------------------------------------
34// Variance — covariance / contravariance for template type parameters
35// ---------------------------------------------------------------------------
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
38pub enum Variance {
39    /// Default: exact type match required (no `@template-covariant` / `@template-contravariant`).
40    #[default]
41    Invariant,
42    /// `@template-covariant T` — `C<Sub>` is assignable to `C<Super>`.
43    Covariant,
44    /// `@template-contravariant T` — `C<Super>` is assignable to `C<Sub>`.
45    Contravariant,
46}
47
48// ---------------------------------------------------------------------------
49// TemplateParam — `@template T` / `@template T of Bound`
50// ---------------------------------------------------------------------------
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub struct TemplateParam {
54    pub name: Name,
55    pub bound: Option<Type>,
56    pub variance: Variance,
57}
58
59// ---------------------------------------------------------------------------
60// KeyedProperty — entry in TKeyedArray
61// ---------------------------------------------------------------------------
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
64pub enum ArrayKey {
65    String(Arc<str>),
66    Int(i64),
67}
68
69impl PartialOrd for ArrayKey {
70    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
71        Some(self.cmp(other))
72    }
73}
74
75impl Ord for ArrayKey {
76    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
77        match (self, other) {
78            (ArrayKey::Int(a), ArrayKey::Int(b)) => a.cmp(b),
79            (ArrayKey::String(a), ArrayKey::String(b)) => a.cmp(b),
80            // Int < String
81            (ArrayKey::Int(_), ArrayKey::String(_)) => std::cmp::Ordering::Less,
82            (ArrayKey::String(_), ArrayKey::Int(_)) => std::cmp::Ordering::Greater,
83        }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
88pub struct KeyedProperty {
89    pub ty: Type,
90    pub optional: bool,
91}
92
93type KeyedMap = IndexMap<ArrayKey, KeyedProperty, rustc_hash::FxBuildHasher>;
94
95/// Entries of an `array{...}` shape, in declaration order. Remembers its
96/// order-independent hash, since a shared shape is hashed again whenever a
97/// type holding it is interned; any mutable access forgets it.
98#[derive(Default, Serialize, Deserialize)]
99#[serde(transparent)]
100pub struct KeyedProperties {
101    map: KeyedMap,
102    #[serde(skip)]
103    hash: std::sync::OnceLock<u64>,
104}
105
106impl KeyedProperties {
107    pub fn with_capacity_and_hasher(n: usize, hasher: rustc_hash::FxBuildHasher) -> Self {
108        KeyedMap::with_capacity_and_hasher(n, hasher).into()
109    }
110
111    /// Hash of the entries, independent of their order (equality ignores it too).
112    pub fn entries_hash(&self) -> u64 {
113        *self.hash.get_or_init(|| {
114            self.map.iter().fold(0u64, |combined, (k, v)| {
115                let mut entry_hasher = rustc_hash::FxHasher::default();
116                k.hash(&mut entry_hasher);
117                v.hash(&mut entry_hasher);
118                combined.wrapping_add(std::hash::Hasher::finish(&entry_hasher))
119            })
120        })
121    }
122}
123
124impl From<KeyedMap> for KeyedProperties {
125    fn from(map: KeyedMap) -> Self {
126        KeyedProperties {
127            map,
128            hash: std::sync::OnceLock::new(),
129        }
130    }
131}
132
133impl Clone for KeyedProperties {
134    fn clone(&self) -> Self {
135        KeyedProperties {
136            map: self.map.clone(),
137            hash: self.hash.clone(),
138        }
139    }
140}
141
142impl std::fmt::Debug for KeyedProperties {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        self.map.fmt(f)
145    }
146}
147
148impl PartialEq for KeyedProperties {
149    fn eq(&self, other: &Self) -> bool {
150        // Remembered hashes that differ settle it without visiting the entries.
151        if let (Some(a), Some(b)) = (self.hash.get(), other.hash.get()) {
152            if a != b {
153                return false;
154            }
155        }
156        self.map == other.map
157    }
158}
159
160impl Eq for KeyedProperties {}
161
162impl std::ops::Deref for KeyedProperties {
163    type Target = KeyedMap;
164    fn deref(&self) -> &KeyedMap {
165        &self.map
166    }
167}
168
169impl std::ops::DerefMut for KeyedProperties {
170    fn deref_mut(&mut self) -> &mut KeyedMap {
171        self.hash = std::sync::OnceLock::new();
172        &mut self.map
173    }
174}
175
176impl FromIterator<(ArrayKey, KeyedProperty)> for KeyedProperties {
177    fn from_iter<I: IntoIterator<Item = (ArrayKey, KeyedProperty)>>(iter: I) -> Self {
178        KeyedMap::from_iter(iter).into()
179    }
180}
181
182impl Extend<(ArrayKey, KeyedProperty)> for KeyedProperties {
183    fn extend<I: IntoIterator<Item = (ArrayKey, KeyedProperty)>>(&mut self, iter: I) {
184        std::ops::DerefMut::deref_mut(self).extend(iter);
185    }
186}
187
188impl IntoIterator for KeyedProperties {
189    type Item = (ArrayKey, KeyedProperty);
190    type IntoIter = indexmap::map::IntoIter<ArrayKey, KeyedProperty>;
191    fn into_iter(self) -> Self::IntoIter {
192        self.map.into_iter()
193    }
194}
195
196impl<'a> IntoIterator for &'a KeyedProperties {
197    type Item = (&'a ArrayKey, &'a KeyedProperty);
198    type IntoIter = indexmap::map::Iter<'a, ArrayKey, KeyedProperty>;
199    fn into_iter(self) -> Self::IntoIter {
200        self.map.iter()
201    }
202}
203
204impl<'a> IntoIterator for &'a mut KeyedProperties {
205    type Item = (&'a ArrayKey, &'a mut KeyedProperty);
206    type IntoIter = indexmap::map::IterMut<'a, ArrayKey, KeyedProperty>;
207    fn into_iter(self) -> Self::IntoIter {
208        std::ops::DerefMut::deref_mut(self).iter_mut()
209    }
210}
211
212// ---------------------------------------------------------------------------
213// Boxed variant payloads — keep `Atomic` (and `Type`, which inlines two) small
214// ---------------------------------------------------------------------------
215
216/// Payload of [`Atomic::TClosure`].
217#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
218pub struct ClosureData {
219    pub params: Box<[FnParam]>,
220    pub return_type: Type,
221    pub this_type: Option<Type>,
222    /// `pure-Closure(...)`, or a closure literal whose body is checked as pure.
223    pub is_pure: bool,
224    /// A `static` closure literal, which has no `$this` to mutate.
225    pub is_static: bool,
226    /// A first-class callable of a named function or method, so surplus arguments are reported.
227    #[serde(default)]
228    pub rejects_surplus_args: bool,
229}
230
231/// Payload of [`Atomic::TConditional`].
232#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
233pub struct ConditionalData {
234    /// The parameter name being tested (without `$`), e.g. `"classOrInterface"`.
235    /// `None` for conditionals that were not parsed from a `$param is` form.
236    pub param_name: Option<Name>,
237    pub subject: Type,
238    pub if_true: Type,
239    pub if_false: Type,
240}
241
242// ---------------------------------------------------------------------------
243// Atomic — every distinct PHP type variant
244// ---------------------------------------------------------------------------
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub enum Atomic {
248    // --- Scalars ---
249    /// `string`
250    TString,
251    /// `"hello"` — a specific string literal
252    TLiteralString(Arc<str>),
253    /// `callable-string` — a string containing a callable name
254    TCallableString,
255    /// `class-string` or `class-string<T>`
256    TClassString(Option<Name>),
257    /// `non-empty-string`
258    TNonEmptyString,
259    /// `numeric-string`
260    TNumericString,
261
262    /// `int`
263    TInt,
264    /// `42` — a specific integer literal
265    TLiteralInt(i64),
266    /// `int<min, max>` — bounded integer range
267    TIntRange { min: Option<i64>, max: Option<i64> },
268    /// `positive-int`
269    TPositiveInt,
270    /// `negative-int`
271    TNegativeInt,
272    /// `non-negative-int`
273    TNonNegativeInt,
274
275    /// `float`
276    TFloat,
277    /// `float` that is always integral (whole-number value, no fractional part).
278    /// Returned by `floor()`, `ceil()`, and zero-precision `round()`. Can be passed to `int`
279    /// parameters without `ImplicitFloatToIntCast` because the conversion is lossless.
280    TIntegralFloat,
281    /// `3.14` — a specific float literal
282    TLiteralFloat(i64, i64), // stored as (int_bits, frac_bits) to be PartialEq+Hash-friendly
283    // We use ordered_float or just store as ordered pair for equality purposes.
284    /// `bool`
285    TBool,
286    /// `true`
287    TTrue,
288    /// `false`
289    TFalse,
290
291    /// `null`
292    TNull,
293
294    // --- Bottom / top ---
295    /// `void` — return-only; can't be used as a value
296    TVoid,
297    /// `never` — function that never returns (throws or infinite loop)
298    TNever,
299    /// `mixed` — top type; accepts anything
300    TMixed,
301    /// `scalar` — int | float | string | bool
302    TScalar,
303    /// `numeric` — int | float | numeric-string
304    TNumeric,
305
306    // --- Objects ---
307    /// `object` — any object
308    TObject,
309    /// `ClassName` / `ClassName<T1, T2>` — specific named class/interface
310    TNamedObject {
311        fqcn: Name,
312        /// Resolved generic type arguments (e.g. `Collection<int>`)
313        type_params: Arc<[Type]>,
314    },
315    /// `static` — late static binding type; resolved to calling class at call site
316    TStaticObject { fqcn: Name },
317    /// `self` — the class in whose body the type appears
318    TSelf { fqcn: Name },
319    /// `parent` — the parent class
320    TParent { fqcn: Name },
321
322    // --- Callables ---
323    /// `callable` or `callable(T): R`
324    TCallable {
325        /// Thin box: an inline `Vec` or boxed slice here, next to `is_pure`,
326        /// would make this the largest variant and grow every `Atomic` (and
327        /// `Type` inlines two of them).
328        params: Option<Box<Vec<FnParam>>>,
329        return_type: Option<Box<Type>>,
330        /// `pure-callable`.
331        is_pure: bool,
332    },
333    /// `Closure` or `Closure(T): R` — more specific than TCallable.
334    /// Payload boxed to keep `Atomic` at 32 bytes (see [`ClosureData`]).
335    TClosure { data: Box<ClosureData> },
336
337    // --- Arrays ---
338    /// `array` or `array<K, V>`
339    TArray { key: Box<Type>, value: Box<Type> },
340    /// `list<T>` — integer-keyed sequential array (keys 0, 1, 2, …)
341    TList { value: Box<Type> },
342    /// `non-empty-array<K, V>`
343    TNonEmptyArray { key: Box<Type>, value: Box<Type> },
344    /// `non-empty-list<T>`
345    TNonEmptyList { value: Box<Type> },
346    /// `array{key: T, ...}` — shape / keyed array
347    TKeyedArray {
348        /// Shared so cloning a shape copies a pointer and equal clones compare by address.
349        properties: Arc<KeyedProperties>,
350        /// If true, additional keys beyond the declared ones may exist
351        is_open: bool,
352        /// If true, the shape represents a list (integer keys only)
353        is_list: bool,
354    },
355
356    // --- Generics / meta-types ---
357    /// `T` — a template type parameter
358    TTemplateParam {
359        name: Name,
360        as_type: Box<Type>,
361        /// The entity (class or function FQN) that declared this template
362        defining_entity: Name,
363    },
364    /// `key-of<T>` — preserved when the operand cannot yet be reduced.
365    TKeyOf { target: Box<Type> },
366    /// `value-of<T>` — preserved when the operand cannot yet be reduced.
367    TValueOf { target: Box<Type> },
368    /// `($param is TypeName ? A : B)` — conditional type.
369    /// Payload boxed to keep `Atomic` at 32 bytes (see [`ConditionalData`]).
370    TConditional { data: Box<ConditionalData> },
371
372    // --- Special object strings ---
373    /// `interface-string` or `interface-string<T>`
374    TInterfaceString(Option<Name>),
375    /// `enum-string`
376    TEnumString,
377    /// `trait-string`
378    TTraitString,
379
380    // --- Enum cases ---
381    /// `EnumName::CaseName` — a specific enum case literal
382    TLiteralEnumCase { enum_fqcn: Name, case_name: Name },
383
384    // --- Intersection ---
385    /// `A&B&C` — PHP 8.1+ pure intersection type
386    TIntersection { parts: Arc<[Type]> },
387}
388
389impl Atomic {
390    /// Whether this atomic type can ever evaluate to a falsy value.
391    pub fn can_be_falsy(&self) -> bool {
392        match self {
393            Atomic::TNull
394            | Atomic::TFalse
395            | Atomic::TBool
396            | Atomic::TNever
397            | Atomic::TLiteralInt(0)
398            | Atomic::TLiteralFloat(0, 0)
399            | Atomic::TInt
400            | Atomic::TFloat
401            | Atomic::TNumeric
402            | Atomic::TScalar
403            | Atomic::TMixed
404            | Atomic::TString
405            | Atomic::TNonEmptyString  // "0" is both non-empty and falsy in PHP
406            | Atomic::TNumericString   // "0" is a valid numeric-string and is falsy
407            | Atomic::TKeyOf { .. }
408            | Atomic::TValueOf { .. }
409            | Atomic::TArray { .. }
410            | Atomic::TList { .. } => true,
411            // Non-empty collections always have at least one element — never falsy in PHP.
412            // TNonEmptyArray and TNonEmptyList are intentionally excluded here.
413
414            Atomic::TLiteralString(s) => s.as_ref().is_empty() || s.as_ref() == "0",
415
416            Atomic::TKeyedArray { properties, .. } => properties.is_empty(),
417
418            // An int range can be falsy (== 0) when 0 is within the bounds.
419            Atomic::TIntRange { min, max } => {
420                min.is_none_or(|lo| lo <= 0) && max.is_none_or(|hi| hi >= 0)
421            }
422            // non-negative-int includes 0
423            Atomic::TNonNegativeInt => true,
424
425            _ => false,
426        }
427    }
428
429    /// Whether this atomic type can ever evaluate to a truthy value.
430    pub fn can_be_truthy(&self) -> bool {
431        match self {
432            Atomic::TNever
433            | Atomic::TVoid
434            | Atomic::TNull
435            | Atomic::TFalse
436            | Atomic::TLiteralInt(0)
437            | Atomic::TLiteralFloat(0, 0) => false,
438            Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
439            // int<0, 0> always equals zero — never truthy.
440            Atomic::TIntRange {
441                min: Some(0),
442                max: Some(0),
443            } => false,
444            // array{} — a closed empty keyed array — is always falsy.
445            Atomic::TKeyedArray {
446                properties,
447                is_open,
448                ..
449            } if !is_open && properties.is_empty() => false,
450            _ => true,
451        }
452    }
453
454    /// Whether this atomic represents a numeric type (int, float, or numeric-string).
455    pub fn is_numeric(&self) -> bool {
456        matches!(
457            self,
458            Atomic::TInt
459                | Atomic::TLiteralInt(_)
460                | Atomic::TIntRange { .. }
461                | Atomic::TPositiveInt
462                | Atomic::TNegativeInt
463                | Atomic::TNonNegativeInt
464                | Atomic::TFloat
465                | Atomic::TIntegralFloat
466                | Atomic::TLiteralFloat(..)
467                | Atomic::TNumeric
468                | Atomic::TNumericString
469        )
470    }
471
472    /// Whether this atomic is an integer variant.
473    pub fn is_int(&self) -> bool {
474        matches!(
475            self,
476            Atomic::TInt
477                | Atomic::TLiteralInt(_)
478                | Atomic::TIntRange { .. }
479                | Atomic::TPositiveInt
480                | Atomic::TNegativeInt
481                | Atomic::TNonNegativeInt
482        )
483    }
484
485    /// Whether this atomic is a string variant.
486    pub fn is_string(&self) -> bool {
487        matches!(
488            self,
489            Atomic::TString
490                | Atomic::TLiteralString(_)
491                | Atomic::TCallableString
492                | Atomic::TClassString(_)
493                | Atomic::TNonEmptyString
494                | Atomic::TNumericString
495                | Atomic::TInterfaceString(_)
496                | Atomic::TEnumString
497                | Atomic::TTraitString
498        )
499    }
500
501    /// Whether this atomic is an array variant.
502    pub fn is_array(&self) -> bool {
503        matches!(
504            self,
505            Atomic::TArray { .. }
506                | Atomic::TList { .. }
507                | Atomic::TNonEmptyArray { .. }
508                | Atomic::TNonEmptyList { .. }
509                | Atomic::TKeyedArray { .. }
510        )
511    }
512
513    /// Whether this atomic is an object variant.
514    pub fn is_object(&self) -> bool {
515        matches!(
516            self,
517            Atomic::TObject
518                | Atomic::TNamedObject { .. }
519                | Atomic::TStaticObject { .. }
520                | Atomic::TSelf { .. }
521                | Atomic::TParent { .. }
522                | Atomic::TIntersection { .. }
523        )
524    }
525
526    /// Whether this atomic can never be an object instance — so `clone`-ing it
527    /// is invalid. Conservative: ambiguous atomics (`mixed`, `callable`,
528    /// `never`, conditionals, template params, enum cases, intersections,
529    /// object types) all return `false` so they never trigger a false positive.
530    pub fn is_definitely_non_object(&self) -> bool {
531        matches!(
532            self,
533            Atomic::TString
534                | Atomic::TLiteralString(_)
535                | Atomic::TCallableString
536                | Atomic::TClassString(_)
537                | Atomic::TNonEmptyString
538                | Atomic::TNumericString
539                | Atomic::TInterfaceString(_)
540                | Atomic::TEnumString
541                | Atomic::TTraitString
542                | Atomic::TInt
543                | Atomic::TLiteralInt(_)
544                | Atomic::TIntRange { .. }
545                | Atomic::TPositiveInt
546                | Atomic::TNegativeInt
547                | Atomic::TNonNegativeInt
548                | Atomic::TFloat
549                | Atomic::TIntegralFloat
550                | Atomic::TLiteralFloat(..)
551                | Atomic::TBool
552                | Atomic::TTrue
553                | Atomic::TFalse
554                | Atomic::TNull
555                | Atomic::TVoid
556                | Atomic::TArray { .. }
557                | Atomic::TList { .. }
558                | Atomic::TNonEmptyArray { .. }
559                | Atomic::TNonEmptyList { .. }
560                | Atomic::TKeyedArray { .. }
561                | Atomic::TScalar
562                | Atomic::TNumeric
563        )
564    }
565
566    /// Whether this atomic is a callable variant.
567    ///
568    /// `TCallableString` is included: it denotes a string *known* to be a
569    /// valid callable, so `!is_callable()` must remove it from the type.
570    pub fn is_callable(&self) -> bool {
571        matches!(
572            self,
573            Atomic::TCallable { .. } | Atomic::TClosure { .. } | Atomic::TCallableString
574        )
575    }
576
577    /// Returns the FQCN if this is a named object type.
578    pub fn named_object_fqcn(&self) -> Option<&str> {
579        match self {
580            Atomic::TNamedObject { fqcn, .. }
581            | Atomic::TStaticObject { fqcn }
582            | Atomic::TSelf { fqcn }
583            | Atomic::TParent { fqcn } => Some(fqcn.as_ref()),
584            _ => None,
585        }
586    }
587
588    /// A human-readable name for this type (used in error messages).
589    pub fn type_name(&self) -> &'static str {
590        match self {
591            Atomic::TString
592            | Atomic::TLiteralString(_)
593            | Atomic::TNonEmptyString
594            | Atomic::TNumericString => "string",
595            Atomic::TCallableString => "callable-string",
596            Atomic::TClassString(_) => "class-string",
597            Atomic::TInt | Atomic::TLiteralInt(_) | Atomic::TIntRange { .. } => "int",
598            Atomic::TPositiveInt => "positive-int",
599            Atomic::TNegativeInt => "negative-int",
600            Atomic::TNonNegativeInt => "non-negative-int",
601            Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..) => "float",
602            Atomic::TBool => "bool",
603            Atomic::TTrue => "true",
604            Atomic::TFalse => "false",
605            Atomic::TNull => "null",
606            Atomic::TVoid => "void",
607            Atomic::TNever => "never",
608            Atomic::TMixed => "mixed",
609            Atomic::TScalar => "scalar",
610            Atomic::TNumeric => "numeric",
611            Atomic::TObject => "object",
612            Atomic::TNamedObject { .. } => "object",
613            Atomic::TStaticObject { .. } => "static",
614            Atomic::TSelf { .. } => "self",
615            Atomic::TParent { .. } => "parent",
616            Atomic::TCallable { .. } => "callable",
617            Atomic::TClosure { .. } => "Closure",
618            Atomic::TArray { .. } => "array",
619            Atomic::TList { .. } => "list",
620            Atomic::TNonEmptyArray { .. } => "non-empty-array",
621            Atomic::TNonEmptyList { .. } => "non-empty-list",
622            Atomic::TKeyedArray { .. } => "array",
623            Atomic::TTemplateParam { .. } => "template-param",
624            Atomic::TKeyOf { .. } => "key-of",
625            Atomic::TValueOf { .. } => "value-of",
626            Atomic::TConditional { .. } => "conditional-type",
627            Atomic::TInterfaceString(_) => "interface-string",
628            Atomic::TEnumString => "enum-string",
629            Atomic::TTraitString => "trait-string",
630            Atomic::TLiteralEnumCase { .. } => "enum-case",
631            Atomic::TIntersection { .. } => "intersection",
632        }
633    }
634}
635
636// ---------------------------------------------------------------------------
637// Hash impls
638// ---------------------------------------------------------------------------
639
640impl Hash for FnParam {
641    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
642        self.name.hash(state);
643        self.ty.hash(state);
644        self.out_ty.hash(state);
645        self.default.hash(state);
646        self.is_variadic.hash(state);
647        self.is_byref.hash(state);
648        self.is_optional.hash(state);
649    }
650}
651
652/// Discriminant tags for `Atomic` used in the manual `Hash` impl below.
653#[allow(non_camel_case_types, clippy::enum_variant_names)]
654#[repr(u8)]
655enum AtomicTag {
656    TString = 0,
657    TLiteralString,
658    TCallableString,
659    TClassString,
660    TNonEmptyString,
661    TNumericString,
662    TInt,
663    TLiteralInt,
664    TIntRange,
665    TPositiveInt,
666    TNegativeInt,
667    TNonNegativeInt,
668    TFloat,
669    TIntegralFloat,
670    TLiteralFloat,
671    TBool,
672    TTrue,
673    TFalse,
674    TNull,
675    TVoid,
676    TNever,
677    TMixed,
678    TScalar,
679    TNumeric,
680    TObject,
681    TNamedObject,
682    TStaticObject,
683    TSelf,
684    TParent,
685    TCallable,
686    TClosure,
687    TArray,
688    TList,
689    TNonEmptyArray,
690    TNonEmptyList,
691    TKeyedArray,
692    TTemplateParam,
693    TKeyOf,
694    TValueOf,
695    TConditional,
696    TInterfaceString,
697    TEnumString,
698    TTraitString,
699    TLiteralEnumCase,
700    TIntersection,
701}
702
703impl Hash for Atomic {
704    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
705        use AtomicTag as T;
706        match self {
707            // --- tag-only variants ---
708            Atomic::TString => (T::TString as u8).hash(state),
709            Atomic::TCallableString => (T::TCallableString as u8).hash(state),
710            Atomic::TNonEmptyString => (T::TNonEmptyString as u8).hash(state),
711            Atomic::TNumericString => (T::TNumericString as u8).hash(state),
712            Atomic::TInt => (T::TInt as u8).hash(state),
713            Atomic::TPositiveInt => (T::TPositiveInt as u8).hash(state),
714            Atomic::TNegativeInt => (T::TNegativeInt as u8).hash(state),
715            Atomic::TNonNegativeInt => (T::TNonNegativeInt as u8).hash(state),
716            Atomic::TFloat => (T::TFloat as u8).hash(state),
717            Atomic::TIntegralFloat => (T::TIntegralFloat as u8).hash(state),
718            Atomic::TBool => (T::TBool as u8).hash(state),
719            Atomic::TTrue => (T::TTrue as u8).hash(state),
720            Atomic::TFalse => (T::TFalse as u8).hash(state),
721            Atomic::TNull => (T::TNull as u8).hash(state),
722            Atomic::TVoid => (T::TVoid as u8).hash(state),
723            Atomic::TNever => (T::TNever as u8).hash(state),
724            Atomic::TMixed => (T::TMixed as u8).hash(state),
725            Atomic::TScalar => (T::TScalar as u8).hash(state),
726            Atomic::TNumeric => (T::TNumeric as u8).hash(state),
727            Atomic::TObject => (T::TObject as u8).hash(state),
728            Atomic::TEnumString => (T::TEnumString as u8).hash(state),
729            Atomic::TTraitString => (T::TTraitString as u8).hash(state),
730
731            // --- variants with fields ---
732            Atomic::TLiteralString(s) => {
733                (T::TLiteralString as u8).hash(state);
734                s.hash(state);
735            }
736            Atomic::TClassString(opt) => {
737                (T::TClassString as u8).hash(state);
738                opt.hash(state);
739            }
740            Atomic::TInterfaceString(opt) => {
741                (T::TInterfaceString as u8).hash(state);
742                opt.hash(state);
743            }
744            Atomic::TLiteralInt(n) => {
745                (T::TLiteralInt as u8).hash(state);
746                n.hash(state);
747            }
748            Atomic::TIntRange { min, max } => {
749                (T::TIntRange as u8).hash(state);
750                min.hash(state);
751                max.hash(state);
752            }
753            Atomic::TLiteralFloat(int_bits, frac_bits) => {
754                (T::TLiteralFloat as u8).hash(state);
755                int_bits.hash(state);
756                frac_bits.hash(state);
757            }
758            Atomic::TNamedObject { fqcn, type_params } => {
759                (T::TNamedObject as u8).hash(state);
760                fqcn.hash(state);
761                type_params.hash(state);
762            }
763            Atomic::TStaticObject { fqcn } => {
764                (T::TStaticObject as u8).hash(state);
765                fqcn.hash(state);
766            }
767            Atomic::TSelf { fqcn } => {
768                (T::TSelf as u8).hash(state);
769                fqcn.hash(state);
770            }
771            Atomic::TParent { fqcn } => {
772                (T::TParent as u8).hash(state);
773                fqcn.hash(state);
774            }
775            Atomic::TCallable {
776                params,
777                return_type,
778                is_pure,
779            } => {
780                (T::TCallable as u8).hash(state);
781                params.hash(state);
782                return_type.hash(state);
783                is_pure.hash(state);
784            }
785            Atomic::TClosure { data } => {
786                (T::TClosure as u8).hash(state);
787                data.hash(state);
788            }
789            Atomic::TArray { key, value } => {
790                (T::TArray as u8).hash(state);
791                key.hash(state);
792                value.hash(state);
793            }
794            Atomic::TList { value } => {
795                (T::TList as u8).hash(state);
796                value.hash(state);
797            }
798            Atomic::TNonEmptyArray { key, value } => {
799                (T::TNonEmptyArray as u8).hash(state);
800                key.hash(state);
801                value.hash(state);
802            }
803            Atomic::TNonEmptyList { value } => {
804                (T::TNonEmptyList as u8).hash(state);
805                value.hash(state);
806            }
807            Atomic::TKeyedArray {
808                properties,
809                is_open,
810                is_list,
811            } => {
812                (T::TKeyedArray as u8).hash(state);
813                properties.entries_hash().hash(state);
814                properties.len().hash(state);
815                is_open.hash(state);
816                is_list.hash(state);
817            }
818            Atomic::TTemplateParam {
819                name,
820                as_type,
821                defining_entity,
822            } => {
823                (T::TTemplateParam as u8).hash(state);
824                name.hash(state);
825                as_type.hash(state);
826                defining_entity.hash(state);
827            }
828            Atomic::TKeyOf { target } => {
829                (T::TKeyOf as u8).hash(state);
830                target.hash(state);
831            }
832            Atomic::TValueOf { target } => {
833                (T::TValueOf as u8).hash(state);
834                target.hash(state);
835            }
836            Atomic::TConditional { data } => {
837                (T::TConditional as u8).hash(state);
838                data.hash(state);
839            }
840            Atomic::TLiteralEnumCase {
841                enum_fqcn,
842                case_name,
843            } => {
844                (T::TLiteralEnumCase as u8).hash(state);
845                enum_fqcn.hash(state);
846                case_name.hash(state);
847            }
848            Atomic::TIntersection { parts } => {
849                (T::TIntersection as u8).hash(state);
850                parts.hash(state);
851            }
852        }
853    }
854}