1use gdscript_api::{BuiltinId, ClassId, ElemRef, EngineApi, TyRef};
11use smol_str::SmolStr;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct ScriptRefId(pub u32);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct SignalSigId(pub u32);
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub struct EnumRef {
28 pub qualified: SmolStr,
30 pub bitfield: bool,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub enum Ty {
37 Builtin(BuiltinId),
39 Object(ClassId),
41 ScriptRef(ScriptRefId),
43 Array(Box<Ty>),
45 Dict(Box<Ty>, Box<Ty>),
47 Enum(EnumRef),
49 Signal(Option<SignalSigId>),
51 Callable,
53 Void,
55 Variant,
57 Unknown,
60 Error,
62}
63
64impl Ty {
65 #[must_use]
67 pub fn array_of_variant() -> Self {
68 Self::Array(Box::new(Self::Variant))
69 }
70
71 #[must_use]
73 pub fn dict_of_variant() -> Self {
74 Self::Dict(Box::new(Self::Variant), Box::new(Self::Variant))
75 }
76
77 #[must_use]
79 pub fn is_variant(&self) -> bool {
80 matches!(self, Self::Variant)
81 }
82
83 #[must_use]
85 pub fn is_unknown(&self) -> bool {
86 matches!(self, Self::Unknown)
87 }
88
89 #[must_use]
91 pub fn is_error(&self) -> bool {
92 matches!(self, Self::Error)
93 }
94
95 #[must_use]
98 pub fn is_uninformative(&self) -> bool {
99 matches!(self, Self::Variant | Self::Unknown | Self::Error)
100 }
101
102 #[must_use]
105 pub fn label(&self, api: &EngineApi) -> Option<String> {
106 Some(match self {
107 Self::Builtin(id) => api.builtin(*id).name.clone(),
108 Self::Object(id) => api.class(*id).name.clone(),
109 Self::Array(elem) => match elem.label(api) {
110 Some(e) if e != "Variant" => format!("Array[{e}]"),
111 _ => "Array".to_owned(),
112 },
113 Self::Dict(k, v) => match (k.label(api), v.label(api)) {
114 (Some(k), Some(v)) if k != "Variant" || v != "Variant" => {
115 format!("Dictionary[{k}, {v}]")
116 }
117 _ => "Dictionary".to_owned(),
118 },
119 Self::Enum(e) => e.qualified.to_string(),
120 Self::Signal(_) => "Signal".to_owned(),
121 Self::Callable => "Callable".to_owned(),
122 Self::Void => "void".to_owned(),
123 Self::Variant => "Variant".to_owned(),
124 Self::ScriptRef(_) | Self::Unknown | Self::Error => return None,
126 })
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
133pub enum TypeSource {
134 Undetected,
136 Inferred,
139 AnnotatedInferred,
141 AnnotatedExplicit,
143}
144
145impl TypeSource {
146 #[must_use]
149 pub fn is_hard(self) -> bool {
150 self > Self::Inferred
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct TypedBinding {
157 pub ty: Ty,
159 pub source: TypeSource,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Assign {
167 Ok,
169 OkUnsafe,
171 Narrowing,
173 IntAsEnum,
175 No,
177}
178
179#[must_use]
182pub fn is_assignable(api: &EngineApi, from: &Ty, to: &Ty) -> Assign {
183 if to.is_variant() {
185 return Assign::Ok;
186 }
187 if matches!(from, Ty::Unknown | Ty::Error) || matches!(to, Ty::Unknown | Ty::Error) {
190 return Assign::Ok;
191 }
192 if from.is_variant() {
193 return Assign::OkUnsafe;
194 }
195
196 match to {
197 Ty::Builtin(to_id) => match from {
198 Ty::Builtin(from_id) if from_id == to_id => Assign::Ok,
199 Ty::Builtin(from_id) => {
200 let from_name = api.builtin(*from_id).name.as_str();
201 let to_name = api.builtin(*to_id).name.as_str();
202 match (from_name, to_name) {
203 ("float", "int") => Assign::Narrowing, ("int", "float")
206 | ("String", "StringName" | "NodePath")
207 | ("StringName" | "NodePath", "String") => Assign::Ok,
208 _ => Assign::No,
209 }
210 }
211 Ty::Enum(_) if api.builtin(*to_id).name == "int" => Assign::Ok,
213 _ => Assign::No,
214 },
215 Ty::Enum(to_enum) => match from {
216 Ty::Enum(from_enum) if from_enum == to_enum => Assign::Ok,
217 Ty::Enum(_) => Assign::IntAsEnum,
220 Ty::Builtin(id) if api.builtin(*id).name == "int" => Assign::IntAsEnum,
222 _ => Assign::No,
223 },
224 Ty::Object(to_class) => match from {
225 Ty::Object(from_class) if api.is_subclass(*from_class, *to_class) => Assign::Ok,
226 Ty::Object(from_class) if api.is_subclass(*to_class, *from_class) => Assign::OkUnsafe,
229 Ty::ScriptRef(_) => Assign::Ok,
231 _ => Assign::No,
232 },
233 Ty::Array(to_elem) => match from {
238 Ty::Array(from_elem)
239 if from_elem == to_elem
240 || from_elem.is_uninformative()
241 || to_elem.is_uninformative() =>
242 {
243 Assign::Ok
244 }
245 _ => Assign::No,
246 },
247 Ty::Dict(to_k, to_v) => match from {
248 Ty::Dict(from_k, from_v)
249 if (from_k == to_k || from_k.is_uninformative() || to_k.is_uninformative())
250 && (from_v == to_v || from_v.is_uninformative() || to_v.is_uninformative()) =>
251 {
252 Assign::Ok
253 }
254 _ => Assign::No,
255 },
256 Ty::Signal(_) => {
257 if matches!(from, Ty::Signal(_)) {
258 Assign::Ok
259 } else {
260 Assign::No
261 }
262 }
263 Ty::Callable => {
264 if matches!(from, Ty::Callable) {
265 Assign::Ok
266 } else {
267 Assign::No
268 }
269 }
270 Ty::Void => {
271 if matches!(from, Ty::Void) {
272 Assign::Ok
273 } else {
274 Assign::No
275 }
276 }
277 Ty::ScriptRef(_) | Ty::Variant | Ty::Unknown | Ty::Error => Assign::Ok,
280 }
281}
282
283#[must_use]
285pub fn resolve_tyref(api: &EngineApi, tyref: &TyRef) -> Ty {
286 match tyref {
287 TyRef::Void => Ty::Void,
288 TyRef::Variant => Ty::Variant,
289 TyRef::Builtin(id) => match api.builtin(*id).name.as_str() {
293 "Callable" => Ty::Callable,
294 "Signal" => Ty::Signal(None),
295 "Array" => Ty::array_of_variant(),
296 "Dictionary" => Ty::dict_of_variant(),
297 _ => Ty::Builtin(*id),
298 },
299 TyRef::Class(id) => Ty::Object(*id),
300 TyRef::TypedArray(elem) => Ty::Array(Box::new(resolve_elemref(api, elem))),
301 TyRef::TypedDict(k, v) => Ty::Dict(
302 Box::new(resolve_elemref(api, k)),
303 Box::new(resolve_elemref(api, v)),
304 ),
305 TyRef::Enum {
306 qualified,
307 bitfield,
308 } => Ty::Enum(EnumRef {
309 qualified: SmolStr::new(qualified),
310 bitfield: *bitfield,
311 }),
312 }
313}
314
315#[must_use]
317pub fn resolve_elemref(_api: &EngineApi, elem: &ElemRef) -> Ty {
318 match elem {
319 ElemRef::Variant => Ty::Variant,
320 ElemRef::Builtin(id) => Ty::Builtin(*id),
321 ElemRef::Class(id) => Ty::Object(*id),
322 ElemRef::Enum {
323 qualified,
324 bitfield,
325 } => Ty::Enum(EnumRef {
326 qualified: SmolStr::new(qualified),
327 bitfield: *bitfield,
328 }),
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 fn ty_of(api: &EngineApi, builtin: &str) -> Ty {
337 Ty::Builtin(api.builtin_by_name(builtin).expect("known builtin"))
338 }
339
340 #[test]
341 fn type_source_hardness() {
342 assert!(!TypeSource::Undetected.is_hard());
343 assert!(!TypeSource::Inferred.is_hard());
344 assert!(TypeSource::AnnotatedInferred.is_hard());
345 assert!(TypeSource::AnnotatedExplicit.is_hard());
346 }
347
348 #[test]
349 fn variant_and_seam_assignability() {
350 let api = gdscript_api::bundled();
351 let int = ty_of(api, "int");
352 assert_eq!(is_assignable(api, &int, &Ty::Variant), Assign::Ok);
354 assert_eq!(is_assignable(api, &Ty::Variant, &int), Assign::OkUnsafe);
355 assert_eq!(is_assignable(api, &Ty::Unknown, &int), Assign::Ok);
357 assert_eq!(is_assignable(api, &int, &Ty::Unknown), Assign::Ok);
358 assert_eq!(is_assignable(api, &Ty::Error, &int), Assign::Ok);
359 }
360
361 #[test]
362 fn numeric_conversions() {
363 let api = gdscript_api::bundled();
364 let int = ty_of(api, "int");
365 let float = ty_of(api, "float");
366 assert_eq!(is_assignable(api, &int, &float), Assign::Ok); assert_eq!(is_assignable(api, &float, &int), Assign::Narrowing);
368 assert_eq!(is_assignable(api, &int, &int), Assign::Ok);
369 let string = ty_of(api, "String");
370 assert_eq!(is_assignable(api, &string, &int), Assign::No);
371 }
372
373 #[test]
374 fn object_subclassing() {
375 let api = gdscript_api::bundled();
376 let node = Ty::Object(api.class_by_name("Node").unwrap());
377 let node2d = Ty::Object(api.class_by_name("Node2D").unwrap());
378 assert_eq!(is_assignable(api, &node2d, &node), Assign::Ok);
381 assert_eq!(is_assignable(api, &node, &node2d), Assign::OkUnsafe);
382 let s = ty_of(api, "String");
384 assert_eq!(is_assignable(api, &s, &node), Assign::No);
385 }
386
387 #[test]
388 fn arrays_are_invariant() {
389 let api = gdscript_api::bundled();
390 let int = ty_of(api, "int");
391 let float = ty_of(api, "float");
392 let arr_int = Ty::Array(Box::new(int.clone()));
393 let arr_int2 = Ty::Array(Box::new(int));
394 let arr_float = Ty::Array(Box::new(float));
395 assert_eq!(is_assignable(api, &arr_int, &arr_int2), Assign::Ok);
396 assert_eq!(is_assignable(api, &arr_int2, &arr_float), Assign::No);
398 }
399
400 #[test]
401 fn enum_int_bridge() {
402 let api = gdscript_api::bundled();
403 let int = ty_of(api, "int");
404 let e = Ty::Enum(EnumRef {
405 qualified: SmolStr::new("Node.ProcessMode"),
406 bitfield: false,
407 });
408 assert_eq!(is_assignable(api, &e, &int), Assign::Ok); assert_eq!(is_assignable(api, &int, &e), Assign::IntAsEnum); }
411
412 #[test]
413 fn label_elides_unknown() {
414 let api = gdscript_api::bundled();
415 assert_eq!(Ty::Unknown.label(api), None);
416 assert_eq!(ty_of(api, "int").label(api).as_deref(), Some("int"));
417 assert_eq!(
418 Ty::Array(Box::new(ty_of(api, "int"))).label(api).as_deref(),
419 Some("Array[int]")
420 );
421 }
422}