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