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    /// `($param is TypeName ? A : B)` — conditional type.
237    /// Payload boxed to keep `Atomic` at 32 bytes (see [`ConditionalData`]).
238    TConditional { data: Box<ConditionalData> },
239
240    // --- Special object strings ---
241    /// `interface-string` or `interface-string<T>`
242    TInterfaceString(Option<Name>),
243    /// `enum-string`
244    TEnumString,
245    /// `trait-string`
246    TTraitString,
247
248    // --- Enum cases ---
249    /// `EnumName::CaseName` — a specific enum case literal
250    TLiteralEnumCase { enum_fqcn: Name, case_name: Name },
251
252    // --- Intersection ---
253    /// `A&B&C` — PHP 8.1+ pure intersection type
254    TIntersection { parts: Arc<[Type]> },
255}
256
257impl Atomic {
258    /// Whether this atomic type can ever evaluate to a falsy value.
259    pub fn can_be_falsy(&self) -> bool {
260        match self {
261            Atomic::TNull
262            | Atomic::TFalse
263            | Atomic::TBool
264            | Atomic::TNever
265            | Atomic::TLiteralInt(0)
266            | Atomic::TLiteralFloat(0, 0)
267            | Atomic::TInt
268            | Atomic::TFloat
269            | Atomic::TNumeric
270            | Atomic::TScalar
271            | Atomic::TMixed
272            | Atomic::TString
273            | Atomic::TNonEmptyString  // "0" is both non-empty and falsy in PHP
274            | Atomic::TNumericString   // "0" is a valid numeric-string and is falsy
275            | Atomic::TArray { .. }
276            | Atomic::TList { .. } => true,
277            // Non-empty collections always have at least one element — never falsy in PHP.
278            // TNonEmptyArray and TNonEmptyList are intentionally excluded here.
279
280            Atomic::TLiteralString(s) => s.as_ref().is_empty() || s.as_ref() == "0",
281
282            Atomic::TKeyedArray { properties, .. } => properties.is_empty(),
283
284            // An int range can be falsy (== 0) when 0 is within the bounds.
285            Atomic::TIntRange { min, max } => {
286                min.is_none_or(|lo| lo <= 0) && max.is_none_or(|hi| hi >= 0)
287            }
288            // non-negative-int includes 0
289            Atomic::TNonNegativeInt => true,
290
291            _ => false,
292        }
293    }
294
295    /// Whether this atomic type can ever evaluate to a truthy value.
296    pub fn can_be_truthy(&self) -> bool {
297        match self {
298            Atomic::TNever
299            | Atomic::TVoid
300            | Atomic::TNull
301            | Atomic::TFalse
302            | Atomic::TLiteralInt(0)
303            | Atomic::TLiteralFloat(0, 0) => false,
304            Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
305            // int<0, 0> always equals zero — never truthy.
306            Atomic::TIntRange {
307                min: Some(0),
308                max: Some(0),
309            } => false,
310            // array{} — a closed empty keyed array — is always falsy.
311            Atomic::TKeyedArray {
312                properties,
313                is_open,
314                ..
315            } if !is_open && properties.is_empty() => false,
316            _ => true,
317        }
318    }
319
320    /// Whether this atomic represents a numeric type (int, float, or numeric-string).
321    pub fn is_numeric(&self) -> bool {
322        matches!(
323            self,
324            Atomic::TInt
325                | Atomic::TLiteralInt(_)
326                | Atomic::TIntRange { .. }
327                | Atomic::TPositiveInt
328                | Atomic::TNegativeInt
329                | Atomic::TNonNegativeInt
330                | Atomic::TFloat
331                | Atomic::TIntegralFloat
332                | Atomic::TLiteralFloat(..)
333                | Atomic::TNumeric
334                | Atomic::TNumericString
335        )
336    }
337
338    /// Whether this atomic is an integer variant.
339    pub fn is_int(&self) -> bool {
340        matches!(
341            self,
342            Atomic::TInt
343                | Atomic::TLiteralInt(_)
344                | Atomic::TIntRange { .. }
345                | Atomic::TPositiveInt
346                | Atomic::TNegativeInt
347                | Atomic::TNonNegativeInt
348        )
349    }
350
351    /// Whether this atomic is a string variant.
352    pub fn is_string(&self) -> bool {
353        matches!(
354            self,
355            Atomic::TString
356                | Atomic::TLiteralString(_)
357                | Atomic::TCallableString
358                | Atomic::TClassString(_)
359                | Atomic::TNonEmptyString
360                | Atomic::TNumericString
361                | Atomic::TInterfaceString(_)
362                | Atomic::TEnumString
363                | Atomic::TTraitString
364        )
365    }
366
367    /// Whether this atomic is an array variant.
368    pub fn is_array(&self) -> bool {
369        matches!(
370            self,
371            Atomic::TArray { .. }
372                | Atomic::TList { .. }
373                | Atomic::TNonEmptyArray { .. }
374                | Atomic::TNonEmptyList { .. }
375                | Atomic::TKeyedArray { .. }
376        )
377    }
378
379    /// Whether this atomic is an object variant.
380    pub fn is_object(&self) -> bool {
381        matches!(
382            self,
383            Atomic::TObject
384                | Atomic::TNamedObject { .. }
385                | Atomic::TStaticObject { .. }
386                | Atomic::TSelf { .. }
387                | Atomic::TParent { .. }
388                | Atomic::TIntersection { .. }
389        )
390    }
391
392    /// Whether this atomic can never be an object instance — so `clone`-ing it
393    /// is invalid. Conservative: ambiguous atomics (`mixed`, `callable`,
394    /// `never`, conditionals, template params, enum cases, intersections,
395    /// object types) all return `false` so they never trigger a false positive.
396    pub fn is_definitely_non_object(&self) -> bool {
397        matches!(
398            self,
399            Atomic::TString
400                | Atomic::TLiteralString(_)
401                | Atomic::TCallableString
402                | Atomic::TClassString(_)
403                | Atomic::TNonEmptyString
404                | Atomic::TNumericString
405                | Atomic::TInterfaceString(_)
406                | Atomic::TEnumString
407                | Atomic::TTraitString
408                | Atomic::TInt
409                | Atomic::TLiteralInt(_)
410                | Atomic::TIntRange { .. }
411                | Atomic::TPositiveInt
412                | Atomic::TNegativeInt
413                | Atomic::TNonNegativeInt
414                | Atomic::TFloat
415                | Atomic::TIntegralFloat
416                | Atomic::TLiteralFloat(..)
417                | Atomic::TBool
418                | Atomic::TTrue
419                | Atomic::TFalse
420                | Atomic::TNull
421                | Atomic::TVoid
422                | Atomic::TArray { .. }
423                | Atomic::TList { .. }
424                | Atomic::TNonEmptyArray { .. }
425                | Atomic::TNonEmptyList { .. }
426                | Atomic::TKeyedArray { .. }
427                | Atomic::TScalar
428                | Atomic::TNumeric
429        )
430    }
431
432    /// Whether this atomic is a callable variant.
433    ///
434    /// `TCallableString` is included: it denotes a string *known* to be a
435    /// valid callable, so `!is_callable()` must remove it from the type.
436    pub fn is_callable(&self) -> bool {
437        matches!(
438            self,
439            Atomic::TCallable { .. } | Atomic::TClosure { .. } | Atomic::TCallableString
440        )
441    }
442
443    /// Returns the FQCN if this is a named object type.
444    pub fn named_object_fqcn(&self) -> Option<&str> {
445        match self {
446            Atomic::TNamedObject { fqcn, .. }
447            | Atomic::TStaticObject { fqcn }
448            | Atomic::TSelf { fqcn }
449            | Atomic::TParent { fqcn } => Some(fqcn.as_ref()),
450            _ => None,
451        }
452    }
453
454    /// A human-readable name for this type (used in error messages).
455    pub fn type_name(&self) -> &'static str {
456        match self {
457            Atomic::TString
458            | Atomic::TLiteralString(_)
459            | Atomic::TNonEmptyString
460            | Atomic::TNumericString => "string",
461            Atomic::TCallableString => "callable-string",
462            Atomic::TClassString(_) => "class-string",
463            Atomic::TInt | Atomic::TLiteralInt(_) | Atomic::TIntRange { .. } => "int",
464            Atomic::TPositiveInt => "positive-int",
465            Atomic::TNegativeInt => "negative-int",
466            Atomic::TNonNegativeInt => "non-negative-int",
467            Atomic::TFloat | Atomic::TIntegralFloat | Atomic::TLiteralFloat(..) => "float",
468            Atomic::TBool => "bool",
469            Atomic::TTrue => "true",
470            Atomic::TFalse => "false",
471            Atomic::TNull => "null",
472            Atomic::TVoid => "void",
473            Atomic::TNever => "never",
474            Atomic::TMixed => "mixed",
475            Atomic::TScalar => "scalar",
476            Atomic::TNumeric => "numeric",
477            Atomic::TObject => "object",
478            Atomic::TNamedObject { .. } => "object",
479            Atomic::TStaticObject { .. } => "static",
480            Atomic::TSelf { .. } => "self",
481            Atomic::TParent { .. } => "parent",
482            Atomic::TCallable { .. } => "callable",
483            Atomic::TClosure { .. } => "Closure",
484            Atomic::TArray { .. } => "array",
485            Atomic::TList { .. } => "list",
486            Atomic::TNonEmptyArray { .. } => "non-empty-array",
487            Atomic::TNonEmptyList { .. } => "non-empty-list",
488            Atomic::TKeyedArray { .. } => "array",
489            Atomic::TTemplateParam { .. } => "template-param",
490            Atomic::TConditional { .. } => "conditional-type",
491            Atomic::TInterfaceString(_) => "interface-string",
492            Atomic::TEnumString => "enum-string",
493            Atomic::TTraitString => "trait-string",
494            Atomic::TLiteralEnumCase { .. } => "enum-case",
495            Atomic::TIntersection { .. } => "intersection",
496        }
497    }
498}
499
500// ---------------------------------------------------------------------------
501// Hash impls
502// ---------------------------------------------------------------------------
503
504impl Hash for FnParam {
505    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
506        self.name.hash(state);
507        self.ty.hash(state);
508        self.out_ty.hash(state);
509        self.default.hash(state);
510        self.is_variadic.hash(state);
511        self.is_byref.hash(state);
512        self.is_optional.hash(state);
513    }
514}
515
516/// Discriminant tags for `Atomic` used in the manual `Hash` impl below.
517#[allow(non_camel_case_types, clippy::enum_variant_names)]
518#[repr(u8)]
519enum AtomicTag {
520    TString = 0,
521    TLiteralString,
522    TCallableString,
523    TClassString,
524    TNonEmptyString,
525    TNumericString,
526    TInt,
527    TLiteralInt,
528    TIntRange,
529    TPositiveInt,
530    TNegativeInt,
531    TNonNegativeInt,
532    TFloat,
533    TIntegralFloat,
534    TLiteralFloat,
535    TBool,
536    TTrue,
537    TFalse,
538    TNull,
539    TVoid,
540    TNever,
541    TMixed,
542    TScalar,
543    TNumeric,
544    TObject,
545    TNamedObject,
546    TStaticObject,
547    TSelf,
548    TParent,
549    TCallable,
550    TClosure,
551    TArray,
552    TList,
553    TNonEmptyArray,
554    TNonEmptyList,
555    TKeyedArray,
556    TTemplateParam,
557    TConditional,
558    TInterfaceString,
559    TEnumString,
560    TTraitString,
561    TLiteralEnumCase,
562    TIntersection,
563}
564
565impl Hash for Atomic {
566    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
567        use AtomicTag as T;
568        match self {
569            // --- tag-only variants ---
570            Atomic::TString => (T::TString as u8).hash(state),
571            Atomic::TCallableString => (T::TCallableString as u8).hash(state),
572            Atomic::TNonEmptyString => (T::TNonEmptyString as u8).hash(state),
573            Atomic::TNumericString => (T::TNumericString as u8).hash(state),
574            Atomic::TInt => (T::TInt as u8).hash(state),
575            Atomic::TPositiveInt => (T::TPositiveInt as u8).hash(state),
576            Atomic::TNegativeInt => (T::TNegativeInt as u8).hash(state),
577            Atomic::TNonNegativeInt => (T::TNonNegativeInt as u8).hash(state),
578            Atomic::TFloat => (T::TFloat as u8).hash(state),
579            Atomic::TIntegralFloat => (T::TIntegralFloat as u8).hash(state),
580            Atomic::TBool => (T::TBool as u8).hash(state),
581            Atomic::TTrue => (T::TTrue as u8).hash(state),
582            Atomic::TFalse => (T::TFalse as u8).hash(state),
583            Atomic::TNull => (T::TNull as u8).hash(state),
584            Atomic::TVoid => (T::TVoid as u8).hash(state),
585            Atomic::TNever => (T::TNever as u8).hash(state),
586            Atomic::TMixed => (T::TMixed as u8).hash(state),
587            Atomic::TScalar => (T::TScalar as u8).hash(state),
588            Atomic::TNumeric => (T::TNumeric as u8).hash(state),
589            Atomic::TObject => (T::TObject as u8).hash(state),
590            Atomic::TEnumString => (T::TEnumString as u8).hash(state),
591            Atomic::TTraitString => (T::TTraitString as u8).hash(state),
592
593            // --- variants with fields ---
594            Atomic::TLiteralString(s) => {
595                (T::TLiteralString as u8).hash(state);
596                s.hash(state);
597            }
598            Atomic::TClassString(opt) => {
599                (T::TClassString as u8).hash(state);
600                opt.hash(state);
601            }
602            Atomic::TInterfaceString(opt) => {
603                (T::TInterfaceString as u8).hash(state);
604                opt.hash(state);
605            }
606            Atomic::TLiteralInt(n) => {
607                (T::TLiteralInt as u8).hash(state);
608                n.hash(state);
609            }
610            Atomic::TIntRange { min, max } => {
611                (T::TIntRange as u8).hash(state);
612                min.hash(state);
613                max.hash(state);
614            }
615            Atomic::TLiteralFloat(int_bits, frac_bits) => {
616                (T::TLiteralFloat as u8).hash(state);
617                int_bits.hash(state);
618                frac_bits.hash(state);
619            }
620            Atomic::TNamedObject { fqcn, type_params } => {
621                (T::TNamedObject as u8).hash(state);
622                fqcn.hash(state);
623                type_params.hash(state);
624            }
625            Atomic::TStaticObject { fqcn } => {
626                (T::TStaticObject as u8).hash(state);
627                fqcn.hash(state);
628            }
629            Atomic::TSelf { fqcn } => {
630                (T::TSelf as u8).hash(state);
631                fqcn.hash(state);
632            }
633            Atomic::TParent { fqcn } => {
634                (T::TParent as u8).hash(state);
635                fqcn.hash(state);
636            }
637            Atomic::TCallable {
638                params,
639                return_type,
640            } => {
641                (T::TCallable as u8).hash(state);
642                params.hash(state);
643                return_type.hash(state);
644            }
645            Atomic::TClosure { data } => {
646                (T::TClosure as u8).hash(state);
647                data.hash(state);
648            }
649            Atomic::TArray { key, value } => {
650                (T::TArray as u8).hash(state);
651                key.hash(state);
652                value.hash(state);
653            }
654            Atomic::TList { value } => {
655                (T::TList as u8).hash(state);
656                value.hash(state);
657            }
658            Atomic::TNonEmptyArray { key, value } => {
659                (T::TNonEmptyArray as u8).hash(state);
660                key.hash(state);
661                value.hash(state);
662            }
663            Atomic::TNonEmptyList { value } => {
664                (T::TNonEmptyList as u8).hash(state);
665                value.hash(state);
666            }
667            Atomic::TKeyedArray {
668                properties,
669                is_open,
670                is_list,
671            } => {
672                (T::TKeyedArray as u8).hash(state);
673                // Combine per-entry hashes commutatively (wrapping add) so the
674                // result is order-independent, consistent with PartialEq which
675                // uses IndexMap value equality — without allocating and sorting.
676                let mut combined: u64 = 0;
677                for (k, v) in properties.iter() {
678                    let mut entry_hasher = rustc_hash::FxHasher::default();
679                    k.hash(&mut entry_hasher);
680                    v.hash(&mut entry_hasher);
681                    combined = combined.wrapping_add(std::hash::Hasher::finish(&entry_hasher));
682                }
683                combined.hash(state);
684                properties.len().hash(state);
685                is_open.hash(state);
686                is_list.hash(state);
687            }
688            Atomic::TTemplateParam {
689                name,
690                as_type,
691                defining_entity,
692            } => {
693                (T::TTemplateParam as u8).hash(state);
694                name.hash(state);
695                as_type.hash(state);
696                defining_entity.hash(state);
697            }
698            Atomic::TConditional { data } => {
699                (T::TConditional as u8).hash(state);
700                data.hash(state);
701            }
702            Atomic::TLiteralEnumCase {
703                enum_fqcn,
704                case_name,
705            } => {
706                (T::TLiteralEnumCase as u8).hash(state);
707                enum_fqcn.hash(state);
708                case_name.hash(state);
709            }
710            Atomic::TIntersection { parts } => {
711                (T::TIntersection as u8).hash(state);
712                parts.hash(state);
713            }
714        }
715    }
716}