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