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::Builtin(id) if api.builtin(*id).name == "int" => Assign::IntAsEnum,
219 _ => Assign::No,
220 },
221 Ty::Object(to_class) => match from {
222 Ty::Object(from_class) if api.is_subclass(*from_class, *to_class) => Assign::Ok,
223 Ty::Object(from_class) if api.is_subclass(*to_class, *from_class) => Assign::OkUnsafe,
226 Ty::ScriptRef(_) => Assign::Ok,
228 _ => Assign::No,
229 },
230 Ty::Array(to_elem) => match from {
235 Ty::Array(from_elem)
236 if from_elem == to_elem
237 || from_elem.is_uninformative()
238 || to_elem.is_uninformative() =>
239 {
240 Assign::Ok
241 }
242 _ => Assign::No,
243 },
244 Ty::Dict(to_k, to_v) => match from {
245 Ty::Dict(from_k, from_v)
246 if (from_k == to_k || from_k.is_uninformative() || to_k.is_uninformative())
247 && (from_v == to_v || from_v.is_uninformative() || to_v.is_uninformative()) =>
248 {
249 Assign::Ok
250 }
251 _ => Assign::No,
252 },
253 Ty::Signal(_) => {
254 if matches!(from, Ty::Signal(_)) {
255 Assign::Ok
256 } else {
257 Assign::No
258 }
259 }
260 Ty::Callable => {
261 if matches!(from, Ty::Callable) {
262 Assign::Ok
263 } else {
264 Assign::No
265 }
266 }
267 Ty::Void => {
268 if matches!(from, Ty::Void) {
269 Assign::Ok
270 } else {
271 Assign::No
272 }
273 }
274 Ty::ScriptRef(_) | Ty::Variant | Ty::Unknown | Ty::Error => Assign::Ok,
277 }
278}
279
280#[must_use]
282pub fn resolve_tyref(api: &EngineApi, tyref: &TyRef) -> Ty {
283 match tyref {
284 TyRef::Void => Ty::Void,
285 TyRef::Variant => Ty::Variant,
286 TyRef::Builtin(id) => match api.builtin(*id).name.as_str() {
290 "Callable" => Ty::Callable,
291 "Signal" => Ty::Signal(None),
292 "Array" => Ty::array_of_variant(),
293 "Dictionary" => Ty::dict_of_variant(),
294 _ => Ty::Builtin(*id),
295 },
296 TyRef::Class(id) => Ty::Object(*id),
297 TyRef::TypedArray(elem) => Ty::Array(Box::new(resolve_elemref(api, elem))),
298 TyRef::TypedDict(k, v) => Ty::Dict(
299 Box::new(resolve_elemref(api, k)),
300 Box::new(resolve_elemref(api, v)),
301 ),
302 TyRef::Enum {
303 qualified,
304 bitfield,
305 } => Ty::Enum(EnumRef {
306 qualified: SmolStr::new(qualified),
307 bitfield: *bitfield,
308 }),
309 }
310}
311
312#[must_use]
314pub fn resolve_elemref(_api: &EngineApi, elem: &ElemRef) -> Ty {
315 match elem {
316 ElemRef::Variant => Ty::Variant,
317 ElemRef::Builtin(id) => Ty::Builtin(*id),
318 ElemRef::Class(id) => Ty::Object(*id),
319 ElemRef::Enum {
320 qualified,
321 bitfield,
322 } => Ty::Enum(EnumRef {
323 qualified: SmolStr::new(qualified),
324 bitfield: *bitfield,
325 }),
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn ty_of(api: &EngineApi, builtin: &str) -> Ty {
334 Ty::Builtin(api.builtin_by_name(builtin).expect("known builtin"))
335 }
336
337 #[test]
338 fn type_source_hardness() {
339 assert!(!TypeSource::Undetected.is_hard());
340 assert!(!TypeSource::Inferred.is_hard());
341 assert!(TypeSource::AnnotatedInferred.is_hard());
342 assert!(TypeSource::AnnotatedExplicit.is_hard());
343 }
344
345 #[test]
346 fn variant_and_seam_assignability() {
347 let api = gdscript_api::bundled();
348 let int = ty_of(api, "int");
349 assert_eq!(is_assignable(api, &int, &Ty::Variant), Assign::Ok);
351 assert_eq!(is_assignable(api, &Ty::Variant, &int), Assign::OkUnsafe);
352 assert_eq!(is_assignable(api, &Ty::Unknown, &int), Assign::Ok);
354 assert_eq!(is_assignable(api, &int, &Ty::Unknown), Assign::Ok);
355 assert_eq!(is_assignable(api, &Ty::Error, &int), Assign::Ok);
356 }
357
358 #[test]
359 fn numeric_conversions() {
360 let api = gdscript_api::bundled();
361 let int = ty_of(api, "int");
362 let float = ty_of(api, "float");
363 assert_eq!(is_assignable(api, &int, &float), Assign::Ok); assert_eq!(is_assignable(api, &float, &int), Assign::Narrowing);
365 assert_eq!(is_assignable(api, &int, &int), Assign::Ok);
366 let string = ty_of(api, "String");
367 assert_eq!(is_assignable(api, &string, &int), Assign::No);
368 }
369
370 #[test]
371 fn object_subclassing() {
372 let api = gdscript_api::bundled();
373 let node = Ty::Object(api.class_by_name("Node").unwrap());
374 let node2d = Ty::Object(api.class_by_name("Node2D").unwrap());
375 assert_eq!(is_assignable(api, &node2d, &node), Assign::Ok);
378 assert_eq!(is_assignable(api, &node, &node2d), Assign::OkUnsafe);
379 let s = ty_of(api, "String");
381 assert_eq!(is_assignable(api, &s, &node), Assign::No);
382 }
383
384 #[test]
385 fn arrays_are_invariant() {
386 let api = gdscript_api::bundled();
387 let int = ty_of(api, "int");
388 let float = ty_of(api, "float");
389 let arr_int = Ty::Array(Box::new(int.clone()));
390 let arr_int2 = Ty::Array(Box::new(int));
391 let arr_float = Ty::Array(Box::new(float));
392 assert_eq!(is_assignable(api, &arr_int, &arr_int2), Assign::Ok);
393 assert_eq!(is_assignable(api, &arr_int2, &arr_float), Assign::No);
395 }
396
397 #[test]
398 fn enum_int_bridge() {
399 let api = gdscript_api::bundled();
400 let int = ty_of(api, "int");
401 let e = Ty::Enum(EnumRef {
402 qualified: SmolStr::new("Node.ProcessMode"),
403 bitfield: false,
404 });
405 assert_eq!(is_assignable(api, &e, &int), Assign::Ok); assert_eq!(is_assignable(api, &int, &e), Assign::IntAsEnum); }
408
409 #[test]
410 fn label_elides_unknown() {
411 let api = gdscript_api::bundled();
412 assert_eq!(Ty::Unknown.label(api), None);
413 assert_eq!(ty_of(api, "int").label(api).as_deref(), Some("int"));
414 assert_eq!(
415 Ty::Array(Box::new(ty_of(api, "int"))).label(api).as_deref(),
416 Some("Array[int]")
417 );
418 }
419}