use std::sync::Arc;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use crate::atomic::Atomic;
pub type AtomicVec = SmallVec<[Atomic; 2]>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Union {
pub types: AtomicVec,
pub possibly_undefined: bool,
pub from_docblock: bool,
}
impl Union {
pub fn empty() -> Self {
Self {
types: SmallVec::new(),
possibly_undefined: false,
from_docblock: false,
}
}
pub fn single(atomic: Atomic) -> Self {
let mut types = SmallVec::new();
types.push(atomic);
Self {
types,
possibly_undefined: false,
from_docblock: false,
}
}
pub fn mixed() -> Self {
Self::single(Atomic::TMixed)
}
pub fn void() -> Self {
Self::single(Atomic::TVoid)
}
pub fn never() -> Self {
Self::single(Atomic::TNever)
}
pub fn null() -> Self {
Self::single(Atomic::TNull)
}
pub fn bool() -> Self {
Self::single(Atomic::TBool)
}
pub fn int() -> Self {
Self::single(Atomic::TInt)
}
pub fn float() -> Self {
Self::single(Atomic::TFloat)
}
pub fn string() -> Self {
Self::single(Atomic::TString)
}
pub fn nullable(atomic: Atomic) -> Self {
let mut types = SmallVec::new();
types.push(atomic);
types.push(Atomic::TNull);
Self {
types,
possibly_undefined: false,
from_docblock: false,
}
}
pub fn from_vec(atomics: Vec<Atomic>) -> Self {
let mut u = Self::empty();
for a in atomics {
u.add_type(a);
}
u
}
pub fn is_empty(&self) -> bool {
self.types.is_empty()
}
pub fn is_single(&self) -> bool {
self.types.len() == 1
}
pub fn is_nullable(&self) -> bool {
self.types.iter().any(|t| matches!(t, Atomic::TNull))
}
pub fn is_mixed(&self) -> bool {
self.types.iter().any(|t| matches!(t, Atomic::TMixed))
}
pub fn is_never(&self) -> bool {
self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
}
pub fn is_void(&self) -> bool {
self.is_single() && matches!(self.types[0], Atomic::TVoid)
}
pub fn can_be_falsy(&self) -> bool {
self.types.iter().any(|t| t.can_be_falsy())
}
pub fn can_be_truthy(&self) -> bool {
self.types.iter().any(|t| t.can_be_truthy())
}
pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
self.types.iter().any(f)
}
pub fn has_named_object(&self, fqcn: &str) -> bool {
self.types.iter().any(|t| match t {
Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
_ => false,
})
}
pub fn add_type(&mut self, atomic: Atomic) {
if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
return;
}
if matches!(atomic, Atomic::TMixed) {
self.types.clear();
self.types.push(Atomic::TMixed);
return;
}
if self.types.contains(&atomic) {
return;
}
if let Atomic::TLiteralInt(_) = &atomic {
if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
return;
}
}
if let Atomic::TLiteralString(_) = &atomic {
if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
return;
}
}
if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
&& self.types.iter().any(|t| matches!(t, Atomic::TBool))
{
return;
}
if matches!(atomic, Atomic::TInt) {
self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
}
if matches!(atomic, Atomic::TString) {
self.types
.retain(|t| !matches!(t, Atomic::TLiteralString(_)));
}
if matches!(atomic, Atomic::TBool) {
self.types
.retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
}
self.types.push(atomic);
}
pub fn remove_null(&self) -> Union {
self.filter(|t| !matches!(t, Atomic::TNull))
}
pub fn remove_false(&self) -> Union {
self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
}
pub fn narrow_to_truthy(&self) -> Union {
if self.is_mixed() {
return Union::mixed();
}
let narrowed = self.filter(|t| t.can_be_truthy());
narrowed.filter(|t| match t {
Atomic::TLiteralInt(0) => false,
Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
Atomic::TLiteralFloat(0, 0) => false,
_ => true,
})
}
pub fn narrow_to_falsy(&self) -> Union {
if self.is_mixed() {
return Union::from_vec(vec![
Atomic::TNull,
Atomic::TFalse,
Atomic::TLiteralInt(0),
Atomic::TLiteralString("".into()),
]);
}
self.filter(|t| t.can_be_falsy())
}
pub fn narrow_instanceof(&self, class: &str) -> Union {
let narrowed_ty = Atomic::TNamedObject {
fqcn: class.into(),
type_params: vec![],
};
let has_object = self.types.iter().any(|t| {
matches!(
t,
Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
});
if has_object || self.is_empty() {
Union::single(narrowed_ty)
} else {
Union::single(narrowed_ty)
}
}
pub fn narrow_to_string(&self) -> Union {
self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
}
pub fn narrow_to_int(&self) -> Union {
self.filter(|t| {
t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
})
}
pub fn narrow_to_float(&self) -> Union {
self.filter(|t| {
matches!(
t,
Atomic::TFloat
| Atomic::TLiteralFloat(..)
| Atomic::TMixed
| Atomic::TScalar
| Atomic::TNumeric
)
})
}
pub fn narrow_to_bool(&self) -> Union {
self.filter(|t| {
matches!(
t,
Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
)
})
}
pub fn narrow_to_null(&self) -> Union {
self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
}
pub fn narrow_to_array(&self) -> Union {
self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
}
pub fn narrow_to_object(&self) -> Union {
self.filter(|t| t.is_object() || matches!(t, Atomic::TMixed))
}
pub fn narrow_to_callable(&self) -> Union {
self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
}
pub fn merge(a: &Union, b: &Union) -> Union {
let mut result = a.clone();
for atomic in &b.types {
result.add_type(atomic.clone());
}
result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
result
}
pub fn intersect_with(&self, other: &Union) -> Union {
if self.is_mixed() {
return other.clone();
}
if other.is_mixed() {
return self.clone();
}
let mut result = Union::empty();
for a in &self.types {
for b in &other.types {
if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
result.add_type(a.clone());
break;
}
}
}
if result.is_empty() {
other.clone()
} else {
result
}
}
pub fn substitute_templates(
&self,
bindings: &std::collections::HashMap<Arc<str>, Union>,
) -> Union {
if bindings.is_empty() {
return self.clone();
}
let mut result = Union::empty();
result.possibly_undefined = self.possibly_undefined;
result.from_docblock = self.from_docblock;
for atomic in &self.types {
match atomic {
Atomic::TTemplateParam { name, .. } => {
if let Some(resolved) = bindings.get(name) {
for t in &resolved.types {
result.add_type(t.clone());
}
} else {
result.add_type(atomic.clone());
}
}
Atomic::TArray { key, value } => {
result.add_type(Atomic::TArray {
key: Box::new(key.substitute_templates(bindings)),
value: Box::new(value.substitute_templates(bindings)),
});
}
Atomic::TList { value } => {
result.add_type(Atomic::TList {
value: Box::new(value.substitute_templates(bindings)),
});
}
Atomic::TNamedObject { fqcn, type_params } => {
if type_params.is_empty() && !fqcn.contains('\\') {
if let Some(resolved) = bindings.get(fqcn.as_ref()) {
for t in &resolved.types {
result.add_type(t.clone());
}
continue;
}
}
let new_params = type_params
.iter()
.map(|p| p.substitute_templates(bindings))
.collect();
result.add_type(Atomic::TNamedObject {
fqcn: fqcn.clone(),
type_params: new_params,
});
}
_ => {
result.add_type(atomic.clone());
}
}
}
result
}
pub fn is_subtype_of_simple(&self, other: &Union) -> bool {
if other.is_mixed() {
return true;
}
if self.is_never() {
return true; }
self.types
.iter()
.all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
}
fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
let mut result = Union::empty();
result.possibly_undefined = self.possibly_undefined;
result.from_docblock = self.from_docblock;
for atomic in &self.types {
if f(atomic) {
result.types.push(atomic.clone());
}
}
result
}
pub fn possibly_undefined(mut self) -> Self {
self.possibly_undefined = true;
self
}
pub fn from_docblock(mut self) -> Self {
self.from_docblock = true;
self
}
}
fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
if sub == sup {
return true;
}
match (sub, sup) {
(Atomic::TNever, _) => true,
(_, Atomic::TMixed) => true,
(Atomic::TMixed, _) => true,
(Atomic::TLiteralInt(_), Atomic::TInt) => true,
(Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
(Atomic::TLiteralInt(_), Atomic::TScalar) => true,
(Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
(Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
(Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
(Atomic::TPositiveInt, Atomic::TInt) => true,
(Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
(Atomic::TNegativeInt, Atomic::TInt) => true,
(Atomic::TNonNegativeInt, Atomic::TInt) => true,
(Atomic::TIntRange { .. }, Atomic::TInt) => true,
(Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
(Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
(Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
(Atomic::TLiteralString(s), Atomic::TString) => {
let _ = s;
true
}
(Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
(Atomic::TLiteralString(_), Atomic::TScalar) => true,
(Atomic::TNonEmptyString, Atomic::TString) => true,
(Atomic::TNumericString, Atomic::TString) => true,
(Atomic::TClassString(_), Atomic::TString) => true,
(Atomic::TInterfaceString, Atomic::TString) => true,
(Atomic::TEnumString, Atomic::TString) => true,
(Atomic::TTraitString, Atomic::TString) => true,
(Atomic::TTrue, Atomic::TBool) => true,
(Atomic::TFalse, Atomic::TBool) => true,
(Atomic::TInt, Atomic::TNumeric) => true,
(Atomic::TFloat, Atomic::TNumeric) => true,
(Atomic::TNumericString, Atomic::TNumeric) => true,
(Atomic::TInt, Atomic::TScalar) => true,
(Atomic::TFloat, Atomic::TScalar) => true,
(Atomic::TString, Atomic::TScalar) => true,
(Atomic::TBool, Atomic::TScalar) => true,
(Atomic::TNumeric, Atomic::TScalar) => true,
(Atomic::TTrue, Atomic::TScalar) => true,
(Atomic::TFalse, Atomic::TScalar) => true,
(Atomic::TNamedObject { .. }, Atomic::TObject) => true,
(Atomic::TStaticObject { .. }, Atomic::TObject) => true,
(Atomic::TSelf { .. }, Atomic::TObject) => true,
(Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
(Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
(Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
(Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
(Atomic::TLiteralInt(_), Atomic::TFloat) => true,
(Atomic::TPositiveInt, Atomic::TFloat) => true,
(Atomic::TInt, Atomic::TFloat) => true,
(Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
(Atomic::TString, Atomic::TCallable { .. }) => true,
(Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
(Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
(Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
(Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
(Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
(Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
(Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
(Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
(Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
fqcn.as_ref().eq_ignore_ascii_case("closure")
}
(Atomic::TClosure { .. }, Atomic::TObject) => true,
(Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
&& value.is_subtype_of_simple(av)
}
(Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
value.is_subtype_of_simple(lv)
}
(Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
&& av.is_subtype_of_simple(lv)
}
(Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
&& av.is_subtype_of_simple(lv)
}
(Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
&& av.is_subtype_of_simple(lv)
}
(Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
matches!(key.types.as_slice(), [Atomic::TInt] | [Atomic::TMixed])
&& av.is_subtype_of_simple(lv)
}
(Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
(Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
}
(Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
}
(Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
(
Atomic::TKeyedArray {
properties,
is_list,
..
},
Atomic::TList { value: lv },
) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
(
Atomic::TKeyedArray {
properties,
is_list,
..
},
Atomic::TNonEmptyList { value: lv },
) => {
*is_list
&& !properties.is_empty()
&& properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
}
(_, Atomic::TTemplateParam { .. }) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_is_single() {
let u = Union::single(Atomic::TString);
assert!(u.is_single());
assert!(!u.is_nullable());
}
#[test]
fn nullable_has_null() {
let u = Union::nullable(Atomic::TString);
assert!(u.is_nullable());
assert_eq!(u.types.len(), 2);
}
#[test]
fn add_type_deduplicates() {
let mut u = Union::single(Atomic::TString);
u.add_type(Atomic::TString);
assert_eq!(u.types.len(), 1);
}
#[test]
fn add_type_literal_subsumed_by_base() {
let mut u = Union::single(Atomic::TInt);
u.add_type(Atomic::TLiteralInt(42));
assert_eq!(u.types.len(), 1);
assert!(matches!(u.types[0], Atomic::TInt));
}
#[test]
fn add_type_base_widens_literals() {
let mut u = Union::single(Atomic::TLiteralInt(1));
u.add_type(Atomic::TLiteralInt(2));
u.add_type(Atomic::TInt);
assert_eq!(u.types.len(), 1);
assert!(matches!(u.types[0], Atomic::TInt));
}
#[test]
fn mixed_subsumes_everything() {
let mut u = Union::single(Atomic::TString);
u.add_type(Atomic::TMixed);
assert_eq!(u.types.len(), 1);
assert!(u.is_mixed());
}
#[test]
fn remove_null() {
let u = Union::nullable(Atomic::TString);
let narrowed = u.remove_null();
assert!(!narrowed.is_nullable());
assert_eq!(narrowed.types.len(), 1);
}
#[test]
fn narrow_to_truthy_removes_null_false() {
let mut u = Union::empty();
u.add_type(Atomic::TString);
u.add_type(Atomic::TNull);
u.add_type(Atomic::TFalse);
let truthy = u.narrow_to_truthy();
assert!(!truthy.is_nullable());
assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
}
#[test]
fn merge_combines_types() {
let a = Union::single(Atomic::TString);
let b = Union::single(Atomic::TInt);
let merged = Union::merge(&a, &b);
assert_eq!(merged.types.len(), 2);
}
#[test]
fn subtype_literal_int_under_int() {
let sub = Union::single(Atomic::TLiteralInt(5));
let sup = Union::single(Atomic::TInt);
assert!(sub.is_subtype_of_simple(&sup));
}
#[test]
fn subtype_never_is_bottom() {
let never = Union::never();
let string = Union::single(Atomic::TString);
assert!(never.is_subtype_of_simple(&string));
}
#[test]
fn subtype_everything_under_mixed() {
let string = Union::single(Atomic::TString);
let mixed = Union::mixed();
assert!(string.is_subtype_of_simple(&mixed));
}
#[test]
fn template_substitution() {
let mut bindings = std::collections::HashMap::new();
bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
let tmpl = Union::single(Atomic::TTemplateParam {
name: Arc::from("T"),
as_type: Box::new(Union::mixed()),
defining_entity: Arc::from("MyClass"),
});
let resolved = tmpl.substitute_templates(&bindings);
assert_eq!(resolved.types.len(), 1);
assert!(matches!(resolved.types[0], Atomic::TString));
}
}