summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorGeorge Marques <george@gmarqu.es>2023-04-19 11:10:35 -0300
committerGeorge Marques <george@gmarqu.es>2023-04-27 09:51:44 -0300
commit0ba6048ad3c945e2bd1d0114b5095535c22103ce (patch)
treee9d17e159dab50dc6cc3d574a25647af01939d6c /modules
parent352ebe97259622f20b47627b4bf747cdfc79304d (diff)
downloadredot-engine-0ba6048ad3c945e2bd1d0114b5095535c22103ce.tar.gz
Add support for static variables in GDScript
Which allows editable data associated with a particular class instead of the instance. Scripts with static variables are kept in memory indefinitely unless the `@static_unload` annotation is used or the `static_unload()` method is called on the GDScript. If the custom function `_static_init()` exists it will be called when the class is loaded, after the static variables are set.
Diffstat (limited to 'modules')
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml6
-rw-r--r--modules/gdscript/gdscript.cpp127
-rw-r--r--modules/gdscript/gdscript.h14
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp85
-rw-r--r--modules/gdscript/gdscript_analyzer.h1
-rw-r--r--modules/gdscript/gdscript_byte_codegen.h2
-rw-r--r--modules/gdscript/gdscript_cache.cpp10
-rw-r--r--modules/gdscript/gdscript_cache.h3
-rw-r--r--modules/gdscript/gdscript_codegen.h1
-rw-r--r--modules/gdscript/gdscript_compiler.cpp228
-rw-r--r--modules/gdscript/gdscript_compiler.h3
-rw-r--r--modules/gdscript/gdscript_editor.cpp9
-rw-r--r--modules/gdscript/gdscript_function.h3
-rw-r--r--modules/gdscript/gdscript_parser.cpp86
-rw-r--r--modules/gdscript/gdscript_parser.h21
-rw-r--r--modules/gdscript/gdscript_vm.cpp4
-rw-r--r--modules/gdscript/gdscript_warning.cpp4
-rw-r--r--modules/gdscript/gdscript_warning.h2
-rw-r--r--modules/gdscript/tests/gdscript_test_runner.cpp16
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd5
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd5
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd6
-rw-r--r--modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_constructor.gd13
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_constructor.out4
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables.gd56
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables.out16
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd10
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_load.out2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd11
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/static_variables_other.out2
35 files changed, 686 insertions, 86 deletions
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 08a4883054..d8f12f7232 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -622,6 +622,12 @@
[/codeblock]
</description>
</annotation>
+ <annotation name="@static_unload">
+ <return type="void" />
+ <description>
+ Make a script with static variables to not persist after all references are lost. If the script is loaded again the static variables will revert to their default values.
+ </description>
+ </annotation>
<annotation name="@tool">
<return type="void" />
<description>
diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp
index e74c2866e6..3bc11c69e9 100644
--- a/modules/gdscript/gdscript.cpp
+++ b/modules/gdscript/gdscript.cpp
@@ -651,6 +651,49 @@ String GDScript::_get_debug_path() const {
}
}
+Error GDScript::_static_init() {
+ if (static_initializer) {
+ Callable::CallError call_err;
+ static_initializer->call(nullptr, nullptr, 0, call_err);
+ if (call_err.error != Callable::CallError::CALL_OK) {
+ return ERR_CANT_CREATE;
+ }
+ }
+ Error err = OK;
+ for (KeyValue<StringName, Ref<GDScript>> &inner : subclasses) {
+ err = inner.value->_static_init();
+ if (err) {
+ break;
+ }
+ }
+ return err;
+}
+
+#ifdef TOOLS_ENABLED
+
+void GDScript::_save_old_static_data() {
+ old_static_variables_indices = static_variables_indices;
+ old_static_variables = static_variables;
+ for (KeyValue<StringName, Ref<GDScript>> &inner : subclasses) {
+ inner.value->_save_old_static_data();
+ }
+}
+
+void GDScript::_restore_old_static_data() {
+ for (KeyValue<StringName, MemberInfo> &E : old_static_variables_indices) {
+ if (static_variables_indices.has(E.key)) {
+ static_variables.write[static_variables_indices[E.key].index] = old_static_variables[E.value.index];
+ }
+ }
+ old_static_variables_indices.clear();
+ old_static_variables.clear();
+ for (KeyValue<StringName, Ref<GDScript>> &inner : subclasses) {
+ inner.value->_restore_old_static_data();
+ }
+}
+
+#endif
+
Error GDScript::reload(bool p_keep_state) {
if (reloading) {
return OK;
@@ -696,6 +739,14 @@ Error GDScript::reload(bool p_keep_state) {
}
}
+ bool can_run = ScriptServer::is_scripting_enabled() || is_tool();
+
+#ifdef DEBUG_ENABLED
+ if (p_keep_state && can_run && is_valid()) {
+ _save_old_static_data();
+ }
+#endif
+
valid = false;
GDScriptParser parser;
Error err = parser.parse(source, path, false);
@@ -726,7 +777,7 @@ Error GDScript::reload(bool p_keep_state) {
return ERR_PARSE_ERROR;
}
- bool can_run = ScriptServer::is_scripting_enabled() || parser.is_tool();
+ can_run = ScriptServer::is_scripting_enabled() || parser.is_tool();
GDScriptCompiler compiler;
err = compiler.compile(&parser, this, p_keep_state);
@@ -760,6 +811,19 @@ Error GDScript::reload(bool p_keep_state) {
}
#endif
+ if (can_run) {
+ err = _static_init();
+ if (err) {
+ return err;
+ }
+ }
+
+#ifdef DEBUG_ENABLED
+ if (can_run && p_keep_state) {
+ _restore_old_static_data();
+ }
+#endif
+
reloading = false;
return OK;
}
@@ -788,6 +852,10 @@ const Variant GDScript::get_rpc_config() const {
return rpc_config;
}
+void GDScript::unload_static() const {
+ GDScriptCache::remove_script(fully_qualified_name);
+}
+
Variant GDScript::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
GDScript *top = this;
while (top) {
@@ -824,6 +892,19 @@ bool GDScript::_get(const StringName &p_name, Variant &r_ret) const {
return true;
}
}
+
+ {
+ HashMap<StringName, MemberInfo>::ConstIterator E = static_variables_indices.find(p_name);
+ if (E) {
+ if (E->value.getter) {
+ Callable::CallError ce;
+ r_ret = const_cast<GDScript *>(this)->callp(E->value.getter, nullptr, 0, ce);
+ return true;
+ }
+ r_ret = static_variables[E->value.index];
+ return true;
+ }
+ }
top = top->_base;
}
@@ -841,7 +922,32 @@ bool GDScript::_set(const StringName &p_name, const Variant &p_value) {
set_source_code(p_value);
reload();
} else {
- return false;
+ const GDScript *top = this;
+ while (top) {
+ HashMap<StringName, MemberInfo>::ConstIterator E = static_variables_indices.find(p_name);
+ if (E) {
+ const GDScript::MemberInfo *member = &E->value;
+ Variant value = p_value;
+ if (member->data_type.has_type && !member->data_type.is_type(value)) {
+ const Variant *args = &p_value;
+ Callable::CallError err;
+ Variant::construct(member->data_type.builtin_type, value, &args, 1, err);
+ if (err.error != Callable::CallError::CALL_OK || !member->data_type.is_type(value)) {
+ return false;
+ }
+ }
+ if (member->setter) {
+ const Variant *args = &value;
+ Callable::CallError err;
+ callp(member->setter, &args, 1, err);
+ return err.error == Callable::CallError::CALL_OK;
+ } else {
+ static_variables.write[member->index] = value;
+ return true;
+ }
+ }
+ top = top->_base;
+ }
}
return true;
@@ -1293,6 +1399,13 @@ void GDScript::clear(GDScript::ClearData *p_clear_data) {
E.value.data_type.script_type_ref = Ref<Script>();
}
+ for (KeyValue<StringName, GDScript::MemberInfo> &E : static_variables_indices) {
+ clear_data->scripts.insert(E.value.data_type.script_type_ref);
+ E.value.data_type.script_type_ref = Ref<Script>();
+ }
+ static_variables.clear();
+ static_variables_indices.clear();
+
if (implicit_initializer) {
clear_data->functions.insert(implicit_initializer);
implicit_initializer = nullptr;
@@ -1303,6 +1416,11 @@ void GDScript::clear(GDScript::ClearData *p_clear_data) {
implicit_ready = nullptr;
}
+ if (static_initializer) {
+ clear_data->functions.insert(static_initializer);
+ static_initializer = nullptr;
+ }
+
_save_orphaned_subclasses(clear_data);
#ifdef TOOLS_ENABLED
@@ -1357,10 +1475,6 @@ GDScript::~GDScript() {
GDScriptLanguage::get_singleton()->script_list.remove(&script_list);
}
-
- if (GDScriptCache::singleton) { // Cache may have been already destroyed at engine shutdown.
- GDScriptCache::remove_script(get_path());
- }
}
//////////////////////////////
@@ -2391,6 +2505,7 @@ GDScriptLanguage::GDScriptLanguage() {
ERR_FAIL_COND(singleton);
singleton = this;
strings._init = StaticCString::create("_init");
+ strings._static_init = StaticCString::create("_static_init");
strings._notification = StaticCString::create("_notification");
strings._set = StaticCString::create("_set");
strings._get = StaticCString::create("_get");
diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h
index 7ba6368947..60bd9eef53 100644
--- a/modules/gdscript/gdscript.h
+++ b/modules/gdscript/gdscript.h
@@ -94,6 +94,8 @@ class GDScript : public Script {
HashSet<StringName> members; //members are just indices to the instantiated script.
HashMap<StringName, Variant> constants;
+ HashMap<StringName, MemberInfo> static_variables_indices;
+ Vector<Variant> static_variables;
HashMap<StringName, GDScriptFunction *> member_functions;
HashMap<StringName, MemberInfo> member_indices; //members are just indices to the instantiated script.
HashMap<StringName, Ref<GDScript>> subclasses;
@@ -102,6 +104,12 @@ class GDScript : public Script {
#ifdef TOOLS_ENABLED
+ // For static data storage during hot-reloading.
+ HashMap<StringName, MemberInfo> old_static_variables_indices;
+ Vector<Variant> old_static_variables;
+ void _save_old_static_data();
+ void _restore_old_static_data();
+
HashMap<StringName, int> member_lines;
HashMap<StringName, Variant> member_default_values;
List<PropertyInfo> members_cache;
@@ -123,6 +131,9 @@ class GDScript : public Script {
GDScriptFunction *implicit_initializer = nullptr;
GDScriptFunction *initializer = nullptr; //direct pointer to new , faster to locate
GDScriptFunction *implicit_ready = nullptr;
+ GDScriptFunction *static_initializer = nullptr;
+
+ Error _static_init();
int subclass_count = 0;
RBSet<Object *> instances;
@@ -268,6 +279,8 @@ public:
virtual const Variant get_rpc_config() const override;
+ void unload_static() const;
+
#ifdef TOOLS_ENABLED
virtual bool is_placeholder_fallback_enabled() const override { return placeholder_fallback_enabled; }
#endif
@@ -439,6 +452,7 @@ public:
struct {
StringName _init;
+ StringName _static_init;
StringName _notification;
StringName _set;
StringName _get;
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index aa91f8fe8d..46edc0431d 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -879,6 +879,8 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
#endif
switch (member.type) {
case GDScriptParser::ClassNode::Member::VARIABLE: {
+ bool previous_static_context = static_context;
+ static_context = member.variable->is_static;
check_class_member_name_conflict(p_class, member.variable->identifier->name, member.variable);
member.variable->set_datatype(resolving_datatype);
resolve_variable(member.variable, false);
@@ -890,6 +892,7 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
E->apply(parser, member.variable);
}
}
+ static_context = previous_static_context;
#ifdef DEBUG_ENABLED
if (member.variable->exported && member.variable->onready) {
parser->push_warning(member.variable, GDScriptWarning::ONREADY_WITH_EXPORT);
@@ -897,7 +900,7 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
if (member.variable->initializer) {
// Check if it is call to get_node() on self (using shorthand $ or not), so we can check if @onready is needed.
// This could be improved by traversing the expression fully and checking the presence of get_node at any level.
- if (!member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) {
+ if (!member.variable->is_static && !member.variable->onready && member.variable->initializer && (member.variable->initializer->type == GDScriptParser::Node::GET_NODE || member.variable->initializer->type == GDScriptParser::Node::CALL || member.variable->initializer->type == GDScriptParser::Node::CAST)) {
GDScriptParser::Node *expr = member.variable->initializer;
if (expr->type == GDScriptParser::Node::CAST) {
expr = static_cast<GDScriptParser::CastNode *>(expr)->operand;
@@ -1082,6 +1085,10 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas
p_source = p_class;
}
+#ifdef DEBUG_ENABLED
+ bool has_static_data = p_class->has_static_data;
+#endif
+
if (!p_class->resolved_interface) {
if (!parser->has_class(p_class)) {
String script_path = p_class->get_datatype().script_path;
@@ -1124,7 +1131,29 @@ void GDScriptAnalyzer::resolve_class_interface(GDScriptParser::ClassNode *p_clas
for (int i = 0; i < p_class->members.size(); i++) {
resolve_class_member(p_class, i);
+
+#ifdef DEBUG_ENABLED
+ if (!has_static_data) {
+ GDScriptParser::ClassNode::Member member = p_class->members[i];
+ if (member.type == GDScriptParser::ClassNode::Member::CLASS) {
+ has_static_data = member.m_class->has_static_data;
+ }
+ }
+#endif
+ }
+
+#ifdef DEBUG_ENABLED
+ if (!has_static_data && p_class->annotated_static_unload) {
+ GDScriptParser::Node *static_unload = nullptr;
+ for (GDScriptParser::AnnotationNode *node : p_class->annotations) {
+ if (node->name == "@static_unload") {
+ static_unload = node;
+ break;
+ }
+ }
+ parser->push_warning(static_unload ? static_unload : p_class, GDScriptWarning::REDUNDANT_STATIC_UNLOAD);
}
+#endif
}
}
@@ -1499,6 +1528,8 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
GDScriptParser::FunctionNode *previous_function = parser->current_function;
parser->current_function = p_function;
+ bool previous_static_context = static_context;
+ static_context = p_function->is_static;
GDScriptParser::DataType prev_datatype = p_function->get_datatype();
@@ -1542,6 +1573,18 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
push_error("Constructor cannot have an explicit return type.", p_function->return_type);
}
}
+ } else if (!p_is_lambda && function_name == GDScriptLanguage::get_singleton()->strings._static_init) {
+ // Static constructor.
+ GDScriptParser::DataType return_type;
+ return_type.kind = GDScriptParser::DataType::BUILTIN;
+ return_type.builtin_type = Variant::NIL;
+ p_function->set_datatype(return_type);
+ if (p_function->return_type) {
+ GDScriptParser::DataType declared_return = resolve_datatype(p_function->return_type);
+ if (declared_return.kind != GDScriptParser::DataType::BUILTIN || declared_return.builtin_type != Variant::NIL) {
+ push_error("Static constructor cannot have an explicit return type.", p_function->return_type);
+ }
+ }
} else {
if (p_function->return_type != nullptr) {
p_function->set_datatype(type_from_metatype(resolve_datatype(p_function->return_type)));
@@ -1625,6 +1668,7 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
parser->ignored_warnings = previously_ignored_warnings;
#endif
parser->current_function = previous_function;
+ static_context = previous_static_context;
}
void GDScriptAnalyzer::resolve_function_body(GDScriptParser::FunctionNode *p_function, bool p_is_lambda) {
@@ -3050,13 +3094,17 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
base_type.is_meta_type = false;
}
- if (is_self && parser->current_function != nullptr && parser->current_function->is_static && !is_static) {
- // Get the parent function above any lambda.
- GDScriptParser::FunctionNode *parent_function = parser->current_function;
- while (parent_function->source_lambda) {
- parent_function = parent_function->source_lambda->parent_function;
+ if (is_self && static_context && !is_static) {
+ if (parser->current_function) {
+ // Get the parent function above any lambda.
+ GDScriptParser::FunctionNode *parent_function = parser->current_function;
+ while (parent_function->source_lambda) {
+ parent_function = parent_function->source_lambda->parent_function;
+ }
+ push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call);
+ } else {
+ push_error(vformat(R"*(Cannot call non-static function "%s()" for static variable initializer.)*", p_call->function_name), p_call);
}
- push_error(vformat(R"*(Cannot call non-static function "%s()" from static function "%s()".)*", p_call->function_name, parent_function->identifier->name), p_call);
} else if (!is_self && base_type.is_meta_type && !is_static) {
base_type.is_meta_type = false; // For `to_string()`.
push_error(vformat(R"*(Cannot call non-static function "%s()" on the class "%s" directly. Make an instance instead.)*", p_call->function_name, base_type.to_string()), p_call);
@@ -3073,7 +3121,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
parser->push_warning(p_call, GDScriptWarning::RETURN_VALUE_DISCARDED, p_call->function_name);
}
- if (is_static && !base_type.is_meta_type && !(is_self && parser->current_function != nullptr && parser->current_function->is_static)) {
+ if (is_static && !is_constructor && !base_type.is_meta_type && !(is_self && static_context)) {
String caller_type = String(base_type.native_type);
if (caller_type.is_empty()) {
@@ -3428,9 +3476,9 @@ void GDScriptAnalyzer::reduce_identifier_from_base(GDScriptParser::IdentifierNod
}
case GDScriptParser::ClassNode::Member::VARIABLE: {
- if (is_base && !base.is_meta_type) {
+ if (is_base && (!base.is_meta_type || member.variable->is_static)) {
p_identifier->set_datatype(member.get_datatype());
- p_identifier->source = GDScriptParser::IdentifierNode::MEMBER_VARIABLE;
+ p_identifier->source = member.variable->is_static ? GDScriptParser::IdentifierNode::STATIC_VARIABLE : GDScriptParser::IdentifierNode::MEMBER_VARIABLE;
p_identifier->variable_source = member.variable;
member.variable->usages += 1;
return;
@@ -3572,6 +3620,7 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
mark_lambda_use_self();
p_identifier->variable_source->usages++;
[[fallthrough]];
+ case GDScriptParser::IdentifierNode::STATIC_VARIABLE:
case GDScriptParser::IdentifierNode::LOCAL_VARIABLE:
p_identifier->set_datatype(p_identifier->variable_source->get_datatype());
found_source = true;
@@ -3602,13 +3651,17 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
if (found_source) {
bool source_is_variable = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_VARIABLE || p_identifier->source == GDScriptParser::IdentifierNode::INHERITED_VARIABLE;
bool source_is_signal = p_identifier->source == GDScriptParser::IdentifierNode::MEMBER_SIGNAL;
- if ((source_is_variable || source_is_signal) && parser->current_function && parser->current_function->is_static) {
- // Get the parent function above any lambda.
- GDScriptParser::FunctionNode *parent_function = parser->current_function;
- while (parent_function->source_lambda) {
- parent_function = parent_function->source_lambda->parent_function;
+ if ((source_is_variable || source_is_signal) && static_context) {
+ if (parser->current_function) {
+ // Get the parent function above any lambda.
+ GDScriptParser::FunctionNode *parent_function = parser->current_function;
+ while (parent_function->source_lambda) {
+ parent_function = parent_function->source_lambda->parent_function;
+ }
+ push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_is_signal ? "signal" : "instance variable", p_identifier->name, parent_function->identifier->name), p_identifier);
+ } else {
+ push_error(vformat(R"*(Cannot access %s "%s" for a static variable initializer.)*", source_is_signal ? "signal" : "instance variable", p_identifier->name), p_identifier);
}
- push_error(vformat(R"*(Cannot access %s "%s" from the static function "%s()".)*", source_is_signal ? "signal" : "instance variable", p_identifier->name, parent_function->identifier->name), p_identifier);
}
if (!lambda_stack.is_empty()) {
diff --git a/modules/gdscript/gdscript_analyzer.h b/modules/gdscript/gdscript_analyzer.h
index 5902035bcd..0c7bf4125b 100644
--- a/modules/gdscript/gdscript_analyzer.h
+++ b/modules/gdscript/gdscript_analyzer.h
@@ -43,6 +43,7 @@ class GDScriptAnalyzer {
const GDScriptParser::EnumNode *current_enum = nullptr;
List<GDScriptParser::LambdaNode *> lambda_stack;
+ bool static_context = false;
// Tests for detecting invalid overloading of script members
static _FORCE_INLINE_ bool has_member_name_conflict_in_script_class(const StringName &p_name, const GDScriptParser::ClassNode *p_current_class_node, const GDScriptParser::Node *p_member);
diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h
index 42c6f80455..fc684e4d8f 100644
--- a/modules/gdscript/gdscript_byte_codegen.h
+++ b/modules/gdscript/gdscript_byte_codegen.h
@@ -366,6 +366,8 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator {
return p_address.address | (GDScriptFunction::ADDR_TYPE_MEMBER << GDScriptFunction::ADDR_BITS);
case Address::CONSTANT:
return p_address.address | (GDScriptFunction::ADDR_TYPE_CONSTANT << GDScriptFunction::ADDR_BITS);
+ case Address::STATIC_VARIABLE:
+ return p_address.address | (GDScriptFunction::ADDR_TYPE_STATIC_VAR << GDScriptFunction::ADDR_BITS);
case Address::LOCAL_VARIABLE:
case Address::FUNCTION_PARAMETER:
return p_address.address | (GDScriptFunction::ADDR_TYPE_STACK << GDScriptFunction::ADDR_BITS);
diff --git a/modules/gdscript/gdscript_cache.cpp b/modules/gdscript/gdscript_cache.cpp
index a009e8e0a8..126fccbbf0 100644
--- a/modules/gdscript/gdscript_cache.cpp
+++ b/modules/gdscript/gdscript_cache.cpp
@@ -342,6 +342,16 @@ Error GDScriptCache::finish_compiling(const String &p_owner) {
return err;
}
+void GDScriptCache::add_static_script(Ref<GDScript> p_script) {
+ ERR_FAIL_COND_MSG(p_script.is_null(), "Trying to cache empty script as static.");
+ ERR_FAIL_COND_MSG(!p_script->is_valid(), "Trying to cache non-compiled script as static.");
+ singleton->static_gdscript_cache[p_script->get_fully_qualified_name()] = p_script;
+}
+
+void GDScriptCache::remove_static_script(const String &p_fqcn) {
+ singleton->static_gdscript_cache.erase(p_fqcn);
+}
+
Ref<PackedScene> GDScriptCache::get_packed_scene(const String &p_path, Error &r_error, const String &p_owner) {
MutexLock lock(singleton->mutex);
diff --git a/modules/gdscript/gdscript_cache.h b/modules/gdscript/gdscript_cache.h
index c7f40f6e82..28266a1c0b 100644
--- a/modules/gdscript/gdscript_cache.h
+++ b/modules/gdscript/gdscript_cache.h
@@ -78,6 +78,7 @@ class GDScriptCache {
HashMap<String, GDScriptParserRef *> parser_map;
HashMap<String, Ref<GDScript>> shallow_gdscript_cache;
HashMap<String, Ref<GDScript>> full_gdscript_cache;
+ HashMap<String, Ref<GDScript>> static_gdscript_cache;
HashMap<String, HashSet<String>> dependencies;
HashMap<String, Ref<PackedScene>> packed_scene_cache;
HashMap<String, HashSet<String>> packed_scene_dependencies;
@@ -101,6 +102,8 @@ public:
static Ref<GDScript> get_full_script(const String &p_path, Error &r_error, const String &p_owner = String(), bool p_update_from_disk = false);
static Ref<GDScript> get_cached_script(const String &p_path);
static Error finish_compiling(const String &p_owner);
+ static void add_static_script(Ref<GDScript> p_script);
+ static void remove_static_script(const String &p_fqcn);
static Ref<PackedScene> get_packed_scene(const String &p_path, Error &r_error, const String &p_owner = "");
static void clear_unreferenced_packed_scenes();
diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h
index e82b4b08ab..dbc2466393 100644
--- a/modules/gdscript/gdscript_codegen.h
+++ b/modules/gdscript/gdscript_codegen.h
@@ -44,6 +44,7 @@ public:
CLASS,
MEMBER,
CONSTANT,
+ STATIC_VARIABLE,
LOCAL_VARIABLE,
FUNCTION_PARAMETER,
TEMPORARY,
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index 43008a5d93..327e24ef11 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -254,13 +254,29 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
gen->write_call_self(temp, codegen.script->member_indices[identifier].getter, args);
return temp;
} else {
- // No getter or inside getter: direct member access.,
+ // No getter or inside getter: direct member access.
int idx = codegen.script->member_indices[identifier].index;
return GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::MEMBER, idx, codegen.script->get_member_type(identifier));
}
}
}
+ // Try static variables.
+ if (codegen.script->static_variables_indices.has(identifier)) {
+ if (codegen.script->static_variables_indices[identifier].getter != StringName() && codegen.script->static_variables_indices[identifier].getter != codegen.function_name) {
+ // Perform getter.
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(codegen.script->static_variables_indices[identifier].data_type);
+ GDScriptCodeGenerator::Address class_addr(GDScriptCodeGenerator::Address::CLASS);
+ Vector<GDScriptCodeGenerator::Address> args; // No argument needed.
+ gen->write_call(temp, class_addr, codegen.script->static_variables_indices[identifier].getter, args);
+ return temp;
+ } else {
+ // No getter or inside getter: direct variable access.
+ int idx = codegen.script->static_variables_indices[identifier].index;
+ return GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::STATIC_VARIABLE, idx, codegen.script->static_variables_indices[identifier].data_type);
+ }
+ }
+
// Try class constants.
{
GDScript *owner = codegen.script;
@@ -563,7 +579,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
// Not exact arguments, but still can use method bind call.
gen->write_call_method_bind(result, self, method, arguments);
}
- } else if ((codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") {
+ } else if (codegen.is_static || (codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") {
GDScriptCodeGenerator::Address self;
self.mode = GDScriptCodeGenerator::Address::CLASS;
if (is_awaited) {
@@ -909,6 +925,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool is_member_property = false;
bool member_property_has_setter = false;
bool member_property_is_in_setter = false;
+ bool is_static = false;
StringName member_property_setter_function;
List<const GDScriptParser::SubscriptNode *> chain;
@@ -925,14 +942,16 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
StringName var_name = identifier->name;
if (_is_class_member_property(codegen, var_name)) {
assign_class_member_property = var_name;
- } else if (!_is_local_or_parameter(codegen, var_name) && codegen.script->member_indices.has(var_name)) {
+ } else if (!_is_local_or_parameter(codegen, var_name) && (codegen.script->member_indices.has(var_name) || codegen.script->static_variables_indices.has(var_name))) {
is_member_property = true;
- member_property_setter_function = codegen.script->member_indices[var_name].setter;
+ is_static = codegen.script->static_variables_indices.has(var_name);
+ const GDScript::MemberInfo &minfo = is_static ? codegen.script->static_variables_indices[var_name] : codegen.script->member_indices[var_name];
+ member_property_setter_function = minfo.setter;
member_property_has_setter = member_property_setter_function != StringName();
member_property_is_in_setter = member_property_has_setter && member_property_setter_function == codegen.function_name;
- target_member_property.mode = GDScriptCodeGenerator::Address::MEMBER;
- target_member_property.address = codegen.script->member_indices[var_name].index;
- target_member_property.type = codegen.script->member_indices[var_name].data_type;
+ target_member_property.mode = is_static ? GDScriptCodeGenerator::Address::STATIC_VARIABLE : GDScriptCodeGenerator::Address::MEMBER;
+ target_member_property.address = minfo.index;
+ target_member_property.type = minfo.data_type;
}
}
break;
@@ -1085,7 +1104,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
if (member_property_has_setter && !member_property_is_in_setter) {
Vector<GDScriptCodeGenerator::Address> args;
args.push_back(assigned);
- gen->write_call(GDScriptCodeGenerator::Address(), GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::SELF), member_property_setter_function, args);
+ GDScriptCodeGenerator::Address self = is_static ? GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::CLASS) : GDScriptCodeGenerator::Address(GDScriptCodeGenerator::Address::SELF);
+ gen->write_call(GDScriptCodeGenerator::Address(), self, member_property_setter_function, args);
} else {
gen->write_assign(target_member_property, assigned);
}
@@ -1134,16 +1154,19 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool is_member = false;
bool has_setter = false;
bool is_in_setter = false;
+ bool is_static = false;
StringName setter_function;
StringName var_name = static_cast<const GDScriptParser::IdentifierNode *>(assignment->assignee)->name;
- if (!_is_local_or_parameter(codegen, var_name) && codegen.script->member_indices.has(var_name)) {
+ if (!_is_local_or_parameter(codegen, var_name) && (codegen.script->member_indices.has(var_name) || codegen.script->static_variables_indices.has(var_name))) {
is_member = true;
- setter_function = codegen.script->member_indices[var_name].setter;
+ is_static = codegen.script->static_variables_indices.has(var_name);
+ GDScript::MemberInfo &minfo = is_static ? codegen.script->static_variables_indices[var_name] : codegen.script->member_indices[var_name];
+ setter_function = minfo.setter;
has_setter = setter_function != StringName();
is_in_setter = has_setter && setter_function == codegen.function_name;
- member.mode = GDScriptCodeGenerator::Address::MEMBER;
- member.address = codegen.script->member_indices[var_name].index;
- member.type = codegen.script->member_indices[var_name].data_type;
+ member.mode = is_static ? GDScriptCodeGenerator::Address::STATIC_VARIABLE : GDScriptCodeGenerator::Address::MEMBER;
+ member.address = minfo.index;
+ member.type = minfo.data_type;
}
GDScriptCodeGenerator::Address target;
@@ -2001,6 +2024,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
}
codegen.function_name = func_name;
+ codegen.is_static = is_static;
codegen.generator->write_start(p_script, func_name, is_static, rpc_config, return_type);
int optional_parameters = 0;
@@ -2024,7 +2048,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
bool is_implicit_ready = !p_func && p_for_ready;
if (!p_for_lambda && is_implicit_initializer) {
- // Initialize the default values for type variables before anything.
+ // Initialize the default values for typed variables before anything.
// This avoids crashes if they are accessed with validated calls before being properly initialized.
// It may happen with out-of-order access or with `@onready` variables.
for (const GDScriptParser::ClassNode::Member &member : p_class->members) {
@@ -2033,6 +2057,10 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
}
const GDScriptParser::VariableNode *field = member.variable;
+ if (field->is_static) {
+ continue;
+ }
+
GDScriptDataType field_type = _gdtype_from_datatype(field->get_datatype(), codegen.script);
GDScriptCodeGenerator::Address dst_address(GDScriptCodeGenerator::Address::MEMBER, codegen.script->member_indices[field->identifier->name].index, field_type);
@@ -2056,6 +2084,10 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
continue;
}
const GDScriptParser::VariableNode *field = p_class->members[i].variable;
+ if (field->is_static) {
+ continue;
+ }
+
if (field->onready != is_implicit_ready) {
// Only initialize in @implicit_ready.
continue;
@@ -2183,6 +2215,135 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
return gd_function;
}
+GDScriptFunction *GDScriptCompiler::_make_static_initializer(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class) {
+ r_error = OK;
+ CodeGen codegen;
+ codegen.generator = memnew(GDScriptByteCodeGenerator);
+
+ codegen.class_node = p_class;
+ codegen.script = p_script;
+
+ StringName func_name = SNAME("@static_initializer");
+ bool is_static = true;
+ Variant rpc_config;
+ GDScriptDataType return_type;
+ return_type.has_type = true;
+ return_type.kind = GDScriptDataType::BUILTIN;
+ return_type.builtin_type = Variant::NIL;
+
+ codegen.function_name = func_name;
+ codegen.is_static = is_static;
+ codegen.generator->write_start(p_script, func_name, is_static, rpc_config, return_type);
+
+ GDScriptCodeGenerator::Address class_addr(GDScriptCodeGenerator::Address::CLASS);
+
+ // Initialize the default values for typed variables before anything.
+ // This avoids crashes if they are accessed with validated calls before being properly initialized.
+ // It may happen with out-of-order access or with `@onready` variables.
+ for (const GDScriptParser::ClassNode::Member &member : p_class->members) {
+ if (member.type != GDScriptParser::ClassNode::Member::VARIABLE) {
+ continue;
+ }
+
+ const GDScriptParser::VariableNode *field = member.variable;
+ if (!field->is_static) {
+ continue;
+ }
+
+ GDScriptDataType field_type = _gdtype_from_datatype(field->get_datatype(), codegen.script);
+
+ if (field_type.has_type) {
+ codegen.generator->write_newline(field->start_line);
+
+ if (field_type.has_container_element_type()) {
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ codegen.generator->write_construct_typed_array(temp, field_type.get_container_element_type(), Vector<GDScriptCodeGenerator::Address>());
+ codegen.generator->write_set_named(class_addr, field->identifier->name, temp);
+ codegen.generator->pop_temporary();
+
+ } else if (field_type.kind == GDScriptDataType::BUILTIN) {
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ codegen.generator->write_construct(temp, field_type.builtin_type, Vector<GDScriptCodeGenerator::Address>());
+ codegen.generator->write_set_named(class_addr, field->identifier->name, temp);
+ codegen.generator->pop_temporary();
+ }
+ // The `else` branch is for objects, in such case we leave it as `null`.
+ }
+ }
+
+ for (int i = 0; i < p_class->members.size(); i++) {
+ // Initialize static fields.
+ if (p_class->members[i].type != GDScriptParser::ClassNode::Member::VARIABLE) {
+ continue;
+ }
+ const GDScriptParser::VariableNode *field = p_class->members[i].variable;
+ if (!field->is_static) {
+ continue;
+ }
+
+ GDScriptDataType field_type = _gdtype_from_datatype(field->get_datatype(), codegen.script);
+
+ if (field->initializer) {
+ // Emit proper line change.
+ codegen.generator->write_newline(field->initializer->start_line);
+
+ GDScriptCodeGenerator::Address src_address = _parse_expression(codegen, r_error, field->initializer, false, true);
+ if (r_error) {
+ memdelete(codegen.generator);
+ return nullptr;
+ }
+
+ GDScriptCodeGenerator::Address temp = codegen.add_temporary(field_type);
+ if (field->use_conversion_assign) {
+ codegen.generator->write_assign_with_conversion(temp, src_address);
+ } else {
+ codegen.generator->write_assign(temp, src_address);
+ }
+ if (src_address.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
+ codegen.generator->pop_temporary();
+ }
+
+ codegen.generator->write_set_named(class_addr, field->identifier->name, temp);
+ codegen.generator->pop_temporary();
+ }
+ }
+
+ if (p_script->has_method(GDScriptLanguage::get_singleton()->strings._static_init)) {
+ codegen.generator->write_newline(p_class->start_line);
+ codegen.generator->write_call(GDScriptCodeGenerator::Address(), class_addr, GDScriptLanguage::get_singleton()->strings._static_init, Vector<GDScriptCodeGenerator::Address>());
+ }
+
+#ifdef DEBUG_ENABLED
+ if (EngineDebugger::is_active()) {
+ String signature;
+ // Path.
+ if (!p_script->get_script_path().is_empty()) {
+ signature += p_script->get_script_path();
+ }
+ // Location.
+ signature += "::0";
+
+ // Function and class.
+
+ if (p_class->identifier) {
+ signature += "::" + String(p_class->identifier->name) + "." + String(func_name);
+ } else {
+ signature += "::" + String(func_name);
+ }
+
+ codegen.generator->set_signature(signature);
+ }
+#endif
+
+ codegen.generator->set_initial_line(p_class->start_line);
+
+ GDScriptFunction *gd_function = codegen.generator->write_end();
+
+ memdelete(codegen.generator);
+
+ return gd_function;
+}
+
Error GDScriptCompiler::_parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter) {
Error err = OK;
@@ -2236,19 +2397,28 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
}
member_functions.clear();
+ p_script->static_variables.clear();
+
if (p_script->implicit_initializer) {
memdelete(p_script->implicit_initializer);
}
if (p_script->implicit_ready) {
memdelete(p_script->implicit_ready);
}
+ if (p_script->static_initializer) {
+ memdelete(p_script->static_initializer);
+ }
+
p_script->member_functions.clear();
p_script->member_indices.clear();
p_script->member_info.clear();
+ p_script->static_variables_indices.clear();
+ p_script->static_variables.clear();
p_script->_signals.clear();
p_script->initializer = nullptr;
p_script->implicit_initializer = nullptr;
p_script->implicit_ready = nullptr;
+ p_script->static_initializer = nullptr;
p_script->clearing = false;
@@ -2357,9 +2527,14 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
prop_info.usage = PROPERTY_USAGE_SCRIPT_VARIABLE;
}
- p_script->member_info[name] = prop_info;
- p_script->member_indices[name] = minfo;
- p_script->members.insert(name);
+ if (variable->is_static) {
+ minfo.index = p_script->static_variables_indices.size();
+ p_script->static_variables_indices[name] = minfo;
+ } else {
+ p_script->member_info[name] = prop_info;
+ p_script->member_indices[name] = minfo;
+ p_script->members.insert(name);
+ }
#ifdef TOOLS_ENABLED
if (variable->initializer != nullptr && variable->initializer->is_constant) {
@@ -2427,6 +2602,8 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
}
}
+ p_script->static_variables.resize(p_script->static_variables_indices.size());
+
parsed_classes.insert(p_script);
parsing_classes.erase(p_script);
@@ -2503,6 +2680,15 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
}
}
+ if (p_class->has_static_data) {
+ Error err = OK;
+ GDScriptFunction *func = _make_static_initializer(err, p_script, p_class);
+ p_script->static_initializer = func;
+ if (err) {
+ return err;
+ }
+ }
+
#ifdef DEBUG_ENABLED
//validate instances if keeping state
@@ -2552,6 +2738,8 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
}
#endif //DEBUG_ENABLED
+ has_static_data = p_class->has_static_data;
+
for (int i = 0; i < p_class->members.size(); i++) {
if (p_class->members[i].type != GDScriptParser::ClassNode::Member::CLASS) {
continue;
@@ -2564,6 +2752,8 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser:
if (err) {
return err;
}
+
+ has_static_data = has_static_data || inner_class->has_static_data;
}
p_script->_init_rpc_methods_properties();
@@ -2650,6 +2840,10 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri
return err;
}
+ if (has_static_data && !root->annotated_static_unload) {
+ GDScriptCache::add_static_script(p_script);
+ }
+
return GDScriptCache::finish_compiling(main_script->get_path());
}
diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h
index 5328c17c73..2d15d461fb 100644
--- a/modules/gdscript/gdscript_compiler.h
+++ b/modules/gdscript/gdscript_compiler.h
@@ -52,6 +52,7 @@ class GDScriptCompiler {
HashMap<StringName, GDScriptCodeGenerator::Address> parameters;
HashMap<StringName, GDScriptCodeGenerator::Address> locals;
List<HashMap<StringName, GDScriptCodeGenerator::Address>> locals_stack;
+ bool is_static = false;
GDScriptCodeGenerator::Address add_local(const StringName &p_name, const GDScriptDataType &p_type) {
uint32_t addr = generator->add_local(p_name, p_type);
@@ -130,6 +131,7 @@ class GDScriptCompiler {
void _add_locals_in_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block);
Error _parse_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block, bool p_add_locals = true);
GDScriptFunction *_parse_function(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::FunctionNode *p_func, bool p_for_ready = false, bool p_for_lambda = false);
+ GDScriptFunction *_make_static_initializer(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class);
Error _parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter);
Error _populate_class_members(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state);
Error _compile_class(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state);
@@ -138,6 +140,7 @@ class GDScriptCompiler {
StringName source;
String error;
GDScriptParser::ExpressionNode *awaited_node = nullptr;
+ bool has_static_data = false;
public:
static void convert_to_initializer_type(Variant &p_variant, const GDScriptParser::VariableNode *p_node);
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index be33c7c591..f1ac234d28 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -2992,6 +2992,15 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
List<MethodInfo> virtual_methods;
ClassDB::get_virtual_methods(class_name, &virtual_methods);
+
+ {
+ // Not truly a virtual method, but can also be "overridden".
+ MethodInfo static_init("_static_init");
+ static_init.return_val.type = Variant::NIL;
+ static_init.flags |= METHOD_FLAG_STATIC | METHOD_FLAG_VIRTUAL;
+ virtual_methods.push_back(static_init);
+ }
+
for (const MethodInfo &mi : virtual_methods) {
String method_hint = mi.name;
if (method_hint.contains(":")) {
diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h
index 1a5e9eef53..390e562e6f 100644
--- a/modules/gdscript/gdscript_function.h
+++ b/modules/gdscript/gdscript_function.h
@@ -409,7 +409,8 @@ public:
ADDR_TYPE_STACK = 0,
ADDR_TYPE_CONSTANT = 1,
ADDR_TYPE_MEMBER = 2,
- ADDR_TYPE_MAX = 3,
+ ADDR_TYPE_STATIC_VAR = 3,
+ ADDR_TYPE_MAX = 4,
};
enum FixedAddresses {
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index 6a4a4e66f2..d3529154cf 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -81,6 +81,8 @@ GDScriptParser::GDScriptParser() {
// TODO: Should this be static?
register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation);
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
+ register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);
+
register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
// Export annotations.
register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>);
@@ -623,7 +625,7 @@ bool GDScriptParser::has_class(const GDScriptParser::ClassNode *p_class) const {
return false;
}
-GDScriptParser::ClassNode *GDScriptParser::parse_class() {
+GDScriptParser::ClassNode *GDScriptParser::parse_class(bool p_is_static) {
ClassNode *n_class = alloc_node<ClassNode>();
ClassNode *previous_class = current_class;
@@ -724,7 +726,7 @@ void GDScriptParser::parse_extends() {
}
template <class T>
-void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind) {
+void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static) {
advance();
#ifdef TOOLS_ENABLED
@@ -749,7 +751,7 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)()
#endif // TOOLS_ENABLED
}
- T *member = (this->*p_parse_function)();
+ T *member = (this->*p_parse_function)(p_is_static);
if (member == nullptr) {
return;
}
@@ -803,10 +805,15 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)()
void GDScriptParser::parse_class_body(bool p_is_multiline) {
bool class_end = false;
+ bool next_is_static = false;
while (!class_end && !is_at_end()) {
- switch (current.type) {
+ GDScriptTokenizer::Token token = current;
+ switch (token.type) {
case GDScriptTokenizer::Token::VAR:
- parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable");
+ parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable", next_is_static);
+ if (next_is_static) {
+ current_class->has_static_data = true;
+ }
break;
case GDScriptTokenizer::Token::CONST:
parse_class_member(&GDScriptParser::parse_constant, AnnotationInfo::CONSTANT, "constant");
@@ -814,9 +821,8 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
case GDScriptTokenizer::Token::SIGNAL:
parse_class_member(&GDScriptParser::parse_signal, AnnotationInfo::SIGNAL, "signal");
break;
- case GDScriptTokenizer::Token::STATIC:
case GDScriptTokenizer::Token::FUNC:
- parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function");
+ parse_class_member(&GDScriptParser::parse_function, AnnotationInfo::FUNCTION, "function", next_is_static);
break;
case GDScriptTokenizer::Token::CLASS:
parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class");
@@ -824,6 +830,13 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
case GDScriptTokenizer::Token::ENUM:
parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum");
break;
+ case GDScriptTokenizer::Token::STATIC: {
+ advance();
+ next_is_static = true;
+ if (!check(GDScriptTokenizer::Token::FUNC) && !check(GDScriptTokenizer::Token::VAR)) {
+ push_error(R"(Expected "func" or "var" after "static".)");
+ }
+ } break;
case GDScriptTokenizer::Token::ANNOTATION: {
advance();
@@ -870,6 +883,9 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
advance();
break;
}
+ if (token.type != GDScriptTokenizer::Token::STATIC) {
+ next_is_static = false;
+ }
if (panic_mode) {
synchronize();
}
@@ -879,11 +895,11 @@ void GDScriptParser::parse_class_body(bool p_is_multiline) {
}
}
-GDScriptParser::VariableNode *GDScriptParser::parse_variable() {
- return parse_variable(true);
+GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static) {
+ return parse_variable(p_is_static, true);
}
-GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_property) {
+GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_is_static, bool p_allow_property) {
VariableNode *variable = alloc_node<VariableNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) {
@@ -893,6 +909,7 @@ GDScriptParser::VariableNode *GDScriptParser::parse_variable(bool p_allow_proper
variable->identifier = parse_identifier();
variable->export_info.name = variable->identifier->name;
+ variable->is_static = p_is_static;
if (match(GDScriptTokenizer::Token::COLON)) {
if (check(GDScriptTokenizer::Token::NEWLINE)) {
@@ -1036,6 +1053,7 @@ void GDScriptParser::parse_property_setter(VariableNode *p_variable) {
complete_extents(identifier);
identifier->name = "@" + p_variable->identifier->name + "_setter";
function->identifier = identifier;
+ function->is_static = p_variable->is_static;
consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected "(" after "set".)");
@@ -1087,6 +1105,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
complete_extents(identifier);
identifier->name = "@" + p_variable->identifier->name + "_getter";
function->identifier = identifier;
+ function->is_static = p_variable->is_static;
FunctionNode *previous_function = current_function;
current_function = function;
@@ -1111,7 +1130,7 @@ void GDScriptParser::parse_property_getter(VariableNode *p_variable) {
}
}
-GDScriptParser::ConstantNode *GDScriptParser::parse_constant() {
+GDScriptParser::ConstantNode *GDScriptParser::parse_constant(bool p_is_static) {
ConstantNode *constant = alloc_node<ConstantNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) {
@@ -1178,7 +1197,7 @@ GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() {
return parameter;
}
-GDScriptParser::SignalNode *GDScriptParser::parse_signal() {
+GDScriptParser::SignalNode *GDScriptParser::parse_signal(bool p_is_static) {
SignalNode *signal = alloc_node<SignalNode>();
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) {
@@ -1223,7 +1242,7 @@ GDScriptParser::SignalNode *GDScriptParser::parse_signal() {
return signal;
}
-GDScriptParser::EnumNode *GDScriptParser::parse_enum() {
+GDScriptParser::EnumNode *GDScriptParser::parse_enum(bool p_is_static) {
EnumNode *enum_node = alloc_node<EnumNode>();
bool named = false;
@@ -1372,23 +1391,23 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
}
}
+ if (!p_function->source_lambda && p_function->identifier && p_function->identifier->name == GDScriptLanguage::get_singleton()->strings._static_init) {
+ if (!p_function->is_static) {
+ push_error(R"(Static constructor must be declared static.)");
+ }
+ if (p_function->parameters.size() != 0) {
+ push_error(R"(Static constructor cannot have parameters.)");
+ }
+ current_class->has_static_data = true;
+ }
+
// TODO: Improve token consumption so it synchronizes to a statement boundary. This way we can get into the function body with unrecognized tokens.
consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":" after %s declaration.)", p_type));
}
-GDScriptParser::FunctionNode *GDScriptParser::parse_function() {
+GDScriptParser::FunctionNode *GDScriptParser::parse_function(bool p_is_static) {
FunctionNode *function = alloc_node<FunctionNode>();
- bool _static = false;
- if (previous.type == GDScriptTokenizer::Token::STATIC) {
- // TODO: Improve message if user uses "static" with "var" or "const"
- if (!consume(GDScriptTokenizer::Token::FUNC, R"(Expected "func" after "static".)")) {
- complete_extents(function);
- return nullptr;
- }
- _static = true;
- }
-
make_completion_context(COMPLETION_OVERRIDE_METHOD, function);
if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after "func".)")) {
@@ -1400,7 +1419,7 @@ GDScriptParser::FunctionNode *GDScriptParser::parse_function() {
current_function = function;
function->identifier = parse_identifier();
- function->is_static = _static;
+ function->is_static = p_is_static;
SuiteNode *body = alloc_node<SuiteNode>();
SuiteNode *previous_suite = current_suite;
@@ -1612,11 +1631,11 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
break;
case GDScriptTokenizer::Token::VAR:
advance();
- result = parse_variable();
+ result = parse_variable(false, false);
break;
case GDScriptTokenizer::Token::CONST:
advance();
- result = parse_constant();
+ result = parse_constant(false);
break;
case GDScriptTokenizer::Token::IF:
advance();
@@ -1646,7 +1665,7 @@ GDScriptParser::Node *GDScriptParser::parse_statement() {
advance();
ReturnNode *n_return = alloc_node<ReturnNode>();
if (!is_statement_end()) {
- if (current_function && current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init) {
+ if (current_function && (current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._init || current_function->identifier->name == GDScriptLanguage::get_singleton()->strings._static_init)) {
push_error(R"(Constructor cannot return a value.)");
}
n_return->return_value = parse_expression(false);
@@ -4101,6 +4120,17 @@ bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_
return true;
}
+bool GDScriptParser::static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target) {
+ ERR_FAIL_COND_V_MSG(p_target->type != Node::CLASS, false, vformat(R"("%s" annotation can only be applied to classes.)", p_annotation->name));
+ ClassNode *p_class = static_cast<ClassNode *>(p_target);
+ if (p_class->annotated_static_unload) {
+ push_error(vformat(R"("%s" annotation can only be used once per script.)", p_annotation->name), p_annotation);
+ return false;
+ }
+ p_class->annotated_static_unload = true;
+ return true;
+}
+
GDScriptParser::DataType GDScriptParser::SuiteNode::Local::get_datatype() const {
switch (type) {
case CONSTANT:
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index d85a527406..8f0265510f 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -709,6 +709,8 @@ public:
ClassNode *outer = nullptr;
bool extends_used = false;
bool onready_used = false;
+ bool has_static_data = false;
+ bool annotated_static_unload = false;
String extends_path;
Vector<IdentifierNode *> extends; // List for indexing: extends A.B.C
DataType base_type;
@@ -847,6 +849,7 @@ public:
LOCAL_BIND, // Pattern bind.
MEMBER_SIGNAL,
MEMBER_VARIABLE,
+ STATIC_VARIABLE,
MEMBER_CONSTANT,
INHERITED_VARIABLE,
};
@@ -1202,6 +1205,7 @@ public:
bool onready = false;
PropertyInfo export_info;
int assignments = 0;
+ bool is_static = false;
#ifdef TOOLS_ENABLED
String doc_description;
#endif // TOOLS_ENABLED
@@ -1405,16 +1409,16 @@ private:
// Main blocks.
void parse_program();
- ClassNode *parse_class();
+ ClassNode *parse_class(bool p_is_static);
void parse_class_name();
void parse_extends();
void parse_class_body(bool p_is_multiline);
template <class T>
- void parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind);
- SignalNode *parse_signal();
- EnumNode *parse_enum();
+ void parse_class_member(T *(GDScriptParser::*p_parse_function)(bool), AnnotationInfo::TargetKind p_target, const String &p_member_kind, bool p_is_static = false);
+ SignalNode *parse_signal(bool p_is_static);
+ EnumNode *parse_enum(bool p_is_static);
ParameterNode *parse_parameter();
- FunctionNode *parse_function();
+ FunctionNode *parse_function(bool p_is_static);
void parse_function_signature(FunctionNode *p_function, SuiteNode *p_body, const String &p_type);
SuiteNode *parse_suite(const String &p_context, SuiteNode *p_suite = nullptr, bool p_for_lambda = false);
// Annotations
@@ -1431,14 +1435,15 @@ private:
bool export_group_annotations(const AnnotationNode *p_annotation, Node *p_target);
bool warning_annotations(const AnnotationNode *p_annotation, Node *p_target);
bool rpc_annotation(const AnnotationNode *p_annotation, Node *p_target);
+ bool static_unload_annotation(const AnnotationNode *p_annotation, Node *p_target);
// Statements.
Node *parse_statement();
- VariableNode *parse_variable();
- VariableNode *parse_variable(bool p_allow_property);
+ VariableNode *parse_variable(bool p_is_static);
+ VariableNode *parse_variable(bool p_is_static, bool p_allow_property);
VariableNode *parse_property(VariableNode *p_variable, bool p_need_indent);
void parse_property_getter(VariableNode *p_variable);
void parse_property_setter(VariableNode *p_variable);
- ConstantNode *parse_constant();
+ ConstantNode *parse_constant(bool p_is_static);
AssertNode *parse_assert();
BreakNode *parse_break();
ContinueNode *parse_continue();
diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp
index 0855a670df..4e18045f3d 100644
--- a/modules/gdscript/gdscript_vm.cpp
+++ b/modules/gdscript/gdscript_vm.cpp
@@ -680,10 +680,10 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
bool awaited = false;
#endif
#ifdef DEBUG_ENABLED
- int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? p_instance->members.size() : 0 };
+ int variant_address_limits[ADDR_TYPE_MAX] = { _stack_size, _constant_count, p_instance ? p_instance->members.size() : 0, script->static_variables.size() };
#endif
- Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr };
+ Variant *variant_addresses[ADDR_TYPE_MAX] = { stack, _constants_ptr, p_instance ? p_instance->members.ptrw() : nullptr, script->static_variables.ptrw() };
#ifdef DEBUG_ENABLED
OPCODE_WHILE(ip < _code_size) {
diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp
index 0cb8e3a2af..c7f72f4eed 100644
--- a/modules/gdscript/gdscript_warning.cpp
+++ b/modules/gdscript/gdscript_warning.cpp
@@ -185,6 +185,9 @@ String GDScriptWarning::get_message() const {
case ONREADY_WITH_EXPORT: {
return R"("@onready" will set the default value after "@export" takes effect and will override it.)";
}
+ case REDUNDANT_STATIC_UNLOAD: {
+ return R"(The "@static_unload" annotation is redundant because the file does not have a class with static variables.)";
+ }
case WARNING_MAX:
break; // Can't happen, but silences warning
}
@@ -254,6 +257,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
"NATIVE_METHOD_OVERRIDE",
"GET_NODE_DEFAULT_WITHOUT_ONREADY",
"ONREADY_WITH_EXPORT",
+ "REDUNDANT_STATIC_UNLOAD",
};
static_assert((sizeof(names) / sizeof(*names)) == WARNING_MAX, "Amount of warning types don't match the amount of warning names.");
diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h
index f0123c518c..7972e6d993 100644
--- a/modules/gdscript/gdscript_warning.h
+++ b/modules/gdscript/gdscript_warning.h
@@ -86,6 +86,7 @@ public:
NATIVE_METHOD_OVERRIDE, // The script method overrides a native one, this may not work as intended.
GET_NODE_DEFAULT_WITHOUT_ONREADY, // A class variable uses `get_node()` (or the `$` notation) as its default value, but does not use the @onready annotation.
ONREADY_WITH_EXPORT, // The `@onready` annotation will set the value after `@export` which is likely not intended.
+ REDUNDANT_STATIC_UNLOAD, // The `@static_unload` annotation is used but the class does not have static data.
WARNING_MAX,
};
@@ -130,6 +131,7 @@ public:
ERROR, // NATIVE_METHOD_OVERRIDE // May not work as expected.
ERROR, // GET_NODE_DEFAULT_WITHOUT_ONREADY // May not work as expected.
ERROR, // ONREADY_WITH_EXPORT // May not work as expected.
+ WARN, // REDUNDANT_STATIC_UNLOAD
};
static_assert((sizeof(default_warning_levels) / sizeof(default_warning_levels[0])) == WARNING_MAX, "Amount of default levels does not match the amount of warnings.");
diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp
index 6299e5d584..b8448d16c2 100644
--- a/modules/gdscript/tests/gdscript_test_runner.cpp
+++ b/modules/gdscript/tests/gdscript_test_runner.cpp
@@ -566,6 +566,14 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) {
ERR_FAIL_V_MSG(result, "\nCould not find test function on: '" + source_file + "'");
}
+ // Setup output handlers.
+ ErrorHandlerData error_data(&result, this);
+
+ _print_handler.userdata = &result;
+ _error_handler.userdata = &error_data;
+ add_print_handler(&_print_handler);
+ add_error_handler(&_error_handler);
+
script->reload();
// Create object instance for test.
@@ -577,14 +585,6 @@ GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) {
obj->set_script(script);
GDScriptInstance *instance = static_cast<GDScriptInstance *>(obj->get_script_instance());
- // Setup output handlers.
- ErrorHandlerData error_data(&result, this);
-
- _print_handler.userdata = &result;
- _error_handler.userdata = &error_data;
- add_print_handler(&_print_handler);
- add_error_handler(&_error_handler);
-
// Call test function.
Callable::CallError call_err;
instance->callp(GDScriptTestRunner::test_function_name, nullptr, 0, call_err);
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd
new file mode 100644
index 0000000000..3dac751ba9
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.gd
@@ -0,0 +1,5 @@
+static func _static_init() -> int:
+ print("static init")
+
+func test():
+ print("done")
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out
new file mode 100644
index 0000000000..42294b7afb
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_constructor_with_return_type.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Static constructor cannot have an explicit return type.
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd
new file mode 100644
index 0000000000..1ae814ea34
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.gd
@@ -0,0 +1,9 @@
+@static_unload
+
+func non_static():
+ return "non static"
+
+static var static_var = non_static()
+
+func test():
+ print("does not run")
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out
new file mode 100644
index 0000000000..f1e9ec34f2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/static_var_init_non_static_call.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Cannot call non-static function "non_static()" for static variable initializer.
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd
new file mode 100644
index 0000000000..cbfa1f314f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.gd
@@ -0,0 +1,5 @@
+func _static_init():
+ print("static init")
+
+func test():
+ print("done")
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out
new file mode 100644
index 0000000000..b2b8711e96
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_not_static.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Static constructor must be declared static.
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd
new file mode 100644
index 0000000000..711243f822
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.gd
@@ -0,0 +1,6 @@
+static func _static_init():
+ print("static init")
+ return true
+
+func test():
+ print("done")
diff --git a/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out
new file mode 100644
index 0000000000..a034850e86
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/errors/static_constructor_returning_something.out
@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Constructor cannot return a value.
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_constructor.gd b/modules/gdscript/tests/scripts/runtime/features/static_constructor.gd
new file mode 100644
index 0000000000..e08f77df12
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_constructor.gd
@@ -0,0 +1,13 @@
+@static_unload
+
+static var foo = "bar"
+
+static func _static_init():
+ print("static init called")
+ prints("foo is", foo)
+
+func test():
+ var _lambda = func _static_init():
+ print("lambda does not conflict with static constructor")
+
+ print("done")
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_constructor.out b/modules/gdscript/tests/scripts/runtime/features/static_constructor.out
new file mode 100644
index 0000000000..7f72a0ac2c
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_constructor.out
@@ -0,0 +1,4 @@
+GDTEST_OK
+static init called
+foo is bar
+done
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables.gd b/modules/gdscript/tests/scripts/runtime/features/static_variables.gd
new file mode 100644
index 0000000000..e193312381
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables.gd
@@ -0,0 +1,56 @@
+@static_unload
+
+static var perm := 0
+
+static var prop := "Hello!":
+ get: return prop + " suffix"
+ set(value): prop = "prefix " + str(value)
+
+static func get_data():
+ return "data"
+
+static var data = get_data()
+
+class Inner:
+ static var prop := "inner"
+ static func _static_init() -> void:
+ prints("Inner._static_init", prop)
+
+ class InnerInner:
+ static var prop := "inner inner"
+ static func _static_init() -> void:
+ prints("InnerInner._static_init", prop)
+
+func test():
+ prints("data:", data)
+
+ prints("perm:", perm)
+ prints("prop:", prop)
+
+ perm = 1
+ prop = "World!"
+
+ prints("perm:", perm)
+ prints("prop:", prop)
+
+ print("other.perm:", StaticVariablesOther.perm)
+ print("other.prop:", StaticVariablesOther.prop)
+
+ StaticVariablesOther.perm = 2
+ StaticVariablesOther.prop = "foo"
+
+ print("other.perm:", StaticVariablesOther.perm)
+ print("other.prop:", StaticVariablesOther.prop)
+
+ @warning_ignore("unsafe_method_access")
+ var path = get_script().get_path().get_base_dir()
+ var other = load(path + "/static_variables_load.gd")
+
+ print("load.perm:", other.perm)
+ print("load.prop:", other.prop)
+
+ other.perm = 3
+ other.prop = "bar"
+
+ print("load.perm:", other.perm)
+ print("load.prop:", other.prop)
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables.out b/modules/gdscript/tests/scripts/runtime/features/static_variables.out
new file mode 100644
index 0000000000..d2491aef5e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables.out
@@ -0,0 +1,16 @@
+GDTEST_OK
+Inner._static_init inner
+InnerInner._static_init inner inner
+data: data
+perm: 0
+prop: prefix Hello! suffix
+perm: 1
+prop: prefix World! suffix
+other.perm:0
+other.prop:prefix Hello! suffix
+other.perm:2
+other.prop:prefix foo suffix
+load.perm:0
+load.prop:prefix Hello! suffix
+load.perm:3
+load.prop:prefix bar suffix
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd
new file mode 100644
index 0000000000..8913b02756
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.gd
@@ -0,0 +1,10 @@
+@static_unload
+
+static var perm := 0
+
+static var prop := "Hello!":
+ get: return prop + " suffix"
+ set(value): prop = "prefix " + str(value)
+
+func test():
+ print("ok")
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_load.out b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_load.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd
new file mode 100644
index 0000000000..7a3b0acca6
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.gd
@@ -0,0 +1,11 @@
+@static_unload
+class_name StaticVariablesOther
+
+static var perm := 0
+
+static var prop := "Hello!":
+ get: return prop + " suffix"
+ set(value): prop = "prefix " + str(value)
+
+func test():
+ print("ok")
diff --git a/modules/gdscript/tests/scripts/runtime/features/static_variables_other.out b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.out
new file mode 100644
index 0000000000..1b47ed10dc
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/static_variables_other.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+ok