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