diff options
author | George Marques <george@gmarqu.es> | 2023-04-19 11:10:35 -0300 |
---|---|---|
committer | George Marques <george@gmarqu.es> | 2023-04-27 09:51:44 -0300 |
commit | 0ba6048ad3c945e2bd1d0114b5095535c22103ce (patch) | |
tree | e9d17e159dab50dc6cc3d574a25647af01939d6c | |
parent | 352ebe97259622f20b47627b4bf747cdfc79304d (diff) | |
download | redot-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.
36 files changed, 689 insertions, 86 deletions
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 4a6368ca6c..949253c998 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -456,6 +456,9 @@ <member name="debug/gdscript/warnings/redundant_await" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a function that is not a coroutine is called with await. </member> + <member name="debug/gdscript/warnings/redundant_static_unload" type="int" setter="" getter="" default="1"> + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the [code]@static_unload[/code] annotation is used in a script without any static variables. + </member> <member name="debug/gdscript/warnings/renamed_in_godot_4_hint" type="bool" setter="" getter="" default="1"> When enabled, using a property, enum, or function that was renamed since Godot 3 will produce a hint if an error occurs. </member> 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 |