Skip to content

Instantly share code, notes, and snippets.

@dalexeev
Created October 8, 2023 17:18
Show Gist options
  • Save dalexeev/bd4e952a601b18ae3a94919f89b98928 to your computer and use it in GitHub Desktop.
Save dalexeev/bd4e952a601b18ae3a94919f89b98928 to your computer and use it in GitHub Desktop.
GodotType
/**************************************************************************/
/* godot_type.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "godot_type.h"
GodotType GodotType::make_builtin_type(Variant::Type p_builtin_type, const Vector<GodotType> &p_params) {
ERR_FAIL_INDEX_V_MSG(p_builtin_type, Variant::VARIANT_MAX, make_variant_type(), "..."); // TODO
GodotType type;
type.builtin_type = p_builtin_type;
bool param_count_valid = false;
switch (p_builtin_type) {
case Variant::ARRAY:
if (p_params.is_empty()) {
param_count_valid = true;
type.params.push_back(make_variant_type());
} else if (p_params.size() == 1) {
ERR_FAIL_COND_V_MSG(p_params[0].builtin_type == Variant::ARRAY && !p_params[0].params[0].is_variant(), make_variant_type(), "..."); // TODO
param_count_valid = true;
type.params.push_back(p_params[0]);
}
break;
case Variant::OBJECT:
param_count_valid = p_params.is_empty();
type.name = SNAME("Object");
break;
default:
param_count_valid = p_params.is_empty();
break;
}
ERR_FAIL_COND_V_MSG(!param_count_valid, make_variant_type(), "..."); // TODO
return type;
}
GodotType GodotType::make_native_type(const StringName &p_native_type) {
ERR_FAIL_COND_V_MSG(!ClassDB::class_exists(p_native_type), make_builtin_type(Variant::OBJECT), "..."); // TODO
GodotType type;
type.builtin_type = Variant::OBJECT;
type.name = p_native_type;
return type;
}
GodotType GodotType::make_script_type(const Ref<Script> &p_script_type) {
ERR_FAIL_COND_V_MSG(p_script_type.is_null(), make_builtin_type(Variant::OBJECT), "..."); // TODO
GodotType type;
type.builtin_type = Variant::OBJECT;
type.name = p_script_type->get_instance_base_type();
type.script_type = p_script_type;
return type;
}
GodotType GodotType::make_generic_type(const StringName &p_generic_name) {
ERR_FAIL_COND_V_MSG(p_generic_name == StringName(), "..."); // TODO
GodotType type;
type.kind = GENERIC;
type.name = p_generic_name;
return type;
}
GodotType GodotType::make_enum_type(const StringName &p_enum_name) { // TODO: scopes?!
ERR_FAIL_COND_V_MSG(/*...*/, make_builtin_type(Variant::INT), "..."); // TODO
GodotType type;
type.kind = ENUM;
type.builtin_type = Variant::INT;
type.name = p_enum_name; // TODO
return type;
}
GodotType GodotType::make_bitfield_type(const GodotType &p_enum_type) {
ERR_FAIL_COND_V_MSG(!is_enum() && !is_generic(), make_builtin_type(Variant::INT), "..."); // TODO
GodotType type;
type.kind = BITFIELD;
type.builtin_type = Variant::INT;
type.params.push_back(p_enum_type);
return type;
}
GodotType GodotType::from_variant_value(const Variant &p_value) {
GodotType type;
type.builtin_type = p_value.get_type();
switch (type.builtin_type) {
case Variant::ARRAY: {
const Array array = p_value;
const Ref<Script> script = array.get_typed_script();
if (script.is_valid()) {
type.params.push_back(make_script_type(script));
} else if (array.get_typed_class_name() != StringName()) {
type.params.push_back(make_native_type(array.get_typed_class_name()));
} else if ((Variant::Type)array.get_typed_builtin() != Variant::NIL) {
type.params.push_back(make_builtin_type((Variant::Type)array.get_typed_builtin()));
} else {
type.params.push_back(make_variant_type());
}
} break;
case Variant::OBJECT:
// TODO
break;
default:
break;
}
return type;
}
GodotType GodotType::from_property_info(const PropertyInfo &p_info) {
GodotType type;
ERR_FAIL_INDEX_V_MSG(p_info.type, Variant::VARIANT_MAX, make_variant_type(), "..."); // TODO
type.builtin_type = p_info.type;
switch (type.builtin_type) {
case Variant::NIL:
if (p_info.hint == PROPERTY_HINT_GENERIC_TYPE && !info.hint_string.is_empty()) {
type.kind = GENERIC;
type.name = info.hint_string;
} else if (p_info.usage & PROPERTY_USAGE_NIL_IS_VARIANT) {
type.kind = VARIANT;
}
break;
case Variant::INT:
if (p_info.class_name != StringName()) {
if (p_info.usage & PROPERTY_USAGE_CLASS_IS_ENUM) {
type.kind = ENUM;
//type.name = p_info.class_name; // TODO
} else if (p_info.usage & PROPERTY_USAGE_CLASS_IS_BITFIELD) {
GodotType enum_type;
enum_type.kind = ENUM;
enum_type.builtin_type = Variant::INT;
//enum_type.name = p_info.class_name; // TODO
type.kind = BITFIELD;
type.params.push_back(enum_type);
}
}
break;
case Variant::ARRAY:
// TODO
break;
case Variant::OBJECT:
// TODO
break;
default:
break;
}
return type;
}
PropertyInfo GodotType::to_property_info(const String &p_property_name) const {
PropertyInfo info;
info.name = p_property_name;
info.type = builtin_type;
switch (kind) {
case BUILTIN:
break;
case VARIANT:
info.usage |= PROPERTY_USAGE_NIL_IS_VARIANT;
break;
case GENERIC:
info.hint = PROPERTY_HINT_GENERIC_TYPE;
info.hint_string = name;
info.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; // ... // TODO
break;
case ENUM:
info.usage |= PROPERTY_USAGE_CLASS_IS_ENUM;
info.class_name = /*...*/; // TODO
break;
case BITFIELD:
info.usage |= PROPERTY_USAGE_CLASS_IS_BITFIELD;
info.class_name = /*...*/; // TODO
break;
}
switch (builtin_type) {
case Variant::ARRAY:
if (!params[0].is_variant()) {
info.hint == PROPERTY_HINT_ARRAY_TYPE;
info.hint_string = /*...*/; // TODO
}
break;
case Variant::OBJECT:
// PROPERTY_HINT_RESOURCE_TYPE
// PROPERTY_HINT_NODE_TYPE
// TODO
break;
default:
break;
}
return info;
}
bool GodotType::is_recursively_generic() const {
if (is_generic()) {
return true;
}
for (int i = 0; i < params.size(); i++) {
if (params[i].is_recursively_generic()) {
return true;
}
}
return false;
}
void GodotType::set_param(int p_index, const GodotType &p_type) {
ERR_FAIL_INDEX_MSG(p_index, params.size(), "..."); // TODO
if (kind == BITFIELD) {
ERR_FAIL_COND_MSG(!p_type.is_enum() && !p_type.is_generic(), "..."); // TODO
} else if (builtin_type == Variant::ARRAY) {
ERR_FAIL_COND_MSG(p_type.builtin_type == Variant::ARRAY && !p_type.params[0].is_variant(), "..."); // TODO
} else {
ERR_FAIL_MSG("..."); // TODO
}
params.write[p_index] = p_type;
}
void apply_generic(const StringName &p_generic_name, const GodotType &p_new_type) {
if (is_generic()) {
if (name == p_generic_name) {
// **Only top-level** generic type can be replaced without checks.
*this = p_new_type;
}
return; // A generic type has no parameters, exit anyway.
}
for (int i = 0; i < params.size(); i++) {
if (params[i].is_generic()) {
if (params[i].name == p_generic_name) {
// The parameter must be changed **via setter**.
set_param(i, p_new_type);
} // else: Skipping the generic parameter.
} else {
// Recursively call the method on **non-generic** parameters.
params.write[i].apply_generic(p_generic_name, p_new_type);
}
}
}
GodotType GodotType::get_base_type() const {
if (builtin_type == Variant::OBJECT) {
if (script_type.is_valid()) {
const Ref<Script> base_script = script_type->get_base_script();
if (base_script.is_valid()) {
return make_script_type(base_script);
} else {
return make_native_type(script_type->get_instance_base_type());
}
} else {
if (name == SNAME("Object")) {
return make_variant_type();
} else {
return make_native_type(ClassDB::get_parent_class(name));
}
}
} else {
return make_variant_type();
}
}
bool GodotType::is_supertype_of(const GodotType &p_other) const {
// TODO
}
/**************************************************************************/
/* godot_type.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef GODOT_TYPE_H
#define GODOT_TYPE_H
#include "core/variant/variant.h"
class GodotType {
enum Kind {
BUILTIN,
VARIANT, // `builtin_type == Variant::NIL`.
GENERIC, // `builtin_type == Variant::NIL`.
ENUM, // `builtin_type == Variant::INT`.
BITFIELD, // `builtin_type == Variant::INT`.
};
Kind kind = BUILTIN;
Variant::Type builtin_type = Variant::NIL;
// `kind == GENERIC`: generic type name (for example `T`).
// `kind == ENUM`: enum name.
// `builtin_type == Variant::OBJECT`: native class name.
StringName name;
// `builtin_type == Variant::OBJECT`.
Ref<Script> script_type; // TODO: WeakRef?
Vector<GodotType> params; // TODO: `Vector<Variant>`?
public:
_FORCE_INLINE_ static GodotType make_nil_type() { return GodotType(); }
_FORCE_INLINE_ static GodotType make_variant_type() {
GodotType type;
type.kind = VARIANT;
return type;
}
static GodotType make_builtin_type(Variant::Type p_builtin_type, const Vector<GodotType> &p_params = Vector<GodotType>());
static GodotType make_native_type(const StringName &p_native_type);
static GodotType make_script_type(const Ref<Script> &p_script_type);
static GodotType make_generic_type(const StringName &p_generic_name);
static GodotType make_enum_type(const StringName &p_enum_name); // TODO: scopes?!
static GodotType make_bitfield_type(const GodotType &p_enum_type);
static GodotType from_variant_value(const Variant &p_value);
static GodotType from_property_info(const PropertyInfo &p_info);
PropertyInfo to_property_info(const String &p_property_name = String()) const;
_FORCE_INLINE_ bool is_nil() const { return kind == BUILTIN && builtin_type == Variant::NIL; }
_FORCE_INLINE_ bool is_variant() const { return kind == VARIANT; }
_FORCE_INLINE_ Variant::Type get_builtin_type() const { return builtin_type; }
_FORCE_INLINE_ StringName get_native_type() const { return (builtin_type == Variant::OBJECT) ? name : StringName(); }
_FORCE_INLINE_ Ref<Script> get_script_type() const { return script_type; }
bool is_recursively_generic() const; // Is generic or contains generic. // TODO: Rename?
_FORCE_INLINE_ bool is_generic() const { return kind == GENERIC; }
_FORCE_INLINE_ StringName get_generic_name() const { return (kind == GENERIC) ? name : StringName(); }
_FORCE_INLINE_ bool is_enum() const { return kind == ENUM; }
_FORCE_INLINE_ StringName get_enum_name() const { return (kind == ENUM) ? name : StringName(); } // TODO: scopes?!
_FORCE_INLINE_ bool is_bitfield() const { return kind == BITFIELD; }
_FORCE_INLINE_ int get_param_count() const { return params.size(); }
_FORCE_INLINE_ GodotType get_param(int p_index) const {
ERR_FAIL_INDEX_V(p_index, params.size(), make_variant_type());
return params[p_index];
}
void set_param(int p_index, const GodotType &p_type);
void apply_generic(const StringName &p_generic_name, const GodotType &p_new_type);
GodotType get_base_type() const;
bool is_supertype_of(const GodotType &p_other) const;
_FORCE_INLINE_ bool is_subtype_of(const GodotType &p_other) const { return p_other.is_supertype_of(*this); }
bool operator==(const GodotType &p_other) const {
// TODO
}
bool operator!=(const GodotType &p_other) const {
return !(*this == p_other);
}
void operator=(const GodotType &p_other) {
kind = p_other.kind;
builtin_type = p_other.builtin_type;
name = p_other.name;
script_type = p_other.script_type;
params = p_other.params;
}
GodotType() {}
GodotType(const GodotType &p_other) {
*this = p_other;
}
};
#endif // GODOT_TYPE_H
// ScriptLanguage methods for is_subtype_of and etc.
// methods to conversion from/to string / docdata
// methods to make string type hints, taking into account scopes context*, language
// * - contextualize type name
// * - scopes/namespaces need to be represented too, even if it can be not a type?
// For exposing we need a separated class (extends RefCounted) with GDCLASS macro.
@dalexeev
Copy link
Author

dalexeev commented Feb 20, 2024

Thanks for the questions, @nlupugla!

  1. I'm not sure if metatypes are needed in the core (only in GDScriptParser::DataType), since GodotType is intended for representing runtime types and types in core API. I don't think the Godot API type system should be so complicated. But if it is needed, then I would do it as kind = METATYPE. For example, MetaType[MyEnum], MetaType[MetaType[MyEnum]], etc. Note that MetaType[MyEnum] is not strictly speaking equal to Dictionary. This is some other operation that maps a set of metatypes into a set of first-level types, "demetatyping".
  2. To me, GodotType as a new built-in Variant type makes the most sense. In this case, GodotType will be exposed as a lightweight structure. Other options are to implement/expose it as a RefCounted class, but in my opinion this is worse, because it narrows the scope of GodotType or makes its implementation more difficult.
  3. Yes, an alternative is presented below.
  4. I think this makes the most sense in the form of separate kinds and type parameters, since it allows you to nest generic types and handle them universally. For example:
    • Type union String|int is literally Union[String, int],
      • i.e. a type with kind = TYPE_UNION and params = { <GodotType String>, <GodotType int> }.
    • BitField[T] is a type with kind = BITFIELD and params = { <GodotType GenericType(T)> }.
  5. C++ allows non-types to be used as parameters of generic types, so I added the comment. Of course, the value must be constant. If we want to allow this, then GodotType must be a built-in Variant type (see p.2).
p.3 alternative
enum Kind {
    BUILTIN,
    NATIVE,
    SCRIPT,
    ENUM,
    BITFIELD,
    VARIANT,
    GENERIC,
};
Kind kind = BUILTIN;

union {
    Variant::Type builtin_type = Variant::NIL;
    StringName native_type;
    Ref<Script> script_type;
    // TODO: enum
    StringName generic_name;
};

Vector<GodotType> params; // TODO: `Vector<Variant>`?

@nlupugla
Copy link

nlupugla commented Feb 20, 2024

  1. That makes sense.
  2. Making a GodotType a new built-in variant does make sense, but isn't adding a new built-in type pretty hard? If GodotType were to be its own built-in, I'd think structs should be as well. I suppose the problem with making GodotType a struct is that GodotType really wants to have methods, which is not currently planned for structs.
  3. See 4
  4. I agree with the approach where e.g. String|int is represented by some UNION identifier along with params = { GodotTypeString, GodotTypeInt }. My concern is that if that identifier is an enum value, it becomes hard to represent user-defined type constructors. For example, a user might want to define BinaryTree<T>. If the identifier is a StringName, they just set it to BinaryTree, but if the identifier is an enum, it seems like we'd have to anticipate with a Kind::BINARY_TREE value.
  5. How will a GodotType instance know whether it is a compile-time constant or not, especially if it is possible for the user to create them at run time? Perhaps it's the analyzer's job to figure that out, though it would be cool if the type representation used by the analyzer were as close as possible to GodotType itself imo, and potentially easier to maintain I'd think.

I was thinking about an approach that might address both points 1. and 5. The idea is to add an int order field to GodotType, where 0th order is basically a value, 1st order is a type, 2nd order is a type of types (metatype) and so on. Then there would be a rule that the order of a type annotation must be greater than the order of the value it annotates and the order of a GodotType instance is the minimum order of its parameters. Most types like int and String would have order = 1. A literal value like 4 could be represented as a GodotType having order = INT_MAX for the purposes of type constructors. This way, you could do

GodotType FourInts = FixedSizeArray<4, int> # type order is min(INT_MAX, 1) = 1

const FIVE = 5
GodotType FiveInts = FixedSizeArray<FIVE, int> # type order is also 1

var six = 6
GodotType SixInts = FixedSizeArray<six, int> # type order is min(0, 1) = 0

var four_ints : FourInts # okay, order of type hint (1) is greater than order of value (0).
var five_ints : FiveInts # also okay
var six_ints : SixInts # analyzer error: order of value (0) is not less than order of type hint (0).

It's likely that this idea is overcomplicating things to be honest, but it might make for a pretty elegant system.

@dalexeev
Copy link
Author

dalexeev commented Feb 21, 2024

2. I don't understand you. By "lightweight structure" I didn't mean the structs you're currently working on. What I meant was that the only way to expose a non-Object class is to add a new built-in Variant type. Yes, it's difficult, but not impossible, see PackedVector4Array PR, which will probably be merged soon. As for Array structs, they are a modification of Variant::ARRAY, not a separate Variant type. But at the same time they can be represented in GodotType as a separate kind = STRUCT, just as for Variant::INT there are 3 kinds (BUILTIN, ENUM and BITFIELD).

4. In case of custom generics this will work too:

  • BinaryTree[T]: { kind = SCRIPT, script_type = Ref<Script>(...), params = { <GodotType GenericType(T)> } }
  • BinaryTree[int]: { kind = SCRIPT, script_type = Ref<Script>(...), params = { <GodotType int> } }
  • BinaryTree[BitField[T]]: { kind = SCRIPT, script_type = Ref<Script>(...), params = { <bitfield_type> } }
    • where <bitfield_type> is { kind = BITFIELD, params = { <GodotType GenericType(T)> } }

5. This is not GodotType's responsibility, it only represents the type. We might even expose static methods to create GodotTypes at runtime, it should be ok. GDScript checks constancy for type specifiers and const initializers. We could potentially allow the following:

const MyType: GodotType = <GodotType>
var my_var: MyType

But not the following:

var MyType: GodotType = <GodotType>
var my_var: MyType

As for the type order, what you suggest can be achieved with kind = METATYPE too. The advantage is that you can universally use generic types, substituting specific types instead of generic placeholders (i.e. instantiate a generic type). In my experience with GDScriptAnalyzer, I really think that the is_meta_type flag (or integer order as you suggested) is a bad thing, as it is bug prone due to insufficient differentiation between first order types and metatypes. Maybe this is less critical in the case of GodotType since all properties are in the private section, unlike GDScriptParser::DataType. Also, with your approach we can't do something like Array[MetaType[T]].

@nlupugla
Copy link

nlupugla commented Feb 22, 2024

  1. Didn't know about about PackedVector4Array, I'll have to check out that PR!

.4. Ah, that makes a lot of sense! Another approach might be to separate the Kind enum into a Source enum with values BUILTIN, NATIVE, SCRIPT and a TypeConstructor enum with values like ENUM, BITFIELD, STRUCT, UNION, ARRAY, DICTIONARY, ...(etc), USER. This way, GodotType can tell whether e.g. an ENUM is native or user-defined.

.5. I think it's fair to put the responsibility of determining constancy/meta-ness/order with the analyzer. That said, I could see it being potentially useful for a user to be able to reflect on whether a particular value is constant or not. The idea to use nested MetaType wrappers so encode order is neat, but I think it has some limitations. If we say T has order 0, MetaType[T] has order 1, and MetaType[MetaType[T]] has order 2, what is the order of something like Array[MetaType[T]] or MetaType[Dictionary[T, MetaType[T]]]? The algorithm would have to know how to properly "unwrap" these situations wouldn't it? On the other hand, an order index makes determining the order pretty straightforward: Array[MetaType[T]] has order 1 = min(0 + 1,) and MetaType[Dictionary[T, MetaType[T]]] has order 1 = 1 + min(0, 0 + 1).

@unfallible
Copy link

I have a couple questions about this (unrelated to my new questions on #737), which are in :

  1. When you mentioned drafting a proposal for first-class types in the discussion of proposal #737, I assumed you meant something akin to C#'s Type class or Java's Class class, which would replace the engine's decentralized type systems where e.g. typeof() and getclass() being not only separate functions but also functions with different return types and if you want to query the features of a type, you have to do it from at least 3 different places (ClassDB, the Script resource, and, when compiling GDScript, GDScriptParser::DataType). The GodotType here seems more narrowly focused though (for example, there's no get_methods() function), but I'm don't understand what that focus is. What are the intended use cases for GodotType? Is it just supposed to provide a way to check whether one type "inherits" another type's features?
  2. Are you planning to represent type safe Callable and Signal objects with this? If so, it seems like at minimum, GodotType would need some way to check if a Callable returns something, such as a has_return_type flag to tell you whether params contains the return type or a return_type instance variable that points to a GodotType if the type object represents a Callable with a return type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment