use gdscript_api::{BuiltinId, ClassId, ElemRef, EngineApi, TyRef};
use smol_str::SmolStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ScriptRefId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SignalSigId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EnumRef {
pub qualified: SmolStr,
pub bitfield: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Ty {
Builtin(BuiltinId),
Object(ClassId),
ScriptRef(ScriptRefId),
Array(Box<Ty>),
Dict(Box<Ty>, Box<Ty>),
Enum(EnumRef),
Signal(Option<SignalSigId>),
Callable,
Void,
Variant,
Unknown,
Error,
}
impl Ty {
#[must_use]
pub fn array_of_variant() -> Self {
Self::Array(Box::new(Self::Variant))
}
#[must_use]
pub fn dict_of_variant() -> Self {
Self::Dict(Box::new(Self::Variant), Box::new(Self::Variant))
}
#[must_use]
pub fn is_variant(&self) -> bool {
matches!(self, Self::Variant)
}
#[must_use]
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
#[must_use]
pub fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
#[must_use]
pub fn is_uninformative(&self) -> bool {
matches!(self, Self::Variant | Self::Unknown | Self::Error)
}
#[must_use]
pub fn label(&self, api: &EngineApi) -> Option<String> {
Some(match self {
Self::Builtin(id) => api.builtin(*id).name.clone(),
Self::Object(id) => api.class(*id).name.clone(),
Self::Array(elem) => match elem.label(api) {
Some(e) if e != "Variant" => format!("Array[{e}]"),
_ => "Array".to_owned(),
},
Self::Dict(k, v) => match (k.label(api), v.label(api)) {
(Some(k), Some(v)) if k != "Variant" || v != "Variant" => {
format!("Dictionary[{k}, {v}]")
}
_ => "Dictionary".to_owned(),
},
Self::Enum(e) => e.qualified.to_string(),
Self::Signal(_) => "Signal".to_owned(),
Self::Callable => "Callable".to_owned(),
Self::Void => "void".to_owned(),
Self::Variant => "Variant".to_owned(),
Self::ScriptRef(_) | Self::Unknown | Self::Error => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TypeSource {
Undetected,
Inferred,
AnnotatedInferred,
AnnotatedExplicit,
}
impl TypeSource {
#[must_use]
pub fn is_hard(self) -> bool {
self > Self::Inferred
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypedBinding {
pub ty: Ty,
pub source: TypeSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Assign {
Ok,
OkUnsafe,
Narrowing,
IntAsEnum,
No,
}
#[must_use]
pub fn is_assignable(api: &EngineApi, from: &Ty, to: &Ty) -> Assign {
if to.is_variant() {
return Assign::Ok;
}
if matches!(from, Ty::Unknown | Ty::Error) || matches!(to, Ty::Unknown | Ty::Error) {
return Assign::Ok;
}
if from.is_variant() {
return Assign::OkUnsafe;
}
match to {
Ty::Builtin(to_id) => match from {
Ty::Builtin(from_id) if from_id == to_id => Assign::Ok,
Ty::Builtin(from_id) => {
let from_name = api.builtin(*from_id).name.as_str();
let to_name = api.builtin(*to_id).name.as_str();
match (from_name, to_name) {
("float", "int") => Assign::Narrowing, ("int", "float")
| ("String", "StringName" | "NodePath")
| ("StringName" | "NodePath", "String") => Assign::Ok,
_ => Assign::No,
}
}
Ty::Enum(_) if api.builtin(*to_id).name == "int" => Assign::Ok,
_ => Assign::No,
},
Ty::Enum(to_enum) => match from {
Ty::Enum(from_enum) if from_enum == to_enum => Assign::Ok,
Ty::Enum(_) => Assign::IntAsEnum,
Ty::Builtin(id) if api.builtin(*id).name == "int" => Assign::IntAsEnum,
_ => Assign::No,
},
Ty::Object(to_class) => match from {
Ty::Object(from_class) if api.is_subclass(*from_class, *to_class) => Assign::Ok,
Ty::Object(from_class) if api.is_subclass(*to_class, *from_class) => Assign::OkUnsafe,
Ty::ScriptRef(_) => Assign::Ok,
_ => Assign::No,
},
Ty::Array(to_elem) => match from {
Ty::Array(from_elem)
if from_elem == to_elem
|| from_elem.is_uninformative()
|| to_elem.is_uninformative() =>
{
Assign::Ok
}
_ => Assign::No,
},
Ty::Dict(to_k, to_v) => match from {
Ty::Dict(from_k, from_v)
if (from_k == to_k || from_k.is_uninformative() || to_k.is_uninformative())
&& (from_v == to_v || from_v.is_uninformative() || to_v.is_uninformative()) =>
{
Assign::Ok
}
_ => Assign::No,
},
Ty::Signal(_) => {
if matches!(from, Ty::Signal(_)) {
Assign::Ok
} else {
Assign::No
}
}
Ty::Callable => {
if matches!(from, Ty::Callable) {
Assign::Ok
} else {
Assign::No
}
}
Ty::Void => {
if matches!(from, Ty::Void) {
Assign::Ok
} else {
Assign::No
}
}
Ty::ScriptRef(_) | Ty::Variant | Ty::Unknown | Ty::Error => Assign::Ok,
}
}
#[must_use]
pub fn resolve_tyref(api: &EngineApi, tyref: &TyRef) -> Ty {
match tyref {
TyRef::Void => Ty::Void,
TyRef::Variant => Ty::Variant,
TyRef::Builtin(id) => match api.builtin(*id).name.as_str() {
"Callable" => Ty::Callable,
"Signal" => Ty::Signal(None),
"Array" => Ty::array_of_variant(),
"Dictionary" => Ty::dict_of_variant(),
_ => Ty::Builtin(*id),
},
TyRef::Class(id) => Ty::Object(*id),
TyRef::TypedArray(elem) => Ty::Array(Box::new(resolve_elemref(api, elem))),
TyRef::TypedDict(k, v) => Ty::Dict(
Box::new(resolve_elemref(api, k)),
Box::new(resolve_elemref(api, v)),
),
TyRef::Enum {
qualified,
bitfield,
} => Ty::Enum(EnumRef {
qualified: SmolStr::new(qualified),
bitfield: *bitfield,
}),
}
}
#[must_use]
pub fn resolve_elemref(_api: &EngineApi, elem: &ElemRef) -> Ty {
match elem {
ElemRef::Variant => Ty::Variant,
ElemRef::Builtin(id) => Ty::Builtin(*id),
ElemRef::Class(id) => Ty::Object(*id),
ElemRef::Enum {
qualified,
bitfield,
} => Ty::Enum(EnumRef {
qualified: SmolStr::new(qualified),
bitfield: *bitfield,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ty_of(api: &EngineApi, builtin: &str) -> Ty {
Ty::Builtin(api.builtin_by_name(builtin).expect("known builtin"))
}
#[test]
fn type_source_hardness() {
assert!(!TypeSource::Undetected.is_hard());
assert!(!TypeSource::Inferred.is_hard());
assert!(TypeSource::AnnotatedInferred.is_hard());
assert!(TypeSource::AnnotatedExplicit.is_hard());
}
#[test]
fn variant_and_seam_assignability() {
let api = gdscript_api::bundled();
let int = ty_of(api, "int");
assert_eq!(is_assignable(api, &int, &Ty::Variant), Assign::Ok);
assert_eq!(is_assignable(api, &Ty::Variant, &int), Assign::OkUnsafe);
assert_eq!(is_assignable(api, &Ty::Unknown, &int), Assign::Ok);
assert_eq!(is_assignable(api, &int, &Ty::Unknown), Assign::Ok);
assert_eq!(is_assignable(api, &Ty::Error, &int), Assign::Ok);
}
#[test]
fn numeric_conversions() {
let api = gdscript_api::bundled();
let int = ty_of(api, "int");
let float = ty_of(api, "float");
assert_eq!(is_assignable(api, &int, &float), Assign::Ok); assert_eq!(is_assignable(api, &float, &int), Assign::Narrowing);
assert_eq!(is_assignable(api, &int, &int), Assign::Ok);
let string = ty_of(api, "String");
assert_eq!(is_assignable(api, &string, &int), Assign::No);
}
#[test]
fn object_subclassing() {
let api = gdscript_api::bundled();
let node = Ty::Object(api.class_by_name("Node").unwrap());
let node2d = Ty::Object(api.class_by_name("Node2D").unwrap());
assert_eq!(is_assignable(api, &node2d, &node), Assign::Ok);
assert_eq!(is_assignable(api, &node, &node2d), Assign::OkUnsafe);
let s = ty_of(api, "String");
assert_eq!(is_assignable(api, &s, &node), Assign::No);
}
#[test]
fn arrays_are_invariant() {
let api = gdscript_api::bundled();
let int = ty_of(api, "int");
let float = ty_of(api, "float");
let arr_int = Ty::Array(Box::new(int.clone()));
let arr_int2 = Ty::Array(Box::new(int));
let arr_float = Ty::Array(Box::new(float));
assert_eq!(is_assignable(api, &arr_int, &arr_int2), Assign::Ok);
assert_eq!(is_assignable(api, &arr_int2, &arr_float), Assign::No);
}
#[test]
fn enum_int_bridge() {
let api = gdscript_api::bundled();
let int = ty_of(api, "int");
let e = Ty::Enum(EnumRef {
qualified: SmolStr::new("Node.ProcessMode"),
bitfield: false,
});
assert_eq!(is_assignable(api, &e, &int), Assign::Ok); assert_eq!(is_assignable(api, &int, &e), Assign::IntAsEnum); }
#[test]
fn label_elides_unknown() {
let api = gdscript_api::bundled();
assert_eq!(Ty::Unknown.label(api), None);
assert_eq!(ty_of(api, "int").label(api).as_deref(), Some("int"));
assert_eq!(
Ty::Array(Box::new(ty_of(api, "int"))).label(api).as_deref(),
Some("Array[int]")
);
}
}