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