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
93// ---------------------------------------------------------------------------
94// Boxed variant payloads — keep `Atomic` (and `Type`, which inlines two) small
95// ---------------------------------------------------------------------------
96
97/// Payload of [`Atomic::TClosure`].
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub struct ClosureData {
100    pub params: Box<[FnParam]>,
101    pub return_type: Type,
102    pub this_type: Option<Type>,
103}
104
105/// Payload of [`Atomic::TConditional`].
106#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
107pub struct ConditionalData {
108    /// The parameter name being tested (without `$`), e.g. `"classOrInterface"`.
109    /// `None` for conditionals that were not parsed from a `$param is` form.
110    pub param_name: Option<Name>,
111    pub subject: Type,
112    pub if_true: Type,
113    pub if_false: Type,
114}
115
116// ---------------------------------------------------------------------------
117// Atomic — every distinct PHP type variant
118// ---------------------------------------------------------------------------
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub enum Atomic {
122    // --- Scalars ---
123    /// `string`
124    TString,
125    /// `"hello"` — a specific string literal
126    TLiteralString(Arc<str>),
127    /// `callable-string` — a string containing a callable name
128    TCallableString,
129    /// `class-string` or `class-string<T>`
130    TClassString(Option<Name>),
131    /// `non-empty-string`
132    TNonEmptyString,
133    /// `numeric-string`
134    TNumericString,
135
136    /// `int`
137    TInt,
138    /// `42` — a specific integer literal
139    TLiteralInt(i64),
140    /// `int<min, max>` — bounded integer range
141    TIntRange { min: Option<i64>, max: Option<i64> },
142    /// `positive-int`
143    TPositiveInt,
144    /// `negative-int`
145    TNegativeInt,
146    /// `non-negative-int`
147    TNonNegativeInt,
148
149    /// `float`
150    TFloat,
151    /// `float` that is always integral (whole-number value, no fractional part).
152    /// Returned by `floor()`, `ceil()`, and zero-precision `round()`. Can be passed to `int`
153    /// parameters without `ImplicitFloatToIntCast` because the conversion is lossless.
154    TIntegralFloat,
155    /// `3.14` — a specific float literal
156    TLiteralFloat(i64, i64), // stored as (int_bits, frac_bits) to be PartialEq+Hash-friendly
157    // We use ordered_float or just store as ordered pair for equality purposes.
158    /// `bool`
159    TBool,
160    /// `true`
161    TTrue,
162    /// `false`
163    TFalse,
164
165    /// `null`
166    TNull,
167
168    // --- Bottom / top ---
169    /// `void` — return-only; can't be used as a value
170    TVoid,
171    /// `never` — function that never returns (throws or infinite loop)
172    TNever,
173    /// `mixed` — top type; accepts anything
174    TMixed,
175    /// `scalar` — int | float | string | bool
176    TScalar,
177    /// `numeric` — int | float | numeric-string
178    TNumeric,
179
180    // --- Objects ---
181    /// `object` — any object
182    TObject,
183    /// `ClassName` / `ClassName<T1, T2>` — specific named class/interface
184    TNamedObject {
185        fqcn: Name,
186        /// Resolved generic type arguments (e.g. `Collection<int>`)
187        type_params: Arc<[Type]>,
188    },
189    /// `static` — late static binding type; resolved to calling class at call site
190    TStaticObject { fqcn: Name },
191    /// `self` — the class in whose body the type appears
192    TSelf { fqcn: Name },
193    /// `parent` — the parent class
194    TParent { fqcn: Name },
195
196    // --- Callables ---
197    /// `callable` or `callable(T): R`
198    TCallable {
199        /// Boxed slice: a `Vec` here would make this the largest variant and
200        /// grow every `Atomic` (and `Type` inlines two of them).
201        params: Option<Box<[FnParam]>>,
202        return_type: Option<Box<Type>>,
203    },
204    /// `Closure` or `Closure(T): R` — more specific than TCallable.
205    /// Payload boxed to keep `Atomic` at 32 bytes (see [`ClosureData`]).
206    TClosure { data: Box<ClosureData> },
207
208    // --- Arrays ---
209    /// `array` or `array<K, V>`
210    TArray { key: Box<Type>, value: Box<Type> },
211    /// `list<T>` — integer-keyed sequential array (keys 0, 1, 2, …)
212    TList { value: Box<Type> },
213    /// `non-empty-array<K, V>`
214    TNonEmptyArray { key: Box<Type>, value: Box<Type> },
215    /// `non-empty-list<T>`
216    TNonEmptyList { value: Box<Type> },
217    /// `array{key: T, ...}` — shape / keyed array
218    TKeyedArray {
219        /// Boxed: an inline `IndexMap` (~72 B) would otherwise set the size of
220        /// every `Atomic` — and `Type` inlines two — for this rare variant.
221        properties: Box<IndexMap<ArrayKey, KeyedProperty>>,
222        /// If true, additional keys beyond the declared ones may exist
223        is_open: bool,
224        /// If true, the shape represents a list (integer keys only)
225        is_list: bool,
226    },
227
228    // --- Generics / meta-types ---
229    /// `T` — a template type parameter
230    TTemplateParam {
231        name: Name,
232        as_type: Box<Type>,
233        /// The entity (class or function FQN) that declared this template
234        defining_entity: Name,
235    },
236    /// `key-of<T>` — preserved when the operand cannot yet be reduced.
237    TKeyOf { target: Box<Type> },
238    /// `value-of<T>` — preserved when the operand cannot yet be reduced.
239    TValueOf { target: Box<Type> },
240    /// `($param is TypeName ? A : B)` — conditional type.
241    /// Payload boxed to keep `Atomic` at 32 bytes (see [`ConditionalData`]).
242    TConditional { data: Box<ConditionalData> },
243
244    // --- Special object strings ---
245    /// `interface-string` or `interface-string<T>`
246    TInterfaceString(Option<Name>),
247    /// `enum-string`
248    TEnumString,
249    /// `trait-string`
250    TTraitString,
251
252    // --- Enum cases ---
253    /// `EnumName::CaseName` — a specific enum case literal
254    TLiteralEnumCase { enum_fqcn: Name, case_name: Name },
255
256    // --- Intersection ---
257    /// `A&B&C` — PHP 8.1+ pure intersection type
258    TIntersection { parts: Arc<[Type]> },
259}
260
261impl Atomic {
262    /// Whether this atomic type can ever evaluate to a falsy value.
263    pub fn can_be_falsy(&self) -> bool {
264        match self {
265            Atomic::TNull
266            | Atomic::TFalse
267            | Atomic::TBool
268            | Atomic::TNever
269            | Atomic::TLiteralInt(0)
270            | Atomic::TLiteralFloat(0, 0)
271            | Atomic::TInt
272            | Atomic::TFloat
273            | Atomic::TNumeric
274            | Atomic::TScalar
275            | Atomic::TMixed
276            | Atomic::TString
277            | Atomic::TNonEmptyString  // "0" is both non-empty and falsy in PHP
278            | Atomic::TNumericString   // "0" is a valid numeric-string and is falsy
279            | Atomic::TKeyOf { .. }
280            | Atomic::TValueOf { .. }
281            | Atomic::TArray { .. }
282            | Atomic::TList { .. } => true,
283            // Non-empty collections always have at least one element — never falsy in PHP.
284            // TNonEmptyArray and TNonEmptyList are intentionally excluded here.
285
286            Atomic::TLiteralString(s) => s.as_ref().is_empty() || s.as_ref() == "0",
287
288            Atomic::TKeyedArray { properties, .. } => properties.is_empty(),
289
290            // An int range can be falsy (== 0) when 0 is within the bounds.
291            Atomic::TIntRange { min, max } => {
292                min.is_none_or(|lo| lo <= 0) && max.is_none_or(|hi| hi >= 0)
293            }
294            // non-negative-int includes 0
295            Atomic::TNonNegativeInt => true,
296
297            _ => false,
298        }
299    }
300
301    /// Whether this atomic type can ever evaluate to a truthy value.
302    pub fn can_be_truthy(&self) -> bool {
303        match self {
304            Atomic::TNever
305            | Atomic::TVoid
306            | Atomic::TNull
307            | Atomic::TFalse
308            | Atomic::TLiteralInt(0)
309            | Atomic::TLiteralFloat(0, 0) => false,
310            Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
311            // int<0, 0> always equals zero — never truthy.
312            Atomic::TIntRange {
313                min: Some(0),
314                max: Some(0),
315            } => false,
316            // array{} — a closed empty keyed array — is always falsy.
317            Atomic::TKeyedArray {
318                properties,
319                is_open,
320                ..
321            } if !is_open && properties.is_empty() => false,
322            _ => true,
323        }
324    }
325
326    /// Whether this atomic represents a numeric type (int, float, or numeric-string).
327    pub fn is_numeric(&self) -> bool {
328        matches!(
329            self,
330            Atomic::TInt
331                | Atomic::TLiteralInt(_)
332                | Atomic::TIntRange { .. }
333                | Atomic::TPositiveInt
334                | Atomic::TNegativeInt
335                | Atomic::TNonNegativeInt
336                | Atomic::TFloat
337                | Atomic::TIntegralFloat
338                | Atomic::TLiteralFloat(..)
339                | Atomic::TNumeric
340                | Atomic::TNumericString
341        )
342    }
343
344    /// Whether this atomic is an integer variant.
345    pub fn is_int(&self) -> bool {
346        matches!(
347            self,
348            Atomic::TInt
349                | Atomic::TLiteralInt(_)
350                | Atomic::TIntRange { .. }
351                | Atomic::TPositiveInt
352                | Atomic::TNegativeInt
353                | Atomic::TNonNegativeInt
354        )
355    }
356
357    /// Whether this atomic is a string variant.
358    pub fn is_string(&self) -> bool {
359        matches!(
360            self,
361            Atomic::TString
362                | Atomic::TLiteralString(_)
363                | Atomic::TCallableString
364                | Atomic::TClassString(_)
365                | Atomic::TNonEmptyString
366                | Atomic::TNumericString
367                | Atomic::TInterfaceString(_)
368                | Atomic::TEnumString
369                | Atomic::TTraitString
370        )
371    }
372
373    /// Whether this atomic is an array variant.
374    pub fn is_array(&self) -> bool {
375        matches!(
376            self,
377            Atomic::TArray { .. }
378                | Atomic::TList { .. }
379                | Atomic::TNonEmptyArray { .. }
380                | Atomic::TNonEmptyList { .. }
381                | Atomic::TKeyedArray { .. }
382        )
383    }
384
385    /// Whether this atomic is an object variant.
386    pub fn is_object(&self) -> bool {
387        matches!(
388            self,
389            Atomic::TObject
390                | Atomic::TNamedObject { .. }
391                | Atomic::TStaticObject { .. }
392                | Atomic::TSelf { .. }
393                | Atomic::TParent { .. }
394                | Atomic::TIntersection { .. }
395        )
396    }
397
398    /// Whether this atomic can never be an object instance — so `clone`-ing it
399    /// is invalid. Conservative: ambiguous atomics (`mixed`, `callable`,
400    /// `never`, conditionals, template params, enum cases, intersections,
401    /// object types) all return `false` so they never trigger a false positive.
402    pub fn is_definitely_non_object(&self) -> bool {
403        matches!(
404            self,
405            Atomic::TString
406                | Atomic::TLiteralString(_)
407                | Atomic::TCallableString
408                | Atomic::TClassString(_)
409                | Atomic::TNonEmptyString
410                | Atomic::TNumericString
411                | Atomic::TInterfaceString(_)
412                | Atomic::TEnumString
413                | Atomic::TTraitString
414                | Atomic::TInt
415                | Atomic::TLiteralInt(_)
416                | Atomic::TIntRange { .. }
417                | Atomic::TPositiveInt
418                | Atomic::TNegativeInt
419                | Atomic::TNonNegativeInt
420                | Atomic::TFloat
421                | Atomic::TIntegralFloat
422                | Atomic::TLiteralFloat(..)
423                | Atomic::TBool
424                | Atomic::TTrue
425                | Atomic::TFalse
426                | Atomic::TNull
427                | Atomic::TVoid
428                | Atomic::TArray { .. }
429                | Atomic::TList { .. }
430                | Atomic::TNonEmptyArray { .. }
431                | Atomic::TNonEmptyList { .. }
432                | Atomic::TKeyedArray { .. }
433                | Atomic::TScalar
434                | Atomic::TNumeric
435        )
436    }
437
438    /// Whether this atomic is a callable variant.
439    ///
440    /// `TCallableString` is included: it denotes a string *known* to be a
441    /// valid callable, so `!is_callable()` must remove it from the type.
442    pub fn is_callable(&self) -> bool {
443        matches!(
444            self,
445            Atomic::TCallable { .. } | Atomic::TClosure { .. } | Atomic::TCallableString
446        )
447    }
448
449    /// Returns the FQCN if this is a named object type.
450    pub fn named_object_fqcn(&self) -> Option<&str> {
451        match self {
452            Atomic::TNamedObject { fqcn, .. }
453            | Atomic::TStaticObject { fqcn }
454            | Atomic::TSelf { fqcn }
455            | Atomic::TParent { fqcn } => Some(fqcn.as_ref()),
456            _ => None,
457        }
458    }
459
460    /// A human-readable name for this type (used in error messages).
461    pub fn type_name(&self) -> &'static str {
462        match self {
463            Atomic::TString
464            | Atomic::TLiteralString(_)
465            | Atomic::TNonEmptyString
466            | Atomic::TNumericString => "string",
467            Atomic::TCallableString => "callable-string",
468            Atomic::TClassString(_) => "class-string",
469            Atomic::TInt | Atomic::TLiteralInt(_) | Atomic::TIntRange { .. } => "int",
470            Atomic::TPositiveInt => "positive-int",
471            Atomic::TNegativeInt => "negative-int",
472            Atomic::TNonNegativeInt => "non-negative-int",
473            Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..) => "float",
474            Atomic::TBool => "bool",
475            Atomic::TTrue => "true",
476            Atomic::TFalse => "false",
477            Atomic::TNull => "null",
478            Atomic::TVoid => "void",
479            Atomic::TNever => "never",
480            Atomic::TMixed => "mixed",
481            Atomic::TScalar => "scalar",
482            Atomic::TNumeric => "numeric",
483            Atomic::TObject => "object",
484            Atomic::TNamedObject { .. } => "object",
485            Atomic::TStaticObject { .. } => "static",
486            Atomic::TSelf { .. } => "self",
487            Atomic::TParent { .. } => "parent",
488            Atomic::TCallable { .. } => "callable",
489            Atomic::TClosure { .. } => "Closure",
490            Atomic::TArray { .. } => "array",
491            Atomic::TList { .. } => "list",
492            Atomic::TNonEmptyArray { .. } => "non-empty-array",
493            Atomic::TNonEmptyList { .. } => "non-empty-list",
494            Atomic::TKeyedArray { .. } => "array",
495            Atomic::TTemplateParam { .. } => "template-param",
496            Atomic::TKeyOf { .. } => "key-of",
497            Atomic::TValueOf { .. } => "value-of",
498            Atomic::TConditional { .. } => "conditional-type",
499            Atomic::TInterfaceString(_) => "interface-string",
500            Atomic::TEnumString => "enum-string",
501            Atomic::TTraitString => "trait-string",
502            Atomic::TLiteralEnumCase { .. } => "enum-case",
503            Atomic::TIntersection { .. } => "intersection",
504        }
505    }
506}
507
508// ---------------------------------------------------------------------------
509// Hash impls
510// ---------------------------------------------------------------------------
511
512impl Hash for FnParam {
513    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
514        self.name.hash(state);
515        self.ty.hash(state);
516        self.out_ty.hash(state);
517        self.default.hash(state);
518        self.is_variadic.hash(state);
519        self.is_byref.hash(state);
520        self.is_optional.hash(state);
521    }
522}
523
524/// Discriminant tags for `Atomic` used in the manual `Hash` impl below.
525#[allow(non_camel_case_types, clippy::enum_variant_names)]
526#[repr(u8)]
527enum AtomicTag {
528    TString = 0,
529    TLiteralString,
530    TCallableString,
531    TClassString,
532    TNonEmptyString,
533    TNumericString,
534    TInt,
535    TLiteralInt,
536    TIntRange,
537    TPositiveInt,
538    TNegativeInt,
539    TNonNegativeInt,
540    TFloat,
541    TIntegralFloat,
542    TLiteralFloat,
543    TBool,
544    TTrue,
545    TFalse,
546    TNull,
547    TVoid,
548    TNever,
549    TMixed,
550    TScalar,
551    TNumeric,
552    TObject,
553    TNamedObject,
554    TStaticObject,
555    TSelf,
556    TParent,
557    TCallable,
558    TClosure,
559    TArray,
560    TList,
561    TNonEmptyArray,
562    TNonEmptyList,
563    TKeyedArray,
564    TTemplateParam,
565    TKeyOf,
566    TValueOf,
567    TConditional,
568    TInterfaceString,
569    TEnumString,
570    TTraitString,
571    TLiteralEnumCase,
572    TIntersection,
573}
574
575impl Hash for Atomic {
576    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
577        use AtomicTag as T;
578        match self {
579            // --- tag-only variants ---
580            Atomic::TString => (T::TString as u8).hash(state),
581            Atomic::TCallableString => (T::TCallableString as u8).hash(state),
582            Atomic::TNonEmptyString => (T::TNonEmptyString as u8).hash(state),
583            Atomic::TNumericString => (T::TNumericString as u8).hash(state),
584            Atomic::TInt => (T::TInt as u8).hash(state),
585            Atomic::TPositiveInt => (T::TPositiveInt as u8).hash(state),
586            Atomic::TNegativeInt => (T::TNegativeInt as u8).hash(state),
587            Atomic::TNonNegativeInt => (T::TNonNegativeInt as u8).hash(state),
588            Atomic::TFloat => (T::TFloat as u8).hash(state),
589            Atomic::TIntegralFloat => (T::TIntegralFloat as u8).hash(state),
590            Atomic::TBool => (T::TBool as u8).hash(state),
591            Atomic::TTrue => (T::TTrue as u8).hash(state),
592            Atomic::TFalse => (T::TFalse as u8).hash(state),
593            Atomic::TNull => (T::TNull as u8).hash(state),
594            Atomic::TVoid => (T::TVoid as u8).hash(state),
595            Atomic::TNever => (T::TNever as u8).hash(state),
596            Atomic::TMixed => (T::TMixed as u8).hash(state),
597            Atomic::TScalar => (T::TScalar as u8).hash(state),
598            Atomic::TNumeric => (T::TNumeric as u8).hash(state),
599            Atomic::TObject => (T::TObject as u8).hash(state),
600            Atomic::TEnumString => (T::TEnumString as u8).hash(state),
601            Atomic::TTraitString => (T::TTraitString as u8).hash(state),
602
603            // --- variants with fields ---
604            Atomic::TLiteralString(s) => {
605                (T::TLiteralString as u8).hash(state);
606                s.hash(state);
607            }
608            Atomic::TClassString(opt) => {
609                (T::TClassString as u8).hash(state);
610                opt.hash(state);
611            }
612            Atomic::TInterfaceString(opt) => {
613                (T::TInterfaceString as u8).hash(state);
614                opt.hash(state);
615            }
616            Atomic::TLiteralInt(n) => {
617                (T::TLiteralInt as u8).hash(state);
618                n.hash(state);
619            }
620            Atomic::TIntRange { min, max } => {
621                (T::TIntRange as u8).hash(state);
622                min.hash(state);
623                max.hash(state);
624            }
625            Atomic::TLiteralFloat(int_bits, frac_bits) => {
626                (T::TLiteralFloat as u8).hash(state);
627                int_bits.hash(state);
628                frac_bits.hash(state);
629            }
630            Atomic::TNamedObject { fqcn, type_params } => {
631                (T::TNamedObject as u8).hash(state);
632                fqcn.hash(state);
633                type_params.hash(state);
634            }
635            Atomic::TStaticObject { fqcn } => {
636                (T::TStaticObject as u8).hash(state);
637                fqcn.hash(state);
638            }
639            Atomic::TSelf { fqcn } => {
640                (T::TSelf as u8).hash(state);
641                fqcn.hash(state);
642            }
643            Atomic::TParent { fqcn } => {
644                (T::TParent as u8).hash(state);
645                fqcn.hash(state);
646            }
647            Atomic::TCallable {
648                params,
649                return_type,
650            } => {
651                (T::TCallable as u8).hash(state);
652                params.hash(state);
653                return_type.hash(state);
654            }
655            Atomic::TClosure { data } => {
656                (T::TClosure as u8).hash(state);
657                data.hash(state);
658            }
659            Atomic::TArray { key, value } => {
660                (T::TArray as u8).hash(state);
661                key.hash(state);
662                value.hash(state);
663            }
664            Atomic::TList { value } => {
665                (T::TList as u8).hash(state);
666                value.hash(state);
667            }
668            Atomic::TNonEmptyArray { key, value } => {
669                (T::TNonEmptyArray as u8).hash(state);
670                key.hash(state);
671                value.hash(state);
672            }
673            Atomic::TNonEmptyList { value } => {
674                (T::TNonEmptyList as u8).hash(state);
675                value.hash(state);
676            }
677            Atomic::TKeyedArray {
678                properties,
679                is_open,
680                is_list,
681            } => {
682                (T::TKeyedArray as u8).hash(state);
683                // Combine per-entry hashes commutatively (wrapping add) so the
684                // result is order-independent, consistent with PartialEq which
685                // uses IndexMap value equality — without allocating and sorting.
686                let mut combined: u64 = 0;
687                for (k, v) in properties.iter() {
688                    let mut entry_hasher = rustc_hash::FxHasher::default();
689                    k.hash(&mut entry_hasher);
690                    v.hash(&mut entry_hasher);
691                    combined = combined.wrapping_add(std::hash::Hasher::finish(&entry_hasher));
692                }
693                combined.hash(state);
694                properties.len().hash(state);
695                is_open.hash(state);
696                is_list.hash(state);
697            }
698            Atomic::TTemplateParam {
699                name,
700                as_type,
701                defining_entity,
702            } => {
703                (T::TTemplateParam as u8).hash(state);
704                name.hash(state);
705                as_type.hash(state);
706                defining_entity.hash(state);
707            }
708            Atomic::TKeyOf { target } => {
709                (T::TKeyOf as u8).hash(state);
710                target.hash(state);
711            }
712            Atomic::TValueOf { target } => {
713                (T::TValueOf as u8).hash(state);
714                target.hash(state);
715            }
716            Atomic::TConditional { data } => {
717                (T::TConditional as u8).hash(state);
718                data.hash(state);
719            }
720            Atomic::TLiteralEnumCase {
721                enum_fqcn,
722                case_name,
723            } => {
724                (T::TLiteralEnumCase as u8).hash(state);
725                enum_fqcn.hash(state);
726                case_name.hash(state);
727            }
728            Atomic::TIntersection { parts } => {
729                (T::TIntersection as u8).hash(state);
730                parts.hash(state);
731            }
732        }
733    }
734}