Skip to content

Commit

Permalink
[Nullable] Implement Nullable Types for GDScript
Browse files Browse the repository at this point in the history
This PR aims to add a feature-set to GDScript tailored
towards the safe usage of potentially-null expressions.
It was initially based off godotengine/godot-proposals#162
and through several discussions over RocketChat it grew to be what it is currently.
  • Loading branch information
joao-pedro-braz authored and Macksaur committed Jun 21, 2024
1 parent 520219d commit 4405e9d
Show file tree
Hide file tree
Showing 146 changed files with 2,014 additions and 171 deletions.
1 change: 1 addition & 0 deletions core/core_constants.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ void register_global_constants() {
BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_EDITOR_BASIC_SETTING);
BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_READ_ONLY);
BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_SECRET);
BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_NULLABLE);

BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_DEFAULT);
BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_NO_EDITOR);
Expand Down
1 change: 1 addition & 0 deletions core/object/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ enum PropertyUsageFlags {
PROPERTY_USAGE_EDITOR_BASIC_SETTING = 1 << 27, //for project or editor settings, show when basic settings are selected.
PROPERTY_USAGE_READ_ONLY = 1 << 28, // Mark a property as read-only in the inspector.
PROPERTY_USAGE_SECRET = 1 << 29, // Export preset credentials that should be stored separately from the rest of the export config.
PROPERTY_USAGE_NULLABLE = 1 << 30, // When the property's type has the nullable qualifier.

PROPERTY_USAGE_DEFAULT = PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
PROPERTY_USAGE_NO_EDITOR = PROPERTY_USAGE_STORAGE,
Expand Down
27 changes: 18 additions & 9 deletions core/variant/array.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ void Array::assign(const Array &p_array) {
// from base classes to subclasses
for (int i = 0; i < size; i++) {
const Variant &element = source[i];
if (element.get_type() != Variant::NIL && (element.get_type() != Variant::OBJECT || !typed.validate_object(element, "assign"))) {
if ((element.get_type() != Variant::NIL && !_p->typed.is_nullable) && (element.get_type() != Variant::OBJECT || !typed.validate_object(element, "assign"))) {
ERR_FAIL_MSG(vformat(R"(Unable to convert array index %i from "%s" to "%s".)", i, Variant::get_type_name(element.get_type()), Variant::get_type_name(typed.type)));
}
}
Expand All @@ -253,7 +253,8 @@ void Array::assign(const Array &p_array) {
// from variants to primitives
for (int i = 0; i < size; i++) {
const Variant *value = source + i;
if (value->get_type() == typed.type) {
// Force-assign the current value if it's null, and we're nullable
if (value->get_type() == typed.type || (value->get_type() == Variant::NIL && _p->typed.is_nullable)) {
data[i] = *value;
continue;
}
Expand All @@ -264,13 +265,16 @@ void Array::assign(const Array &p_array) {
Variant::construct(typed.type, data[i], &value, 1, ce);
ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert array index %i from "%s" to "%s".)", i, Variant::get_type_name(value->get_type()), Variant::get_type_name(typed.type)));
}
} else if (Variant::can_convert_strict(source_typed.type, typed.type)) {
} else if (Variant::can_convert_strict(source_typed.type, typed.type) && (source_typed.type != Variant::NIL || _p->typed.is_nullable)) {
// from primitives to different convertible primitives
for (int i = 0; i < size; i++) {
const Variant *value = source + i;
Callable::CallError ce;
Variant::construct(typed.type, data[i], &value, 1, ce);
ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert array index %i from "%s" to "%s".)", i, Variant::get_type_name(value->get_type()), Variant::get_type_name(typed.type)));
// Avoid constructing the variant if it's null, and we're nullable
if (value->get_type() != Variant::NIL && !_p->typed.is_nullable) {
Callable::CallError ce;
Variant::construct(typed.type, data[i], &value, 1, ce);
ERR_FAIL_COND_MSG(ce.error, vformat(R"(Unable to convert array index %i from "%s" to "%s".)", i, Variant::get_type_name(value->get_type()), Variant::get_type_name(typed.type)));
}
}
} else {
ERR_FAIL_MSG(vformat(R"(Cannot assign contents of "Array[%s]" to "Array[%s]".)", Variant::get_type_name(source_typed.type), Variant::get_type_name(typed.type)));
Expand Down Expand Up @@ -773,14 +777,14 @@ const void *Array::id() const {
return _p;
}

Array::Array(const Array &p_from, uint32_t p_type, const StringName &p_class_name, const Variant &p_script) {
Array::Array(const Array &p_from, uint32_t p_type, const StringName &p_class_name, const Variant &p_script, bool p_is_nullable) {
_p = memnew(ArrayPrivate);
_p->refcount.init();
set_typed(p_type, p_class_name, p_script);
set_typed(p_type, p_class_name, p_script, p_is_nullable);
assign(p_from);
}

void Array::set_typed(uint32_t p_type, const StringName &p_class_name, const Variant &p_script) {
void Array::set_typed(uint32_t p_type, const StringName &p_class_name, const Variant &p_script, bool p_is_nullable) {
ERR_FAIL_COND_MSG(_p->read_only, "Array is in read-only state.");
ERR_FAIL_COND_MSG(_p->array.size() > 0, "Type can only be set when array is empty.");
ERR_FAIL_COND_MSG(_p->refcount.get() > 1, "Type can only be set when array has no more than one user.");
Expand All @@ -792,6 +796,7 @@ void Array::set_typed(uint32_t p_type, const StringName &p_class_name, const Var
_p->typed.type = Variant::Type(p_type);
_p->typed.class_name = p_class_name;
_p->typed.script = script;
_p->typed.is_nullable = p_is_nullable;
_p->typed.where = "TypedArray";
}

Expand All @@ -815,6 +820,10 @@ Variant Array::get_typed_script() const {
return _p->typed.script;
}

bool Array::is_nullable() const {
return _p->typed.is_nullable;
}

void Array::make_read_only() {
if (_p->read_only == nullptr) {
_p->read_only = memnew(Variant);
Expand Down
5 changes: 3 additions & 2 deletions core/variant/array.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,18 @@ class Array {

const void *id() const;

void set_typed(uint32_t p_type, const StringName &p_class_name, const Variant &p_script);
void set_typed(uint32_t p_type, const StringName &p_class_name, const Variant &p_script, bool p_is_nullable = false);
bool is_typed() const;
bool is_same_typed(const Array &p_other) const;
uint32_t get_typed_builtin() const;
StringName get_typed_class_name() const;
Variant get_typed_script() const;
bool is_nullable() const;

void make_read_only();
bool is_read_only() const;

Array(const Array &p_base, uint32_t p_type, const StringName &p_class_name, const Variant &p_script);
Array(const Array &p_base, uint32_t p_type, const StringName &p_class_name, const Variant &p_script, bool p_is_nullable = false);
Array(const Array &p_from);
Array();
~Array();
Expand Down
12 changes: 9 additions & 3 deletions core/variant/container_type_validate.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ struct ContainerTypeValidate {
Variant::Type type = Variant::NIL;
StringName class_name;
Ref<Script> script;
bool is_nullable = false;
const char *where = "container";

_FORCE_INLINE_ bool can_reference(const ContainerTypeValidate &p_type) const {
if (type != p_type.type) {
return false;
} else if (type != Variant::OBJECT) {
return true;
// If this is not nullable, it can't safely be replaced by a nullable one.
return is_nullable || !p_type.is_nullable;
}

if (class_name == StringName()) {
Expand All @@ -67,10 +69,10 @@ struct ContainerTypeValidate {
}

_FORCE_INLINE_ bool operator==(const ContainerTypeValidate &p_type) const {
return type == p_type.type && class_name == p_type.class_name && script == p_type.script;
return type == p_type.type && class_name == p_type.class_name && script == p_type.script && is_nullable == p_type.is_nullable;
}
_FORCE_INLINE_ bool operator!=(const ContainerTypeValidate &p_type) const {
return type != p_type.type || class_name != p_type.class_name || script != p_type.script;
return type != p_type.type || class_name != p_type.class_name || script != p_type.script || is_nullable != p_type.is_nullable;
}

// Coerces String and StringName into each other and int into float when needed.
Expand All @@ -79,6 +81,10 @@ struct ContainerTypeValidate {
return true;
}

if (inout_variant.get_type() == Variant::NIL && is_nullable) {
return true;
}

if (type != inout_variant.get_type()) {
if (inout_variant.get_type() == Variant::NIL && type == Variant::OBJECT) {
return true;
Expand Down
4 changes: 3 additions & 1 deletion core/variant/variant_op.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,8 @@ void Variant::_register_variant_operators() {

register_op<OperatorEvaluatorObjectHasPropertyString>(Variant::OP_IN, Variant::STRING, Variant::OBJECT);
register_op<OperatorEvaluatorObjectHasPropertyStringName>(Variant::OP_IN, Variant::STRING_NAME, Variant::OBJECT);

register_op<OperatorEvaluatorObjectHasPropertyStringName>(Variant::OP_IN, Variant::STRING_NAME, Variant::OBJECT);
}

#undef register_string_op
Expand Down Expand Up @@ -1136,7 +1138,7 @@ static const char *_op_names[Variant::OP_MAX] = {
"or",
"xor",
"not",
"in"
"in",
};

String Variant::get_operator_name(Operator p_op) {
Expand Down
3 changes: 3 additions & 0 deletions doc/classes/@GlobalScope.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3023,6 +3023,9 @@
<constant name="PROPERTY_USAGE_SECRET" value="536870912" enum="PropertyUsageFlags" is_bitfield="true">
An export preset property with this flag contains confidential information and is stored separately from the rest of the export preset configuration.
</constant>
<constant name="PROPERTY_USAGE_NULLABLE" value="1073741824" enum="PropertyUsageFlags" is_bitfield="true">
The property has the nullable qualifier and therefore can be null.
</constant>
<constant name="PROPERTY_USAGE_DEFAULT" value="6" enum="PropertyUsageFlags" is_bitfield="true">
Default usage (storage and editor).
</constant>
Expand Down
Loading

0 comments on commit 4405e9d

Please sign in to comment.