瀏覽代碼

Merge pull request #82808 from dalexeev/gds-vararg

GDScript: Add support for variadic functions
Thaddeus Crews 2 月之前
父節點
當前提交
0f05e91889
共有 33 個文件被更改,包括 416 次插入65 次删除
  1. 2 0
      core/doc_data.h
  2. 20 13
      editor/connections_dialog.cpp
  3. 86 16
      editor/editor_help.cpp
  4. 1 0
      editor/editor_help.h
  5. 8 0
      editor/property_selector.cpp
  6. 18 6
      modules/gdscript/editor/gdscript_docgen.cpp
  7. 1 1
      modules/gdscript/editor/gdscript_highlighter.cpp
  8. 48 4
      modules/gdscript/gdscript_analyzer.cpp
  9. 10 0
      modules/gdscript/gdscript_compiler.cpp
  10. 26 3
      modules/gdscript/gdscript_editor.cpp
  11. 3 2
      modules/gdscript/gdscript_function.cpp
  12. 2 0
      modules/gdscript/gdscript_function.h
  13. 23 2
      modules/gdscript/gdscript_parser.cpp
  14. 3 0
      modules/gdscript/gdscript_parser.h
  15. 5 0
      modules/gdscript/gdscript_tokenizer.cpp
  16. 1 0
      modules/gdscript/gdscript_tokenizer.h
  17. 29 16
      modules/gdscript/gdscript_vm.cpp
  18. 15 2
      modules/gdscript/language_server/gdscript_extend_parser.cpp
  19. 22 0
      modules/gdscript/tests/scripts/analyzer/errors/variadic_functions.gd
  20. 5 0
      modules/gdscript/tests/scripts/analyzer/errors/variadic_functions.out
  21. 5 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_params_after_rest.gd
  22. 2 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_params_after_rest.out
  23. 5 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_after_rest.gd
  24. 2 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_after_rest.out
  25. 5 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_with_default.gd
  26. 2 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_with_default.out
  27. 5 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_static_init.gd
  28. 2 0
      modules/gdscript/tests/scripts/parser/errors/variadic_func_static_init.out
  29. 2 0
      modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd
  30. 38 0
      modules/gdscript/tests/scripts/runtime/features/variadic_functions.gd
  31. 12 0
      modules/gdscript/tests/scripts/runtime/features/variadic_functions.out
  32. 4 0
      modules/gdscript/tests/scripts/utils.notest.gd
  33. 4 0
      modules/gdscript/tests/test_gdscript.cpp

+ 2 - 0
core/doc_data.h

@@ -107,6 +107,8 @@ public:
 		bool is_experimental = false;
 		String experimental_message;
 		Vector<ArgumentDoc> arguments;
+		// NOTE: Only for GDScript for now. The rest argument is not saved to the XML file.
+		ArgumentDoc rest_argument;
 		Vector<int> errors_returned;
 		String keywords;
 		bool operator<(const MethodDoc &p_method) const {

+ 20 - 13
editor/connections_dialog.cpp

@@ -314,12 +314,15 @@ List<MethodInfo> ConnectDialog::_filter_method_list(const List<MethodInfo> &p_me
 		}
 
 		if (check_signal) {
-			if (mi.arguments.size() != effective_args.size()) {
+			const unsigned min_argc = mi.arguments.size() - mi.default_arguments.size();
+			const unsigned max_argc = (mi.flags & METHOD_FLAG_VARARG) ? UINT_MAX : mi.arguments.size();
+
+			if (effective_args.size() < min_argc || effective_args.size() > max_argc) {
 				continue;
 			}
 
 			bool type_mismatch = false;
-			for (int64_t i = 0; i < mi.arguments.size(); ++i) {
+			for (int64_t i = 0; i < effective_args.size() && i < mi.arguments.size(); ++i) {
 				Variant::Type stype = effective_args[i].first;
 				Variant::Type mtype = mi.arguments[i].type;
 
@@ -609,10 +612,14 @@ String ConnectDialog::get_signature(const MethodInfo &p_method, PackedStringArra
 		String arg_name = pi.name.is_empty() ? "arg" + itos(i) : pi.name;
 		signature.append(arg_name + ": " + type_name);
 		if (r_arg_names) {
-			r_arg_names->push_back(arg_name + ":" + type_name);
+			r_arg_names->push_back(arg_name + ": " + type_name);
 		}
 	}
 
+	if (p_method.flags & METHOD_FLAG_VARARG) {
+		signature.append(p_method.arguments.is_empty() ? "..." : ", ...");
+	}
+
 	signature.append(")");
 	return String().join(signature);
 }
@@ -1044,7 +1051,7 @@ void ConnectionsDock::_make_or_edit_connection() {
 		}
 
 		for (int i = 0; i < cd.binds.size(); i++) {
-			script_function_args.push_back("extra_arg_" + itos(i) + ":" + Variant::get_type_name(cd.binds[i].get_type()));
+			script_function_args.push_back("extra_arg_" + itos(i) + ": " + Variant::get_type_name(cd.binds[i].get_type()));
 		}
 
 		EditorNode::get_singleton()->emit_signal(SNAME("script_add_function_request"), target, cd.method, script_function_args);
@@ -1182,9 +1189,9 @@ bool ConnectionsDock::_is_connection_inherited(Connection &p_connection) {
  * Open connection dialog with TreeItem data to CREATE a brand-new connection.
  */
 void ConnectionsDock::_open_connection_dialog(TreeItem &p_item) {
-	Dictionary sinfo = p_item.get_metadata(0);
-	String signal_name = sinfo["name"];
-	PackedStringArray signal_args = sinfo["args"];
+	const Dictionary sinfo = p_item.get_metadata(0);
+	const StringName signal_name = sinfo["name"];
+	const PackedStringArray signal_args = sinfo["args"];
 
 	Node *dst_node = selected_node->get_owner() ? selected_node->get_owner() : selected_node;
 	if (!dst_node || dst_node->get_script().is_null()) {
@@ -1193,12 +1200,12 @@ void ConnectionsDock::_open_connection_dialog(TreeItem &p_item) {
 
 	ConnectDialog::ConnectionData cd;
 	cd.source = selected_node;
-	cd.signal = StringName(signal_name);
+	cd.signal = signal_name;
 	cd.target = dst_node;
 	cd.method = ConnectDialog::generate_method_callback_name(cd.source, signal_name, cd.target);
 	connect_dialog->init(cd, signal_args);
 	connect_dialog->set_title(TTR("Connect a Signal to a Method"));
-	connect_dialog->popup_dialog(signal_name + "(" + String(", ").join(signal_args) + ")");
+	connect_dialog->popup_dialog(signal_name.operator String() + "(" + String(", ").join(signal_args) + ")");
 }
 
 /*
@@ -1215,12 +1222,12 @@ void ConnectionsDock::_open_edit_connection_dialog(TreeItem &p_item) {
 	Node *dst = Object::cast_to<Node>(cd.target);
 
 	if (src && dst) {
-		const String &signal_name_ref = cd.signal;
-		PackedStringArray signal_args = signal_item->get_metadata(0).operator Dictionary()["args"];
+		const StringName &signal_name = cd.signal;
+		const PackedStringArray signal_args = signal_item->get_metadata(0).operator Dictionary()["args"];
 
-		connect_dialog->set_title(vformat(TTR("Edit Connection: '%s'"), cd.signal));
-		connect_dialog->popup_dialog(signal_name_ref);
 		connect_dialog->init(cd, signal_args, true);
+		connect_dialog->set_title(vformat(TTR("Edit Connection: '%s'"), cd.signal));
+		connect_dialog->popup_dialog(signal_name.operator String() + "(" + String(", ").join(signal_args) + ")");
 	}
 }
 

+ 86 - 16
editor/editor_help.cpp

@@ -582,14 +582,20 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview
 	for (int j = 0; j < p_method.arguments.size(); j++) {
 		const DocData::ArgumentDoc &argument = p_method.arguments[j];
 
-		class_desc->push_color(theme_cache.text_color);
-
 		if (j > 0) {
+			class_desc->push_color(theme_cache.symbol_color);
 			class_desc->add_text(", ");
+			class_desc->pop(); // color
 		}
 
+		class_desc->push_color(theme_cache.text_color);
 		class_desc->add_text(argument.name);
+		class_desc->pop(); // color
+
+		class_desc->push_color(theme_cache.symbol_color);
 		class_desc->add_text(colon_nbsp);
+		class_desc->pop(); // color
+
 		_add_type(argument.type, argument.enumeration, argument.is_bitfield);
 
 		if (!argument.default_value.is_empty()) {
@@ -601,13 +607,11 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview
 			class_desc->add_text(_fix_constant(argument.default_value));
 			class_desc->pop(); // color
 		}
-
-		class_desc->pop(); // color
 	}
 
 	if (is_vararg) {
 		if (!p_method.arguments.is_empty()) {
-			class_desc->push_color(theme_cache.text_color);
+			class_desc->push_color(theme_cache.symbol_color);
 			class_desc->add_text(", ");
 			class_desc->pop(); // color
 		}
@@ -615,6 +619,22 @@ void EditorHelp::_add_method(const DocData::MethodDoc &p_method, bool p_overview
 		class_desc->push_color(theme_cache.symbol_color);
 		class_desc->add_text("...");
 		class_desc->pop(); // color
+
+		const DocData::ArgumentDoc &rest_argument = p_method.rest_argument;
+
+		class_desc->push_color(theme_cache.text_color);
+		class_desc->add_text(rest_argument.name.is_empty() ? "args" : rest_argument.name);
+		class_desc->pop(); // color
+
+		class_desc->push_color(theme_cache.symbol_color);
+		class_desc->add_text(colon_nbsp);
+		class_desc->pop(); // color
+
+		if (rest_argument.type.is_empty()) {
+			_add_type("Array");
+		} else {
+			_add_type(rest_argument.type, rest_argument.enumeration, rest_argument.is_bitfield);
+		}
 	}
 
 	class_desc->push_color(theme_cache.symbol_color);
@@ -1558,14 +1578,20 @@ void EditorHelp::_update_doc() {
 			for (int j = 0; j < signal.arguments.size(); j++) {
 				const DocData::ArgumentDoc &argument = signal.arguments[j];
 
-				class_desc->push_color(theme_cache.text_color);
-
 				if (j > 0) {
+					class_desc->push_color(theme_cache.symbol_color);
 					class_desc->add_text(", ");
+					class_desc->pop(); // color
 				}
 
+				class_desc->push_color(theme_cache.text_color);
 				class_desc->add_text(argument.name);
+				class_desc->pop(); // color
+
+				class_desc->push_color(theme_cache.symbol_color);
 				class_desc->add_text(colon_nbsp);
+				class_desc->pop(); // color
+
 				_add_type(argument.type, argument.enumeration, argument.is_bitfield);
 
 				// Signals currently do not support default argument values, neither the core nor GDScript.
@@ -1579,8 +1605,6 @@ void EditorHelp::_update_doc() {
 					class_desc->add_text(_fix_constant(argument.default_value));
 					class_desc->pop(); // color
 				}
-
-				class_desc->pop(); // color
 			}
 
 			class_desc->push_color(theme_cache.symbol_color);
@@ -2002,14 +2026,20 @@ void EditorHelp::_update_doc() {
 				for (int j = 0; j < annotation.arguments.size(); j++) {
 					const DocData::ArgumentDoc &argument = annotation.arguments[j];
 
-					class_desc->push_color(theme_cache.text_color);
-
 					if (j > 0) {
+						class_desc->push_color(theme_cache.symbol_color);
 						class_desc->add_text(", ");
+						class_desc->pop(); // color
 					}
 
+					class_desc->push_color(theme_cache.text_color);
 					class_desc->add_text(argument.name);
+					class_desc->pop(); // color
+
+					class_desc->push_color(theme_cache.symbol_color);
 					class_desc->add_text(colon_nbsp);
+					class_desc->pop(); // color
+
 					_add_type(argument.type, argument.enumeration, argument.is_bitfield);
 
 					if (!argument.default_value.is_empty()) {
@@ -2021,13 +2051,11 @@ void EditorHelp::_update_doc() {
 						class_desc->add_text(_fix_constant(argument.default_value));
 						class_desc->pop(); // color
 					}
-
-					class_desc->pop(); // color
 				}
 
 				if (annotation.qualifiers.contains("vararg")) {
 					if (!annotation.arguments.is_empty()) {
-						class_desc->push_color(theme_cache.text_color);
+						class_desc->push_color(theme_cache.symbol_color);
 						class_desc->add_text(", ");
 						class_desc->pop(); // color
 					}
@@ -2035,6 +2063,22 @@ void EditorHelp::_update_doc() {
 					class_desc->push_color(theme_cache.symbol_color);
 					class_desc->add_text("...");
 					class_desc->pop(); // color
+
+					const DocData::ArgumentDoc &rest_argument = annotation.rest_argument;
+
+					class_desc->push_color(theme_cache.text_color);
+					class_desc->add_text(rest_argument.name.is_empty() ? "args" : rest_argument.name);
+					class_desc->pop(); // color
+
+					class_desc->push_color(theme_cache.symbol_color);
+					class_desc->add_text(colon_nbsp);
+					class_desc->pop(); // color
+
+					if (rest_argument.type.is_empty()) {
+						_add_type("Array");
+					} else {
+						_add_type(rest_argument.type, rest_argument.enumeration, rest_argument.is_bitfield);
+					}
 				}
 
 				class_desc->push_color(theme_cache.symbol_color);
@@ -3730,10 +3774,13 @@ EditorHelpBit::HelpData EditorHelpBit::_get_method_help_data(const StringName &p
 			}
 			current.doc_type = { method.return_type, method.return_enum, method.return_is_bitfield };
 			for (const DocData::ArgumentDoc &argument : method.arguments) {
-				const DocType argument_type = { argument.type, argument.enumeration, argument.is_bitfield };
-				current.arguments.push_back({ argument.name, argument_type, argument.default_value });
+				const DocType argument_doc_type = { argument.type, argument.enumeration, argument.is_bitfield };
+				current.arguments.push_back({ argument.name, argument_doc_type, argument.default_value });
 			}
 			current.qualifiers = method.qualifiers;
+			const DocData::ArgumentDoc &rest_argument = method.rest_argument;
+			const DocType rest_argument_doc_type = { rest_argument.type, rest_argument.enumeration, rest_argument.is_bitfield };
+			current.rest_argument = { rest_argument.name, rest_argument_doc_type, rest_argument.default_value };
 
 			if (method.name == p_method_name) {
 				result = current;
@@ -3895,6 +3942,7 @@ void EditorHelpBit::_update_labels() {
 
 		title->pop(); // font
 
+		const Color text_color = get_theme_color(SNAME("text_color"), SNAME("EditorHelp"));
 		const Color symbol_color = get_theme_color(SNAME("symbol_color"), SNAME("EditorHelp"));
 		const Color value_color = get_theme_color(SNAME("value_color"), SNAME("EditorHelp"));
 		const Color qualifier_color = get_theme_color(SNAME("qualifier_color"), SNAME("EditorHelp"));
@@ -3970,10 +4018,14 @@ void EditorHelpBit::_update_labels() {
 					const ArgumentData &argument = help_data.arguments[i];
 
 					if (i > 0) {
+						title->push_color(symbol_color);
 						title->add_text(", ");
+						title->pop(); // color
 					}
 
+					title->push_color(text_color);
 					title->add_text(argument.name);
+					title->pop(); // color
 
 					title->push_color(symbol_color);
 					title->add_text(colon_nbsp);
@@ -3994,12 +4046,30 @@ void EditorHelpBit::_update_labels() {
 
 				if (help_data.qualifiers.contains("vararg")) {
 					if (!help_data.arguments.is_empty()) {
+						title->push_color(symbol_color);
 						title->add_text(", ");
+						title->pop(); // color
 					}
 
 					title->push_color(symbol_color);
 					title->add_text("...");
 					title->pop(); // color
+
+					const ArgumentData &rest_argument = help_data.rest_argument;
+
+					title->push_color(text_color);
+					title->add_text(rest_argument.name.is_empty() ? "args" : rest_argument.name);
+					title->pop(); // color
+
+					title->push_color(symbol_color);
+					title->add_text(colon_nbsp);
+					title->pop(); // color
+
+					if (rest_argument.doc_type.type.is_empty()) {
+						_add_type_to_title({ "Array", "", false });
+					} else {
+						_add_type_to_title(rest_argument.doc_type);
+					}
 				}
 
 				title->push_color(symbol_color);

+ 1 - 0
editor/editor_help.h

@@ -302,6 +302,7 @@ class EditorHelpBit : public VBoxContainer {
 		DocType doc_type;
 		String value;
 		Vector<ArgumentData> arguments;
+		ArgumentData rest_argument;
 		String qualifiers;
 		String resource_path;
 	};

+ 8 - 0
editor/property_selector.cpp

@@ -287,8 +287,16 @@ void PropertySelector::_update_search() {
 				}
 			}
 
+			if (mi.flags & METHOD_FLAG_VARARG) {
+				desc += mi.arguments.is_empty() ? "..." : ", ...";
+			}
+
 			desc += ")";
 
+			if (mi.flags & METHOD_FLAG_VARARG) {
+				desc += " vararg";
+			}
+
 			if (mi.flags & METHOD_FLAG_CONST) {
 				desc += " const";
 			}

+ 18 - 6
modules/gdscript/editor/gdscript_docgen.cpp

@@ -408,13 +408,25 @@ void GDScriptDocGen::_generate_docs(GDScript *p_script, const GDP::ClassNode *p_
 				method_doc.is_experimental = m_func->doc_data.is_experimental;
 				method_doc.experimental_message = m_func->doc_data.experimental_message;
 
-				// Currently, an abstract function cannot be static.
+				if (m_func->is_vararg()) {
+					if (!method_doc.qualifiers.is_empty()) {
+						method_doc.qualifiers += " ";
+					}
+					method_doc.qualifiers += "vararg";
+					method_doc.rest_argument.name = m_func->rest_parameter->identifier->name;
+					_doctype_from_gdtype(m_func->rest_parameter->get_datatype(), method_doc.rest_argument.type, method_doc.rest_argument.enumeration);
+				}
 				if (m_func->is_abstract) {
-					method_doc.qualifiers = "abstract";
-				} else if (m_func->is_static) {
-					method_doc.qualifiers = "static";
-				} else {
-					method_doc.qualifiers = "";
+					if (!method_doc.qualifiers.is_empty()) {
+						method_doc.qualifiers += " ";
+					}
+					method_doc.qualifiers += "abstract";
+				}
+				if (m_func->is_static) {
+					if (!method_doc.qualifiers.is_empty()) {
+						method_doc.qualifiers += " ";
+					}
+					method_doc.qualifiers += "static";
 				}
 
 				if (func_name == "_init") {

+ 1 - 1
modules/gdscript/editor/gdscript_highlighter.cpp

@@ -492,7 +492,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
 				k--;
 			}
 
-			if (str[k] == '.') {
+			if (str[k] == '.' && (k < 1 || str[k - 1] != '.')) {
 				in_member_variable = true;
 			}
 		}

+ 48 - 4
modules/gdscript/gdscript_analyzer.cpp

@@ -1798,6 +1798,33 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 		}
 	}
 
+	if (p_function->is_vararg()) {
+		resolve_parameter(p_function->rest_parameter);
+		if (p_function->rest_parameter->datatype_specifier != nullptr) {
+			GDScriptParser::DataType specified_type = p_function->rest_parameter->get_datatype();
+			if (specified_type.kind != GDScriptParser::DataType::BUILTIN || specified_type.builtin_type != Variant::ARRAY) {
+				push_error(vformat(R"(The rest parameter type must be "Array", but "%s" is specified.)", specified_type.to_string()), p_function->rest_parameter->datatype_specifier);
+			} else if ((specified_type.has_container_element_type(0) && !specified_type.get_container_element_type(0).is_variant())) {
+				push_error(R"(Typed arrays are currently not supported for the rest parameter.)", p_function->rest_parameter->datatype_specifier);
+			}
+		} else {
+			GDScriptParser::DataType inferred_type;
+			inferred_type.type_source = GDScriptParser::DataType::INFERRED;
+			inferred_type.kind = GDScriptParser::DataType::BUILTIN;
+			inferred_type.builtin_type = Variant::ARRAY;
+			p_function->rest_parameter->set_datatype(inferred_type);
+#ifdef DEBUG_ENABLED
+			parser->push_warning(p_function->rest_parameter, GDScriptWarning::UNTYPED_DECLARATION, "Parameter", p_function->rest_parameter->identifier->name);
+#endif
+		}
+#ifdef DEBUG_ENABLED
+		if (p_function->rest_parameter->usages == 0 && !String(p_function->rest_parameter->identifier->name).begins_with("_") && !p_function->is_abstract) {
+			parser->push_warning(p_function->rest_parameter->identifier, GDScriptWarning::UNUSED_PARAMETER, function_visible_name, p_function->rest_parameter->identifier->name);
+		}
+		is_shadowing(p_function->rest_parameter->identifier, "function parameter", true);
+#endif // DEBUG_ENABLED
+	}
+
 	if (!p_is_lambda && function_name == GDScriptLanguage::get_singleton()->strings._init) {
 		// Constructor.
 		GDScriptParser::DataType return_type = parser->current_class->get_datatype();
@@ -1864,15 +1891,23 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 				}
 			}
 
-			int par_count_diff = p_function->parameters.size() - parameters_types.size();
-			valid = valid && par_count_diff >= 0;
-			valid = valid && default_value_count >= default_par_count + par_count_diff;
+			int parent_min_argc = parameters_types.size() - default_par_count;
+			int parent_max_argc = (method_flags & METHOD_FLAG_VARARG) ? INT_MAX : parameters_types.size();
+			int current_min_argc = p_function->parameters.size() - default_value_count;
+			int current_max_argc = p_function->is_vararg() ? INT_MAX : p_function->parameters.size();
+
+			// `[current_min_argc..current_max_argc]` must include `[parent_min_argc..parent_max_argc]`.
+			valid = valid && current_min_argc <= parent_min_argc && parent_max_argc <= current_max_argc;
 
 			if (valid) {
 				int i = 0;
 				for (const GDScriptParser::DataType &parent_par_type : parameters_types) {
+					if (i >= p_function->parameters.size()) {
+						break;
+					}
+					const GDScriptParser::DataType &current_par_type = p_function->parameters[i]->datatype;
+					i++;
 					// Check parameter type contravariance.
-					GDScriptParser::DataType current_par_type = p_function->parameters[i++]->get_datatype();
 					if (parent_par_type.is_variant() && parent_par_type.is_hard_type()) {
 						// `is_type_compatible()` returns `true` if one of the types is `Variant`.
 						// Don't allow narrowing a hard `Variant`.
@@ -1902,6 +1937,12 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode *
 
 					j++;
 				}
+				if (method_flags & METHOD_FLAG_VARARG) {
+					if (!parameters_types.is_empty()) {
+						parent_signature += ", ";
+					}
+					parent_signature += "...";
+				}
 				parent_signature += ") -> ";
 
 				const String return_type = parent_return_type.to_string_strict();
@@ -5791,6 +5832,9 @@ bool GDScriptAnalyzer::get_function_signature(GDScriptParser::Node *p_source, bo
 				r_default_arg_count++;
 			}
 		}
+		if (found_function->is_vararg()) {
+			r_method_flags.set_flag(METHOD_FLAG_VARARG);
+		}
 		r_return_type = p_is_constructor ? p_base_type : found_function->get_datatype();
 		r_return_type.is_meta_type = false;
 		r_return_type.is_coroutine = found_function->is_coroutine;

+ 10 - 0
modules/gdscript/gdscript_compiler.cpp

@@ -2338,6 +2338,7 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
 	codegen.generator->write_start(p_script, func_name, is_static, rpc_config, return_type);
 
 	int optional_parameters = 0;
+	GDScriptCodeGenerator::Address vararg_addr;
 
 	if (p_func) {
 		for (int i = 0; i < p_func->parameters.size(); i++) {
@@ -2353,6 +2354,11 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
 			}
 		}
 
+		if (p_func->is_vararg()) {
+			vararg_addr = codegen.add_local(p_func->rest_parameter->identifier->name, _gdtype_from_datatype(p_func->rest_parameter->get_datatype(), codegen.script));
+			method_info.flags |= METHOD_FLAG_VARARG;
+		}
+
 		method_info.default_arguments.append_array(p_func->default_arg_values);
 	}
 
@@ -2519,6 +2525,10 @@ GDScriptFunction *GDScriptCompiler::_parse_function(Error &r_error, GDScript *p_
 			gd_function->return_type.kind = GDScriptDataType::BUILTIN;
 			gd_function->return_type.builtin_type = Variant::NIL;
 		}
+
+		if (p_func->is_vararg()) {
+			gd_function->_vararg_index = vararg_addr.address;
+		}
 	}
 
 	gd_function->method_info = method_info;

+ 26 - 3
modules/gdscript/gdscript_editor.cpp

@@ -778,7 +778,7 @@ static String _make_arguments_hint(const MethodInfo &p_info, int p_arg_idx, bool
 		if (p_arg_idx >= p_info.arguments.size()) {
 			arghint += String::chr(0xFFFF);
 		}
-		arghint += "...";
+		arghint += "...args: Array"; // `MethodInfo` does not support the rest parameter name.
 		if (p_arg_idx >= p_info.arguments.size()) {
 			arghint += String::chr(0xFFFF);
 		}
@@ -796,9 +796,9 @@ static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_functio
 		arghint = "(";
 	} else {
 		if (p_function->get_datatype().builtin_type == Variant::NIL) {
-			arghint = "void " + p_function->identifier->name.operator String() + "(";
+			arghint = "void " + p_function->identifier->name + "(";
 		} else {
-			arghint = p_function->get_datatype().to_string() + " " + p_function->identifier->name.operator String() + "(";
+			arghint = p_function->get_datatype().to_string() + " " + p_function->identifier->name + "(";
 		}
 	}
 
@@ -870,6 +870,20 @@ static String _make_arguments_hint(const GDScriptParser::FunctionNode *p_functio
 		}
 	}
 
+	if (p_function->is_vararg()) {
+		if (!p_function->parameters.is_empty()) {
+			arghint += ", ";
+		}
+		if (p_arg_idx >= p_function->parameters.size()) {
+			arghint += String::chr(0xFFFF);
+		}
+		const GDScriptParser::ParameterNode *rest_param = p_function->rest_parameter;
+		arghint += "..." + rest_param->identifier->name + ": " + rest_param->get_datatype().to_string();
+		if (p_arg_idx >= p_function->parameters.size()) {
+			arghint += String::chr(0xFFFF);
+		}
+	}
+
 	arghint += ")";
 
 	return arghint;
@@ -3610,6 +3624,15 @@ static void _find_call_arguments(GDScriptParser::CompletionContext &p_context, c
 						method_hint += ": " + _get_visual_datatype(mi.arguments[i], true, class_name);
 					}
 				}
+				if (mi.flags & METHOD_FLAG_VARARG) {
+					if (!mi.arguments.is_empty()) {
+						method_hint += ", ";
+					}
+					method_hint += "...args"; // `MethodInfo` does not support the rest parameter name.
+					if (use_type_hint) {
+						method_hint += ": Array";
+					}
+				}
 				method_hint += ")";
 				if (use_type_hint) {
 					method_hint += " -> " + _get_visual_datatype(mi.return_val, false, class_name);

+ 3 - 2
modules/gdscript/gdscript_function.cpp

@@ -248,8 +248,9 @@ Variant GDScriptFunctionState::resume(const Variant &p_arg) {
 void GDScriptFunctionState::_clear_stack() {
 	if (state.stack_size) {
 		Variant *stack = (Variant *)state.stack.ptr();
-		// The first 3 are special addresses and not copied to the state, so we skip them here.
-		for (int i = 3; i < state.stack_size; i++) {
+		// First `GDScriptFunction::FIXED_ADDRESSES_MAX` stack addresses are special
+		// and not copied to the state, so we skip them here.
+		for (int i = GDScriptFunction::FIXED_ADDRESSES_MAX; i < state.stack_size; i++) {
 			stack[i].~Variant();
 		}
 		state.stack_size = 0;

+ 2 - 0
modules/gdscript/gdscript_function.h

@@ -458,6 +458,7 @@ private:
 	GDScript *_script = nullptr;
 	int _initial_line = 0;
 	int _argument_count = 0;
+	int _vararg_index = -1;
 	int _stack_size = 0;
 	int _instruction_args_size = 0;
 
@@ -579,6 +580,7 @@ public:
 	_FORCE_INLINE_ StringName get_source() const { return source; }
 	_FORCE_INLINE_ GDScript *get_script() const { return _script; }
 	_FORCE_INLINE_ bool is_static() const { return _static; }
+	_FORCE_INLINE_ bool is_vararg() const { return _vararg_index >= 0; }
 	_FORCE_INLINE_ MethodInfo get_method_info() const { return method_info; }
 	_FORCE_INLINE_ int get_argument_count() const { return _argument_count; }
 	_FORCE_INLINE_ Variant get_rpc_config() const { return rpc_config; }

+ 23 - 2
modules/gdscript/gdscript_parser.cpp

@@ -1632,20 +1632,40 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
 				// Allow for trailing comma.
 				break;
 			}
+
+			bool is_rest = false;
+			if (match(GDScriptTokenizer::Token::PERIOD_PERIOD_PERIOD)) {
+				is_rest = true;
+			}
+
 			ParameterNode *parameter = parse_parameter();
 			if (parameter == nullptr) {
 				break;
 			}
+
+			if (p_function->is_vararg()) {
+				push_error("Cannot have parameters after the rest parameter.");
+				continue;
+			}
+
 			if (parameter->initializer != nullptr) {
+				if (is_rest) {
+					push_error("The rest parameter cannot have a default value.");
+					continue;
+				}
 				default_used = true;
 			} else {
-				if (default_used) {
+				if (default_used && !is_rest) {
 					push_error("Cannot have mandatory parameters after optional parameters.");
 					continue;
 				}
 			}
+
 			if (p_function->parameters_indices.has(parameter->identifier->name)) {
 				push_error(vformat(R"(Parameter with name "%s" was already declared for this %s.)", parameter->identifier->name, p_type));
+			} else if (is_rest) {
+				p_function->rest_parameter = parameter;
+				p_body->add_local(parameter, current_function);
 			} else {
 				p_function->parameters_indices[parameter->identifier->name] = p_function->parameters.size();
 				p_function->parameters.push_back(parameter);
@@ -1669,7 +1689,7 @@ void GDScriptParser::parse_function_signature(FunctionNode *p_function, SuiteNod
 		if (!p_function->is_static) {
 			push_error(R"(Static constructor must be declared static.)");
 		}
-		if (p_function->parameters.size() != 0) {
+		if (!p_function->parameters.is_empty() || p_function->is_vararg()) {
 			push_error(R"(Static constructor cannot have parameters.)");
 		}
 		current_class->has_static_data = true;
@@ -4233,6 +4253,7 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // SEMICOLON,
 		{ nullptr,                                          &GDScriptParser::parse_attribute,            	PREC_ATTRIBUTE }, // PERIOD,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // PERIOD_PERIOD,
+		{ nullptr,                                          nullptr,                                        PREC_NONE }, // PERIOD_PERIOD_PERIOD,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // COLON,
 		{ &GDScriptParser::parse_get_node,               	nullptr,                                        PREC_NONE }, // DOLLAR,
 		{ nullptr,                                          nullptr,                                        PREC_NONE }, // FORWARD_ARROW,

+ 3 - 0
modules/gdscript/gdscript_parser.h

@@ -851,6 +851,7 @@ public:
 		IdentifierNode *identifier = nullptr;
 		Vector<ParameterNode *> parameters;
 		HashMap<StringName, int> parameters_indices;
+		ParameterNode *rest_parameter = nullptr;
 		TypeNode *return_type = nullptr;
 		SuiteNode *body = nullptr;
 		bool is_abstract = false;
@@ -869,6 +870,8 @@ public:
 		bool resolved_signature = false;
 		bool resolved_body = false;
 
+		_FORCE_INLINE_ bool is_vararg() const { return rest_parameter != nullptr; }
+
 		FunctionNode() {
 			type = FUNCTION;
 		}

+ 5 - 0
modules/gdscript/gdscript_tokenizer.cpp

@@ -135,6 +135,7 @@ static const char *token_names[] = {
 	";", // SEMICOLON,
 	".", // PERIOD,
 	"..", // PERIOD_PERIOD,
+	"...", // PERIOD_PERIOD_PERIOD,
 	":", // COLON,
 	"$", // DOLLAR,
 	"->", // FORWARD_ARROW,
@@ -1501,6 +1502,10 @@ GDScriptTokenizer::Token GDScriptTokenizerText::scan() {
 		case '.':
 			if (_peek() == '.') {
 				_advance();
+				if (_peek() == '.') {
+					_advance();
+					return make_token(Token::PERIOD_PERIOD_PERIOD);
+				}
 				return make_token(Token::PERIOD_PERIOD);
 			} else if (is_digit(_peek())) {
 				// Number starting with '.'.

+ 1 - 0
modules/gdscript/gdscript_tokenizer.h

@@ -139,6 +139,7 @@ public:
 			SEMICOLON,
 			PERIOD,
 			PERIOD_PERIOD,
+			PERIOD_PERIOD_PERIOD,
 			COLON,
 			DOLLAR,
 			FORWARD_ARROW,

+ 29 - 16
modules/gdscript/gdscript_vm.cpp

@@ -554,10 +554,12 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
 	} else {
 		if (p_argcount != _argument_count) {
 			if (p_argcount > _argument_count) {
-				r_err.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS;
-				r_err.expected = _argument_count;
-				call_depth--;
-				return _get_default_variant_for_data_type(return_type);
+				if (!is_vararg()) {
+					r_err.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS;
+					r_err.expected = _argument_count;
+					call_depth--;
+					return _get_default_variant_for_data_type(return_type);
+				}
 			} else if (p_argcount < _argument_count - _default_arg_count) {
 				r_err.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS;
 				r_err.expected = _argument_count - _default_arg_count;
@@ -568,21 +570,21 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
 			}
 		}
 
-		// Add 3 here for self, class, and nil.
-		alloca_size = sizeof(Variant *) * 3 + sizeof(Variant *) * _instruction_args_size + sizeof(Variant) * _stack_size;
+		alloca_size = sizeof(Variant *) * FIXED_ADDRESSES_MAX + sizeof(Variant *) * _instruction_args_size + sizeof(Variant) * _stack_size;
 
 		uint8_t *aptr = (uint8_t *)alloca(alloca_size);
 		stack = (Variant *)aptr;
 
-		for (int i = 0; i < p_argcount; i++) {
+		const int non_vararg_arg_count = MIN(p_argcount, _argument_count);
+		for (int i = 0; i < non_vararg_arg_count; i++) {
 			if (!argument_types[i].has_type) {
-				memnew_placement(&stack[i + 3], Variant(*p_args[i]));
+				memnew_placement(&stack[i + FIXED_ADDRESSES_MAX], Variant(*p_args[i]));
 				continue;
 			}
 			// If types already match, don't call Variant::construct(). Constructors of some types
 			// (e.g. packed arrays) do copies, whereas they pass by reference when inside a Variant.
 			if (argument_types[i].is_type(*p_args[i], false)) {
-				memnew_placement(&stack[i + 3], Variant(*p_args[i]));
+				memnew_placement(&stack[i + FIXED_ADDRESSES_MAX], Variant(*p_args[i]));
 				continue;
 			}
 			if (!argument_types[i].is_type(*p_args[i], true)) {
@@ -597,11 +599,11 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
 					const GDScriptDataType &arg_key_type = argument_types[i].get_container_element_type_or_variant(0);
 					const GDScriptDataType &arg_value_type = argument_types[i].get_container_element_type_or_variant(1);
 					Dictionary dict(p_args[i]->operator Dictionary(), arg_key_type.builtin_type, arg_key_type.native_type, arg_key_type.script_type, arg_value_type.builtin_type, arg_value_type.native_type, arg_value_type.script_type);
-					memnew_placement(&stack[i + 3], Variant(dict));
+					memnew_placement(&stack[i + FIXED_ADDRESSES_MAX], Variant(dict));
 				} else if (argument_types[i].builtin_type == Variant::ARRAY && argument_types[i].has_container_element_type(0)) {
 					const GDScriptDataType &arg_type = argument_types[i].container_element_types[0];
 					Array array(p_args[i]->operator Array(), arg_type.builtin_type, arg_type.native_type, arg_type.script_type);
-					memnew_placement(&stack[i + 3], Variant(array));
+					memnew_placement(&stack[i + FIXED_ADDRESSES_MAX], Variant(array));
 				} else {
 					Variant variant;
 					Variant::construct(argument_types[i].builtin_type, variant, &p_args[i], 1, r_err);
@@ -612,16 +614,27 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
 						call_depth--;
 						return _get_default_variant_for_data_type(return_type);
 					}
-					memnew_placement(&stack[i + 3], Variant(variant));
+					memnew_placement(&stack[i + FIXED_ADDRESSES_MAX], Variant(variant));
 				}
 			} else {
-				memnew_placement(&stack[i + 3], Variant(*p_args[i]));
+				memnew_placement(&stack[i + FIXED_ADDRESSES_MAX], Variant(*p_args[i]));
 			}
 		}
-		for (int i = p_argcount + 3; i < _stack_size; i++) {
+		for (int i = non_vararg_arg_count + FIXED_ADDRESSES_MAX; i < _stack_size; i++) {
 			memnew_placement(&stack[i], Variant);
 		}
 
+		if (is_vararg()) {
+			Array vararg;
+			stack[_vararg_index] = vararg;
+			if (p_argcount > _argument_count) {
+				vararg.resize(p_argcount - _argument_count);
+				for (int i = 0; i < p_argcount - _argument_count; i++) {
+					vararg[i] = *p_args[i + _argument_count];
+				}
+			}
+		}
+
 		if (_instruction_args_size) {
 			instruction_args = (Variant **)&aptr[sizeof(Variant) * _stack_size];
 		} else {
@@ -2559,8 +2572,8 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a
 
 					gdfs->state.stack.resize(alloca_size);
 
-					// First 3 stack addresses are special, so we just skip them here.
-					for (int i = 3; i < _stack_size; i++) {
+					// First `FIXED_ADDRESSES_MAX` stack addresses are special, so we just skip them here.
+					for (int i = FIXED_ADDRESSES_MAX; i < _stack_size; i++) {
 						memnew_placement(&gdfs->state.stack.write[sizeof(Variant) * i], Variant(stack[i]));
 					}
 					gdfs->state.stack_size = _stack_size;

+ 15 - 2
modules/gdscript/language_server/gdscript_extend_parser.cpp

@@ -508,9 +508,22 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN
 			parameters += " = " + parameter->initializer->reduced_value.to_json_string();
 		}
 	}
+	if (p_func->is_vararg()) {
+		if (!p_func->parameters.is_empty()) {
+			parameters += ", ";
+		}
+		const ParameterNode *rest_param = p_func->rest_parameter;
+		parameters += "..." + rest_param->identifier->name + ": " + rest_param->get_datatype().to_string();
+	}
 	r_symbol.detail += parameters + ")";
-	if (p_func->get_datatype().is_hard_type()) {
-		r_symbol.detail += " -> " + p_func->get_datatype().to_string();
+
+	const DataType return_type = p_func->get_datatype();
+	if (return_type.is_hard_type()) {
+		if (return_type.kind == DataType::BUILTIN && return_type.builtin_type == Variant::NIL) {
+			r_symbol.detail += " -> void";
+		} else {
+			r_symbol.detail += " -> " + return_type.to_string();
+		}
 	}
 
 	List<GDScriptParser::SuiteNode *> function_nodes;

+ 22 - 0
modules/gdscript/tests/scripts/analyzer/errors/variadic_functions.gd

@@ -0,0 +1,22 @@
+class A:
+	func f1(x: int, ...args: Array) -> void:
+		prints(x, args)
+
+	func f2(x: int, ...args: Array) -> void:
+		prints(x, args)
+
+class B extends A:
+	func f1(x: int, y: int, ...args: Array) -> void:
+		prints(x, y, args)
+
+	func f2(x: int) -> void:
+		print(x)
+
+func g(...args: int):
+	pass
+
+func h(...args: Array[int]):
+	pass
+
+func test():
+	pass

+ 5 - 0
modules/gdscript/tests/scripts/analyzer/errors/variadic_functions.out

@@ -0,0 +1,5 @@
+GDTEST_ANALYZER_ERROR
+>> ERROR at line 15: The rest parameter type must be "Array", but "int" is specified.
+>> ERROR at line 18: Typed arrays are currently not supported for the rest parameter.
+>> ERROR at line 9: The function signature doesn't match the parent. Parent signature is "f1(int, ...) -> void".
+>> ERROR at line 12: The function signature doesn't match the parent. Parent signature is "f2(int, ...) -> void".

+ 5 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_params_after_rest.gd

@@ -0,0 +1,5 @@
+func f(...args, extra_arg):
+	pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_params_after_rest.out

@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Cannot have parameters after the rest parameter.

+ 5 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_after_rest.gd

@@ -0,0 +1,5 @@
+func f(...args, ...more_args):
+	pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_after_rest.out

@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Cannot have parameters after the rest parameter.

+ 5 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_with_default.gd

@@ -0,0 +1,5 @@
+func f(...args = []):
+	pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_rest_with_default.out

@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+The rest parameter cannot have a default value.

+ 5 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_static_init.gd

@@ -0,0 +1,5 @@
+static func _static_init(...args) -> void:
+	pass
+
+func test():
+	pass

+ 2 - 0
modules/gdscript/tests/scripts/parser/errors/variadic_func_static_init.out

@@ -0,0 +1,2 @@
+GDTEST_PARSER_ERROR
+Static constructor cannot have parameters.

+ 2 - 0
modules/gdscript/tests/scripts/runtime/features/abstract_methods.gd

@@ -4,6 +4,7 @@ abstract class A:
 
 	# No `UNUSED_PARAMETER` warning.
 	abstract func func_with_param(param: int) -> int
+	abstract func func_with_rest_param(...args: Array) -> int
 	abstract func func_with_semicolon() -> int;
 	abstract func func_1() -> int; abstract func func_2() -> int
 	abstract func func_without_return_type()
@@ -23,6 +24,7 @@ class C extends B:
 		return "text_2c"
 
 	func func_with_param(param: int) -> int: return param
+	func func_with_rest_param(...args: Array) -> int: return args.size()
 	func func_with_semicolon() -> int: return 0
 	func func_1() -> int: return 0
 	func func_2() -> int: return 0

+ 38 - 0
modules/gdscript/tests/scripts/runtime/features/variadic_functions.gd

@@ -0,0 +1,38 @@
+class A:
+	func f(x: int) -> void:
+		print(x)
+
+class B extends A:
+	func f(x: int, ...args: Array) -> void:
+		prints(x, args)
+
+class C extends B:
+	func f(x: int, y: int = 0, ...args: Array) -> void:
+		prints(x, y, args)
+
+class D extends C:
+	func f(...args: Array) -> void:
+		print(args)
+
+func test_func(x: int, y: int = 0, ...args: Array) -> void:
+	prints(x, y, args)
+
+var test_lambda := func (x: int, y: int = 0, ...args: Array) -> void:
+	prints(x, y, args)
+
+func test():
+	for method in get_method_list():
+		if str(method.name).begins_with("test_"):
+			print(Utils.get_method_signature(method))
+
+	test_func(1)
+	test_func(1, 2)
+	test_func(1, 2, 3)
+	test_func(1, 2, 3, 4)
+	test_func(1, 2, 3, 4, 5)
+
+	test_lambda.call(1)
+	test_lambda.call(1, 2)
+	test_lambda.call(1, 2, 3)
+	test_lambda.call(1, 2, 3, 4)
+	test_lambda.call(1, 2, 3, 4, 5)

+ 12 - 0
modules/gdscript/tests/scripts/runtime/features/variadic_functions.out

@@ -0,0 +1,12 @@
+GDTEST_OK
+func test_func(x: int, y: int = 0, ...args) -> void
+1 0 []
+1 2 []
+1 2 [3]
+1 2 [3, 4]
+1 2 [3, 4, 5]
+1 0 []
+1 2 []
+1 2 [3]
+1 2 [3, 4]
+1 2 [3, 4, 5]

+ 4 - 0
modules/gdscript/tests/scripts/utils.notest.gd

@@ -117,6 +117,10 @@ static func get_method_signature(method: Dictionary, is_signal: bool = false) ->
 		if i >= mandatory_argc:
 			result += " = " + var_to_str(default_args[i - mandatory_argc])
 
+	if method.flags & METHOD_FLAG_VARARG:
+		# `MethodInfo` does not support the rest parameter name.
+		result += "...args" if args.is_empty() else ", ...args"
+
 	result += ")"
 	if is_signal:
 		if get_type(method.return, true) != "void":

+ 4 - 0
modules/gdscript/tests/test_gdscript.cpp

@@ -207,6 +207,10 @@ static void disassemble_function(const GDScriptFunction *p_func, const Vector<St
 		arg_string += arg_info.name;
 		is_first_arg = false;
 	}
+	if (p_func->is_vararg()) {
+		// `MethodInfo` does not support the rest parameter name.
+		arg_string += (p_func->get_argument_count() == 0) ? "...args" : ", ...args";
+	}
 
 	print_line(vformat("Function %s(%s)", p_func->get_name(), arg_string));
 #ifdef TOOLS_ENABLED