From 5d6e8538065050d5f5579ec03cfa9e241811e062 Mon Sep 17 00:00:00 2001 From: George Marques Date: Fri, 1 May 2020 19:14:56 -0300 Subject: New GDScript tokenizer and parser Sometimes to fix something you have to break it first. This get GDScript mostly working with the new tokenizer and parser but a lot of things isn't working yet. It compiles and it's usable, and that should be enough for now. Don't worry: other huge commits will come after this. --- modules/gdscript/gdscript_parser.cpp | 10989 ++++++++------------------------- 1 file changed, 2574 insertions(+), 8415 deletions(-) (limited to 'modules/gdscript/gdscript_parser.cpp') diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 63da849723..2d82cd2d5e 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -30,8870 +30,3029 @@ #include "gdscript_parser.h" -#include "core/core_string_names.h" -#include "core/engine.h" #include "core/io/resource_loader.h" +#include "core/math/math_defs.h" #include "core/os/file_access.h" -#include "core/print_string.h" -#include "core/project_settings.h" -#include "core/reference.h" -#include "core/script_language.h" -#include "gdscript.h" + +#ifdef DEBUG_ENABLED +#include "core/os/os.h" +#include "core/string_builder.h" +#endif // DEBUG_ENABLED + +static HashMap builtin_types; +Variant::Type GDScriptParser::get_builtin_type(const StringName &p_type) { + if (builtin_types.empty()) { + builtin_types["bool"] = Variant::BOOL; + builtin_types["int"] = Variant::INT; + builtin_types["float"] = Variant::FLOAT; + builtin_types["String"] = Variant::STRING; + builtin_types["Vector2"] = Variant::VECTOR2; + builtin_types["Vector2i"] = Variant::VECTOR2I; + builtin_types["Rect2"] = Variant::RECT2; + builtin_types["Rect2i"] = Variant::RECT2I; + builtin_types["Transform2D"] = Variant::TRANSFORM2D; + builtin_types["Vector3"] = Variant::VECTOR3; + builtin_types["Vector3i"] = Variant::VECTOR3I; + builtin_types["AABB"] = Variant::AABB; + builtin_types["Plane"] = Variant::PLANE; + builtin_types["Quat"] = Variant::QUAT; + builtin_types["Basis"] = Variant::BASIS; + builtin_types["Transform"] = Variant::TRANSFORM; + builtin_types["Color"] = Variant::COLOR; + builtin_types["RID"] = Variant::_RID; + builtin_types["Object"] = Variant::OBJECT; + builtin_types["StringName"] = Variant::STRING_NAME; + builtin_types["NodePath"] = Variant::NODE_PATH; + builtin_types["Dictionary"] = Variant::DICTIONARY; + builtin_types["Callable"] = Variant::CALLABLE; + builtin_types["Signal"] = Variant::SIGNAL; + builtin_types["Array"] = Variant::ARRAY; + builtin_types["PackedByteArray"] = Variant::PACKED_BYTE_ARRAY; + builtin_types["PackedInt32Array"] = Variant::PACKED_INT32_ARRAY; + builtin_types["PackedInt64Array"] = Variant::PACKED_INT64_ARRAY; + builtin_types["PackedFloat32Array"] = Variant::PACKED_FLOAT32_ARRAY; + builtin_types["PackedFloat64Array"] = Variant::PACKED_FLOAT64_ARRAY; + builtin_types["PackedStringArray"] = Variant::PACKED_STRING_ARRAY; + builtin_types["PackedVector2Array"] = Variant::PACKED_VECTOR2_ARRAY; + builtin_types["PackedVector3Array"] = Variant::PACKED_VECTOR3_ARRAY; + builtin_types["PackedColorArray"] = Variant::PACKED_COLOR_ARRAY; + // NIL is not here, hence the -1. + if (builtin_types.size() != Variant::VARIANT_MAX - 1) { + ERR_PRINT("Outdated parser: amount of built-in types don't match the amount of types in Variant."); + } + } + + if (builtin_types.has(p_type)) { + return builtin_types[p_type]; + } + return Variant::VARIANT_MAX; +} + +GDScriptFunctions::Function GDScriptParser::get_builtin_function(const StringName &p_name) { + for (int i = 0; i < GDScriptFunctions::FUNC_MAX; i++) { + if (p_name == GDScriptFunctions::get_func_name(GDScriptFunctions::Function(i))) { + return GDScriptFunctions::Function(i); + } + } + return GDScriptFunctions::FUNC_MAX; +} + +GDScriptParser::GDScriptParser() { + // Register valid annotations. + // TODO: Should this be static? + // TODO: Validate applicable types (e.g. a VARIABLE annotation that only applies to string variables). + register_annotation(MethodInfo("@tool"), AnnotationInfo::SCRIPT, &GDScriptParser::tool_annotation); + register_annotation(MethodInfo("@icon", { Variant::STRING, "icon_path" }), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation); + register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation); + // Export annotations. + register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_enum", { Variant::STRING, "names" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 0, true); + register_annotation(MethodInfo("@export_file", { Variant::STRING, "filter" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 1, true); + register_annotation(MethodInfo("@export_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_global_file", { Variant::STRING, "filter" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 1, true); + register_annotation(MethodInfo("@export_global_dir"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_multiline"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_placeholder"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_range", { Variant::FLOAT, "min" }, { Variant::FLOAT, "max" }, { Variant::FLOAT, "step" }, { Variant::STRING, "slider1" }, { Variant::STRING, "slider2" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 3); + register_annotation(MethodInfo("@export_exp_range", { Variant::FLOAT, "min" }, { Variant::FLOAT, "max" }, { Variant::FLOAT, "step" }, { Variant::STRING, "slider1" }, { Variant::STRING, "slider2" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 3); + register_annotation(MethodInfo("@export_exp_easing", { Variant::STRING, "hint1" }, { Variant::STRING, "hint2" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 2); + register_annotation(MethodInfo("@export_color_no_alpha"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_node_path", { Variant::STRING, "type" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 1, true); + register_annotation(MethodInfo("@export_flags", { Variant::STRING, "names" }), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations, 0, true); + register_annotation(MethodInfo("@export_flags_2d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_flags_2d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_flags_3d_render"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + register_annotation(MethodInfo("@export_flags_3d_physics"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations); + // Networking. + register_annotation(MethodInfo("@remote"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations); + register_annotation(MethodInfo("@master"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations); + register_annotation(MethodInfo("@puppet"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations); + register_annotation(MethodInfo("@remotesync"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations); + register_annotation(MethodInfo("@mastersync"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations); + register_annotation(MethodInfo("@puppetsync"), AnnotationInfo::VARIABLE | AnnotationInfo::FUNCTION, &GDScriptParser::network_annotations); + // TODO: Warning annotations. +} + +GDScriptParser::~GDScriptParser() { + clear(); +} template T *GDScriptParser::alloc_node() { - T *t = memnew(T); + T *node = memnew(T); - t->next = list; - list = t; + node->next = list; + list = node; - if (!head) { - head = t; + // TODO: Properly set positions for all nodes. + node->start_line = previous.start_line; + node->end_line = previous.end_line; + node->leftmost_column = previous.leftmost_column; + node->rightmost_column = previous.rightmost_column; + + return node; +} + +void GDScriptParser::clear() { + while (list != nullptr) { + Node *element = list; + list = list->next; + memdelete(element); } - t->line = tokenizer->get_token_line(); - t->column = tokenizer->get_token_column(); - return t; + head = nullptr; + list = nullptr; + _is_tool = false; + for_completion = false; + errors.clear(); + multiline_stack.clear(); } -#ifdef DEBUG_ENABLED -static String _find_function_name(const GDScriptParser::OperatorNode *p_call); -#endif // DEBUG_ENABLED +void GDScriptParser::push_error(const String &p_message, const Node *p_origin) { + // TODO: Improve error reporting by pointing at source code. + // TODO: Errors might point at more than one place at once (e.g. show previous declaration). + panic_mode = true; + // TODO: Improve positional information. + if (p_origin == nullptr) { + errors.push_back({ p_message, current.start_line, current.start_column }); + } else { + errors.push_back({ p_message, p_origin->start_line, p_origin->leftmost_column }); + } +} + +Error GDScriptParser::parse(const String &p_source_code, const String &p_script_path, bool p_for_completion) { + clear(); + tokenizer.set_source_code(p_source_code); + current = tokenizer.scan(); + + push_multiline(false); // Keep one for the whole parsing. + parse_program(); + pop_multiline(); -bool GDScriptParser::_end_statement() { - if (tokenizer->get_token() == GDScriptTokenizer::TK_SEMICOLON) { - tokenizer->advance(); - return true; //handle next - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE || tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - return true; //will be handled properly +#ifdef DEBUG_ENABLED + if (multiline_stack.size() > 0) { + ERR_PRINT("Parser bug: Imbalanced multiline stack."); } +#endif - return false; + if (errors.empty()) { + return OK; + } else { + return ERR_PARSE_ERROR; + } } -void GDScriptParser::_set_end_statement_error(String p_name) { - String error_msg; - if (tokenizer->get_token() == GDScriptTokenizer::TK_IDENTIFIER) { - error_msg = vformat("Expected end of statement (\"%s\"), got %s (\"%s\") instead.", p_name, tokenizer->get_token_name(tokenizer->get_token()), tokenizer->get_token_identifier()); - } else { - error_msg = vformat("Expected end of statement (\"%s\"), got %s instead.", p_name, tokenizer->get_token_name(tokenizer->get_token())); +GDScriptTokenizer::Token GDScriptParser::advance() { + if (current.type == GDScriptTokenizer::Token::TK_EOF) { + ERR_FAIL_COND_V_MSG(current.type == GDScriptTokenizer::Token::TK_EOF, current, "GDScript parser bug: Trying to advance past the end of stream."); } - _set_error(error_msg); + previous = current; + current = tokenizer.scan(); + while (current.type == GDScriptTokenizer::Token::ERROR) { + push_error(current.literal); + current = tokenizer.scan(); + } + return previous; } -bool GDScriptParser::_enter_indent_block(BlockNode *p_block) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_COLON) { - // report location at the previous token (on the previous line) - int error_line = tokenizer->get_token_line(-1); - int error_column = tokenizer->get_token_column(-1); - _set_error("':' expected at end of line.", error_line, error_column); +bool GDScriptParser::match(GDScriptTokenizer::Token::Type p_token_type) { + if (!check(p_token_type)) { return false; } - tokenizer->advance(); + advance(); + return true; +} - if (tokenizer->get_token() == GDScriptTokenizer::TK_EOF) { - return false; +bool GDScriptParser::check(GDScriptTokenizer::Token::Type p_token_type) { + if (p_token_type == GDScriptTokenizer::Token::IDENTIFIER) { + return current.is_identifier(); } + return current.type == p_token_type; +} - if (tokenizer->get_token() != GDScriptTokenizer::TK_NEWLINE) { - // be more python-like - IndentLevel current_level = indent_level.back()->get(); - indent_level.push_back(current_level); +bool GDScriptParser::consume(GDScriptTokenizer::Token::Type p_token_type, const String &p_error_message) { + if (match(p_token_type)) { return true; - //_set_error("newline expected after ':'."); - //return false; - } - - while (true) { - if (tokenizer->get_token() != GDScriptTokenizer::TK_NEWLINE) { - return false; //wtf - } else if (tokenizer->get_token(1) == GDScriptTokenizer::TK_EOF) { - return false; - } else if (tokenizer->get_token(1) != GDScriptTokenizer::TK_NEWLINE) { - int indent = tokenizer->get_token_line_indent(); - int tabs = tokenizer->get_token_line_tab_indent(); - IndentLevel current_level = indent_level.back()->get(); - IndentLevel new_indent(indent, tabs); - if (new_indent.is_mixed(current_level)) { - _set_error("Mixed tabs and spaces in indentation."); - return false; - } + } + push_error(p_error_message); + return false; +} - if (indent <= current_level.indent) { - return false; - } +bool GDScriptParser::is_at_end() { + return check(GDScriptTokenizer::Token::TK_EOF); +} - indent_level.push_back(new_indent); - tokenizer->advance(); - return true; +void GDScriptParser::synchronize() { + panic_mode = false; + while (!is_at_end()) { + if (previous.type == GDScriptTokenizer::Token::NEWLINE || previous.type == GDScriptTokenizer::Token::SEMICOLON) { + return; + } - } else if (p_block) { - NewLineNode *nl = alloc_node(); - nl->line = tokenizer->get_token_line(); - p_block->statements.push_back(nl); + switch (current.type) { + case GDScriptTokenizer::Token::CLASS: + case GDScriptTokenizer::Token::FUNC: + case GDScriptTokenizer::Token::STATIC: + case GDScriptTokenizer::Token::VAR: + case GDScriptTokenizer::Token::CONST: + case GDScriptTokenizer::Token::SIGNAL: + //case GDScriptTokenizer::Token::IF: // Can also be inside expressions. + case GDScriptTokenizer::Token::FOR: + case GDScriptTokenizer::Token::WHILE: + case GDScriptTokenizer::Token::MATCH: + case GDScriptTokenizer::Token::RETURN: + case GDScriptTokenizer::Token::ANNOTATION: + return; + default: + // Do nothing. + break; } - tokenizer->advance(); // go to next newline + advance(); } } -bool GDScriptParser::_parse_arguments(Node *p_parent, Vector &p_args, bool p_static, bool p_can_codecomplete, bool p_parsing_constant) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - tokenizer->advance(); - } else { - parenthesis++; - int argidx = 0; - - while (true) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - _make_completable_call(argidx); - completion_node = p_parent; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT && tokenizer->get_token_constant().get_type() == Variant::STRING && tokenizer->get_token(1) == GDScriptTokenizer::TK_CURSOR) { - //completing a string argument.. - completion_cursor = tokenizer->get_token_constant(); - - _make_completable_call(argidx); - completion_node = p_parent; - tokenizer->advance(1); - return false; - } +void GDScriptParser::push_multiline(bool p_state) { + multiline_stack.push_back(p_state); + tokenizer.set_multiline_mode(p_state); + if (p_state) { + // Consume potential whitespace tokens already waiting in line. + while (current.type == GDScriptTokenizer::Token::NEWLINE || current.type == GDScriptTokenizer::Token::INDENT || current.type == GDScriptTokenizer::Token::DEDENT) { + current = tokenizer.scan(); // Don't call advance() here, as we don't want to change the previous token. + } + } +} - Node *arg = _parse_expression(p_parent, p_static, false, p_parsing_constant); - if (!arg) { - return false; - } +void GDScriptParser::pop_multiline() { + ERR_FAIL_COND_MSG(multiline_stack.size() == 0, "Parser bug: trying to pop from multiline stack without available value."); + multiline_stack.pop_back(); + tokenizer.set_multiline_mode(multiline_stack.size() > 0 ? multiline_stack.back()->get() : false); +} + +bool GDScriptParser::is_statement_end() { + return check(GDScriptTokenizer::Token::NEWLINE) || check(GDScriptTokenizer::Token::SEMICOLON); +} - p_args.push_back(arg); +void GDScriptParser::end_statement(const String &p_context) { + bool found = false; + while (is_statement_end()) { + // Remove sequential newlines/semicolons. + found = true; + advance(); + } + if (!found) { + push_error(vformat(R"(Expected end of statement after %s, found "%s" instead.)", p_context, current.get_name())); + } +} - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - tokenizer->advance(); - break; +void GDScriptParser::parse_program() { + if (current.type == GDScriptTokenizer::Token::TK_EOF) { + // Empty file. + push_error("Source file is empty."); + return; + } - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_COMMA) { - if (tokenizer->get_token(1) == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expression expected"); - return false; - } + head = alloc_node(); + current_class = head; - tokenizer->advance(); - argidx++; - } else { - // something is broken - _set_error("Expected ',' or ')'"); - return false; + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + // Check for @tool annotation. + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL); + if (annotation->name == "@tool") { + // TODO: don't allow @tool anywhere else. (Should all script annotations be the first thing?). + _is_tool = true; + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after "@tool" annotation.)"); } + // @tool annotation has no specific target. + annotation->apply(this, nullptr); + } else { + annotation_stack.push_back(annotation); } - parenthesis--; } - return true; -} - -void GDScriptParser::_make_completable_call(int p_arg) { - completion_cursor = StringName(); - completion_type = COMPLETION_CALL_ARGUMENTS; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_argument = p_arg; - completion_block = current_block; - completion_found = true; - tokenizer->advance(); -} + for (bool should_break = false; !should_break;) { + // Order here doesn't matter, but there should be only one of each at most. + switch (current.type) { + case GDScriptTokenizer::Token::CLASS_NAME: + if (!annotation_stack.empty()) { + push_error(R"("class_name" should be used before annotations.)"); + } + advance(); + if (head->identifier != nullptr) { + push_error(R"("class_name" can only be used once.)"); + } else { + parse_class_name(); + } + break; + case GDScriptTokenizer::Token::EXTENDS: + if (!annotation_stack.empty()) { + push_error(R"("extends" should be used before annotations.)"); + } + advance(); + if (head->extends_used) { + push_error(R"("extends" can only be used once.)"); + } else { + parse_extends(); + } + break; + default: + should_break = true; + break; + } -bool GDScriptParser::_get_completable_identifier(CompletionType p_type, StringName &identifier) { - identifier = StringName(); - if (tokenizer->is_token_literal()) { - identifier = tokenizer->get_token_literal(); - tokenizer->advance(); - } - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = identifier; - completion_type = p_type; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_block = current_block; - completion_found = true; - completion_ident_is_call = false; - tokenizer->advance(); - - if (tokenizer->is_token_literal()) { - identifier = identifier.operator String() + tokenizer->get_token_literal().operator String(); - tokenizer->advance(); + if (panic_mode) { + synchronize(); } + } - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - completion_ident_is_call = true; + if (match(GDScriptTokenizer::Token::ANNOTATION)) { + // Check for @icon annotation. + AnnotationNode *annotation = parse_annotation(AnnotationInfo::SCRIPT | AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + if (annotation->name == "@icon") { + if (previous.type != GDScriptTokenizer::Token::NEWLINE) { + push_error(R"(Expected newline after "@icon" annotation.)"); + } + annotation->apply(this, head); + } else { + annotation_stack.push_back(annotation); + } } - return true; } - return false; + parse_class_body(); + + if (!check(GDScriptTokenizer::Token::TK_EOF)) { + push_error("Expected end of file."); + } + + clear_unused_annotations(); } -GDScriptParser::Node *GDScriptParser::_parse_expression(Node *p_parent, bool p_static, bool p_allow_assign, bool p_parsing_constant) { - //Vector expressions; - //Vector operators; +GDScriptParser::ClassNode *GDScriptParser::parse_class() { + ClassNode *n_class = alloc_node(); - Vector expression; + ClassNode *previous_class = current_class; + current_class = n_class; + n_class->outer = previous_class; - Node *expr = nullptr; + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the class name after "class".)")) { + n_class->identifier = parse_identifier(); + } - int op_line = tokenizer->get_token_line(); // when operators are created at the bottom, the line might have been changed (\n found) + if (check(GDScriptTokenizer::Token::EXTENDS)) { + parse_extends(); + } - while (true) { - /*****************/ - /* Parse Operand */ - /*****************/ + consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after class declaration.)"); + consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected newline after class declaration.)"); - if (parenthesis > 0) { - //remove empty space (only allowed if inside parenthesis - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - } - } + if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected indented block after class declaration.)")) { + current_class = previous_class; + return n_class; + } - // Check that the next token is not TK_CURSOR and if it is, the offset should be incremented. - int next_valid_offset = 1; - if (tokenizer->get_token(next_valid_offset) == GDScriptTokenizer::TK_CURSOR) { - next_valid_offset++; - // There is a chunk of the identifier that also needs to be ignored (not always there!) - if (tokenizer->get_token(next_valid_offset) == GDScriptTokenizer::TK_IDENTIFIER) { - next_valid_offset++; - } - } + parse_class_body(); - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - //subexpression () - tokenizer->advance(); - parenthesis++; - Node *subexpr = _parse_expression(p_parent, p_static, p_allow_assign, p_parsing_constant); - parenthesis--; - if (!subexpr) { - return nullptr; - } + consume(GDScriptTokenizer::Token::DEDENT, R"(Missing unindent at the end of the class body.)"); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected ')' in expression"); - return nullptr; - } + current_class = previous_class; + return n_class; +} - tokenizer->advance(); - expr = subexpr; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_DOLLAR) { - tokenizer->advance(); - - String path; - - bool need_identifier = true; - bool done = false; - int line = tokenizer->get_token_line(); - - while (!done) { - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_CURSOR: { - completion_type = COMPLETION_GET_NODE; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_cursor = path; - completion_argument = 0; - completion_block = current_block; - completion_found = true; - tokenizer->advance(); - } break; - case GDScriptTokenizer::TK_CONSTANT: { - if (!need_identifier) { - done = true; - break; - } +void GDScriptParser::parse_class_name() { + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifier for the global class name after "class_name".)")) { + current_class->identifier = parse_identifier(); + } - if (tokenizer->get_token_constant().get_type() != Variant::STRING) { - _set_error("Expected string constant or identifier after '$' or '/'."); - return nullptr; - } + // TODO: Move this to annotation + if (match(GDScriptTokenizer::Token::COMMA)) { + // Icon path. + if (consume(GDScriptTokenizer::Token::LITERAL, R"(Expected class icon path string after ",".)")) { + if (previous.literal.get_type() != Variant::STRING) { + push_error(vformat(R"(Only strings can be used for the class icon path, found "%s" instead.)", Variant::get_type_name(previous.literal.get_type()))); + } + current_class->icon_path = previous.literal; + } + } - path += String(tokenizer->get_token_constant()); - tokenizer->advance(); - need_identifier = false; + if (match(GDScriptTokenizer::Token::EXTENDS)) { + // Allow extends on the same line. + parse_extends(); + } else { + end_statement("class_name statement"); + } +} - } break; - case GDScriptTokenizer::TK_OP_DIV: { - if (need_identifier) { - done = true; - break; - } +void GDScriptParser::parse_extends() { + current_class->extends_used = true; - path += "/"; - tokenizer->advance(); - need_identifier = true; - - } break; - default: { - // Instead of checking for TK_IDENTIFIER, we check with is_token_literal, as this allows us to use match/sync/etc. as a name - if (need_identifier && tokenizer->is_token_literal()) { - path += String(tokenizer->get_token_literal()); - tokenizer->advance(); - need_identifier = false; - } else { - done = true; - } + if (match(GDScriptTokenizer::Token::LITERAL)) { + if (previous.literal.get_type() != Variant::STRING) { + push_error(vformat(R"(Only strings or identifiers can be used after "extends", found "%s" instead.)", Variant::get_type_name(previous.literal.get_type()))); + } + current_class->extends_path = previous.literal; - break; - } - } - } + if (!match(GDScriptTokenizer::Token::PERIOD)) { + end_statement("superclass path"); + return; + } + } - if (path == "") { - _set_error("Path expected after $."); - return nullptr; - } + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected superclass name after "extends".)")) { + return; + } + current_class->extends.push_back(previous.literal); - OperatorNode *op = alloc_node(); - op->op = OperatorNode::OP_CALL; - op->line = line; - op->arguments.push_back(alloc_node()); - op->arguments[0]->line = line; - - IdentifierNode *funcname = alloc_node(); - funcname->name = "get_node"; - funcname->line = line; - op->arguments.push_back(funcname); - - ConstantNode *nodepath = alloc_node(); - nodepath->value = NodePath(StringName(path)); - nodepath->datatype = _type_from_variant(nodepath->value); - nodepath->line = line; - op->arguments.push_back(nodepath); - - expr = op; - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - tokenizer->advance(); - continue; //no point in cursor in the middle of expression - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONSTANT) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node(); - constant->value = tokenizer->get_token_constant(); - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_PI) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node(); - constant->value = Math_PI; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_TAU) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node(); - constant->value = Math_TAU; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_INF) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node(); - constant->value = Math_INF; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_CONST_NAN) { - //constant defined by tokenizer - ConstantNode *constant = alloc_node(); - constant->value = Math_NAN; - constant->datatype = _type_from_variant(constant->value); - tokenizer->advance(); - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_PRELOAD) { - //constant defined by tokenizer - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected '(' after 'preload'"); - return nullptr; - } - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = StringName(); - completion_node = p_parent; - completion_type = COMPLETION_RESOURCE_PATH; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_block = current_block; - completion_argument = 0; - completion_found = true; - tokenizer->advance(); - } + while (match(GDScriptTokenizer::Token::PERIOD)) { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected superclass name after ".".)")) { + return; + } + current_class->extends.push_back(previous.literal); + } - String path; - bool found_constant = false; - bool valid = false; - ConstantNode *cn; + end_statement("superclass"); +} - Node *subexpr = _parse_and_reduce_expression(p_parent, p_static); - if (subexpr) { - if (subexpr->type == Node::TYPE_CONSTANT) { - cn = static_cast(subexpr); - found_constant = true; - } - if (subexpr->type == Node::TYPE_IDENTIFIER) { - IdentifierNode *in = static_cast(subexpr); - - // Try to find the constant expression by the identifier - if (current_class->constant_expressions.has(in->name)) { - Node *cn_exp = current_class->constant_expressions[in->name].expression; - if (cn_exp->type == Node::TYPE_CONSTANT) { - cn = static_cast(cn_exp); - found_constant = true; - } - } - } +template +void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(), AnnotationInfo::TargetKind p_target, const String &p_member_kind) { + advance(); + T *member = (this->*p_parse_function)(); + if (member == nullptr) { + return; + } + // Consume annotations. + while (!annotation_stack.empty()) { + AnnotationNode *last_annotation = annotation_stack.back()->get(); + if (last_annotation->applies_to(p_target)) { + last_annotation->apply(this, member); + member->annotations.push_front(last_annotation); + annotation_stack.pop_back(); + } else { + push_error(vformat(R"(Annotation "%s" cannot be applied to a %s.)", last_annotation->name, p_member_kind)); + clear_unused_annotations(); + return; + } + } + if (member->identifier != nullptr) { + // Enums may be unnamed. + // TODO: Consider names in outer scope too, for constants and classes (and static functions?) + if (current_class->members_indices.has(member->identifier->name)) { + push_error(vformat(R"(%s "%s" has the same name as a previously declared %s.)", p_member_kind.capitalize(), member->identifier->name, current_class->get_member(member->identifier->name).get_type_name())); + } else { + current_class->add_member(member); + } + } +} - if (found_constant && cn->value.get_type() == Variant::STRING) { - valid = true; - path = (String)cn->value; +void GDScriptParser::parse_class_body() { + bool class_end = false; + while (!class_end && !is_at_end()) { + switch (current.type) { + case GDScriptTokenizer::Token::VAR: + parse_class_member(&GDScriptParser::parse_variable, AnnotationInfo::VARIABLE, "variable"); + break; + case GDScriptTokenizer::Token::CONST: + parse_class_member(&GDScriptParser::parse_constant, AnnotationInfo::CONSTANT, "constant"); + break; + 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"); + break; + case GDScriptTokenizer::Token::CLASS: + parse_class_member(&GDScriptParser::parse_class, AnnotationInfo::CLASS, "class"); + break; + case GDScriptTokenizer::Token::ENUM: + parse_class_member(&GDScriptParser::parse_enum, AnnotationInfo::NONE, "enum"); + break; + case GDScriptTokenizer::Token::ANNOTATION: { + advance(); + AnnotationNode *annotation = parse_annotation(AnnotationInfo::CLASS_LEVEL); + if (annotation != nullptr) { + annotation_stack.push_back(annotation); } + break; } + case GDScriptTokenizer::Token::PASS: + end_statement(R"("pass")"); + break; + case GDScriptTokenizer::Token::DEDENT: + class_end = true; + break; + default: + push_error(vformat(R"(Unexpected "%s" in class body.)", current.get_name())); + advance(); + break; + } + if (panic_mode) { + synchronize(); + } + } +} - if (!valid) { - _set_error("expected string constant as 'preload' argument."); - return nullptr; - } +GDScriptParser::VariableNode *GDScriptParser::parse_variable() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected variable name after "var".)")) { + return nullptr; + } - if (!path.is_abs_path() && base_path != "") { - path = base_path.plus_file(path); - } - path = path.replace("///", "//").simplify_path(); - if (path == self_path) { - _set_error("Can't preload itself (use 'get_script()')."); - return nullptr; - } + VariableNode *variable = alloc_node(); + variable->identifier = parse_identifier(); - Ref res; - dependencies.push_back(path); - if (!dependencies_only) { - if (!validating) { - //this can be too slow for just validating code - if (for_completion && ScriptCodeCompletionCache::get_singleton() && FileAccess::exists(path)) { - res = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(path); - } else if (!for_completion || FileAccess::exists(path)) { - res = ResourceLoader::load(path); - } - } else { - if (!FileAccess::exists(path)) { - _set_error("Can't preload resource at path: " + path); - return nullptr; - } else if (ScriptCodeCompletionCache::get_singleton()) { - res = ScriptCodeCompletionCache::get_singleton()->get_cached_resource(path); - } - } - if (!res.is_valid()) { - _set_error("Can't preload resource at path: " + path); - return nullptr; - } - } + if (match(GDScriptTokenizer::Token::COLON)) { + if (check((GDScriptTokenizer::Token::EQUAL))) { + // Infer type. + variable->infer_datatype = true; + } else { + // Parse type. + variable->datatype_specifier = parse_type(); + } + } - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected ')' after 'preload' path"); - return nullptr; - } + if (match(GDScriptTokenizer::Token::EQUAL)) { + // Initializer. + variable->initializer = parse_expression(false); + } - Ref gds = res; - if (gds.is_valid() && !gds->is_valid()) { - _set_error("Couldn't fully preload the script, possible cyclic reference or compilation error. Use \"load()\" instead if a cyclic reference is intended."); - return nullptr; - } + end_statement("variable declaration"); - tokenizer->advance(); + variable->export_info.name = variable->identifier->name; - ConstantNode *constant = alloc_node(); - constant->value = res; - constant->datatype = _type_from_variant(constant->value); + return variable; +} - expr = constant; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_PR_YIELD) { - if (!current_function) { - _set_error("\"yield()\" can only be used inside function blocks."); - return nullptr; - } +GDScriptParser::ConstantNode *GDScriptParser::parse_constant() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected constant name after "const".)")) { + return nullptr; + } - current_function->has_yield = true; + ConstantNode *constant = alloc_node(); + constant->identifier = parse_identifier(); - tokenizer->advance(); - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_OPEN) { - _set_error("Expected \"(\" after \"yield\"."); - return nullptr; - } + if (match(GDScriptTokenizer::Token::COLON)) { + if (check((GDScriptTokenizer::Token::EQUAL))) { + // Infer type. + constant->infer_datatype = true; + } else { + // Parse type. + constant->datatype_specifier = parse_type(); + } + } - tokenizer->advance(); + if (consume(GDScriptTokenizer::Token::EQUAL, R"(Expected initializer after constant name.)")) { + // Initializer. + constant->initializer = parse_expression(false); - OperatorNode *yield = alloc_node(); - yield->op = OperatorNode::OP_YIELD; + if (constant->initializer == nullptr) { + push_error(R"(Expected initializer expression for constant.)"); + return nullptr; + } + } - while (tokenizer->get_token() == GDScriptTokenizer::TK_NEWLINE) { - tokenizer->advance(); - } + end_statement("constant declaration"); - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - expr = yield; - tokenizer->advance(); - } else { - parenthesis++; + return constant; +} - Node *object = _parse_and_reduce_expression(p_parent, p_static); - if (!object) { - return nullptr; - } - yield->arguments.push_back(object); +GDScriptParser::ParameterNode *GDScriptParser::parse_parameter() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected parameter name.)")) { + return nullptr; + } - if (tokenizer->get_token() != GDScriptTokenizer::TK_COMMA) { - _set_error("Expected \",\" after the first argument of \"yield\"."); - return nullptr; - } + ParameterNode *parameter = alloc_node(); + parameter->identifier = parse_identifier(); - tokenizer->advance(); - - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - completion_cursor = StringName(); - completion_node = object; - completion_type = COMPLETION_YIELD; - completion_class = current_class; - completion_function = current_function; - completion_line = tokenizer->get_token_line(); - completion_argument = 0; - completion_block = current_block; - completion_found = true; - tokenizer->advance(); - } + if (match(GDScriptTokenizer::Token::COLON)) { + if (check((GDScriptTokenizer::Token::EQUAL))) { + // Infer type. + parameter->infer_datatype = true; + } else { + // Parse type. + parameter->datatype_specifier = parse_type(); + } + } - Node *signal = _parse_and_reduce_expression(p_parent, p_static); - if (!signal) { - return nullptr; - } - yield->arguments.push_back(signal); + if (match(GDScriptTokenizer::Token::EQUAL)) { + // Default value. + parameter->default_value = parse_expression(false); + } - if (tokenizer->get_token() != GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - _set_error("Expected \")\" after the second argument of \"yield\"."); - return nullptr; - } + return parameter; +} - parenthesis--; +GDScriptParser::SignalNode *GDScriptParser::parse_signal() { + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected signal name after "signal".)")) { + return nullptr; + } - tokenizer->advance(); + SignalNode *signal = alloc_node(); + signal->identifier = parse_identifier(); - expr = yield; + if (match(GDScriptTokenizer::Token::PARENTHESIS_OPEN)) { + while (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { + ParameterNode *parameter = parse_parameter(); + if (parameter == nullptr) { + break; } - - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_SELF) { - if (p_static) { - _set_error("\"self\" isn't allowed in a static function or constant expression."); - return nullptr; + if (parameter->default_value != nullptr) { + push_error(R"(Signal parameters cannot have a default value.)"); } - //constant defined by tokenizer - SelfNode *self = alloc_node(); - tokenizer->advance(); - expr = self; - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE && tokenizer->get_token(1) == GDScriptTokenizer::TK_PERIOD) { - Variant::Type bi_type = tokenizer->get_token_type(); - tokenizer->advance(2); - - StringName identifier; - - if (_get_completable_identifier(COMPLETION_BUILT_IN_TYPE_CONSTANT, identifier)) { - completion_built_in_constant = bi_type; + if (signal->parameters_indices.has(parameter->identifier->name)) { + push_error(vformat(R"(Parameter with name "%s" was already declared for this signal.)", parameter->identifier->name)); + } else { + signal->parameters_indices[parameter->identifier->name] = signal->parameters.size(); + signal->parameters.push_back(parameter); } + } + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after signal parameters.)*"); + } - if (identifier == StringName()) { - _set_error("Built-in type constant or static function expected after \".\"."); - return nullptr; - } - if (!Variant::has_constant(bi_type, identifier)) { - if (tokenizer->get_token() == GDScriptTokenizer::TK_PARENTHESIS_OPEN && - Variant::is_method_const(bi_type, identifier) && - Variant::get_method_return_type(bi_type, identifier) == bi_type) { - tokenizer->advance(); + end_statement("signal declaration"); - OperatorNode *construct = alloc_node(); - construct->op = OperatorNode::OP_CALL; + return signal; +} - TypeNode *tn = alloc_node(); - tn->vtype = bi_type; - construct->arguments.push_back(tn); +GDScriptParser::EnumNode *GDScriptParser::parse_enum() { + EnumNode *enum_node = alloc_node(); + bool named = false; - OperatorNode *op = alloc_node(); - op->op = OperatorNode::OP_CALL; - op->arguments.push_back(construct); + if (check(GDScriptTokenizer::Token::IDENTIFIER)) { + advance(); + enum_node->identifier = parse_identifier(); + named = true; + } - IdentifierNode *id = alloc_node(); - id->name = identifier; - op->arguments.push_back(id); + push_multiline(true); + consume(GDScriptTokenizer::Token::BRACE_OPEN, vformat(R"(Expected "{" after %s.)", named ? "enum name" : R"("enum")")); - if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { - return nullptr; - } + int current_value = 0; - expr = op; - } else { - // Object is a special case - bool valid = false; - if (bi_type == Variant::OBJECT) { - int object_constant = ClassDB::get_integer_constant("Object", identifier, &valid); - if (valid) { - ConstantNode *cn = alloc_node(); - cn->value = object_constant; - cn->datatype = _type_from_variant(cn->value); - expr = cn; - } - } - if (!valid) { - _set_error("Static constant '" + identifier.operator String() + "' not present in built-in type " + Variant::get_type_name(bi_type) + "."); - return nullptr; + do { + if (check(GDScriptTokenizer::Token::BRACE_CLOSE)) { + break; // Allow trailing comma. + } + if (consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected identifer for enum key.)")) { + EnumNode::Value item; + item.identifier = parse_identifier(); + bool found = false; + + if (!named) { + // TODO: Abstract this recursive member check. + ClassNode *parent = current_class; + while (parent != nullptr) { + if (parent->members_indices.has(item.identifier->name)) { + push_error(vformat(R"(Name "%s" is already used as a class %s.)", item.identifier->name, parent->get_member(item.identifier->name).get_type_name())); + found = true; + break; } + parent = parent->outer; } - } else { - ConstantNode *cn = alloc_node(); - cn->value = Variant::get_constant_value(bi_type, identifier); - cn->datatype = _type_from_variant(cn->value); - expr = cn; } - } else if (tokenizer->get_token(next_valid_offset) == GDScriptTokenizer::TK_PARENTHESIS_OPEN && tokenizer->is_token_literal()) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - //function or constructor - - OperatorNode *op = alloc_node(); - op->op = OperatorNode::OP_CALL; - - //Do a quick Array and Dictionary Check. Replace if either require no arguments. - bool replaced = false; - - if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_TYPE) { - Variant::Type ct = tokenizer->get_token_type(); - if (!p_parsing_constant) { - if (ct == Variant::ARRAY) { - if (tokenizer->get_token(2) == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - ArrayNode *arr = alloc_node(); - expr = arr; - replaced = true; - tokenizer->advance(3); - } - } - if (ct == Variant::DICTIONARY) { - if (tokenizer->get_token(2) == GDScriptTokenizer::TK_PARENTHESIS_CLOSE) { - DictionaryNode *dict = alloc_node(); - expr = dict; - replaced = true; - tokenizer->advance(3); - } + if (match(GDScriptTokenizer::Token::EQUAL)) { + if (consume(GDScriptTokenizer::Token::LITERAL, R"(Expected integer value after "=".)")) { + item.custom_value = parse_literal(); + + if (item.custom_value->value.get_type() != Variant::INT) { + push_error(R"(Expected integer value after "=".)"); + item.custom_value = nullptr; + } else { + current_value = item.custom_value->value; } } + } + item.value = current_value++; + enum_node->values.push_back(item); + if (!found) { + // Add as member of current class. + current_class->add_member(item); + } + } + } while (match(GDScriptTokenizer::Token::COMMA)); - if (!replaced) { - TypeNode *tn = alloc_node(); - tn->vtype = tokenizer->get_token_type(); - op->arguments.push_back(tn); - tokenizer->advance(2); - } - } else if (tokenizer->get_token() == GDScriptTokenizer::TK_BUILT_IN_FUNC) { - BuiltInFunctionNode *bn = alloc_node(); - bn->function = tokenizer->get_token_built_in_func(); - op->arguments.push_back(bn); - tokenizer->advance(2); - } else { - SelfNode *self = alloc_node(); - op->arguments.push_back(self); + pop_multiline(); + consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected closing "}" for enum.)"); - StringName identifier; - if (_get_completable_identifier(COMPLETION_FUNCTION, identifier)) { - } + end_statement("enum"); - IdentifierNode *id = alloc_node(); - id->name = identifier; - op->arguments.push_back(id); - tokenizer->advance(1); - } + return enum_node; +} - if (tokenizer->get_token() == GDScriptTokenizer::TK_CURSOR) { - _make_completable_call(0); - completion_node = op; +GDScriptParser::FunctionNode *GDScriptParser::parse_function() { + 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".)")) { + return nullptr; + } + _static = true; + } - if (op->arguments[0]->type == GDScriptParser::Node::Type::TYPE_BUILT_IN_FUNCTION) { - BuiltInFunctionNode *bn = static_cast(op->arguments[0]); - if (bn->function == GDScriptFunctions::Function::RESOURCE_LOAD) { - completion_type = COMPLETION_RESOURCE_PATH; - } - } + if (!consume(GDScriptTokenizer::Token::IDENTIFIER, R"(Expected function name after "func".)")) { + return nullptr; + } + + FunctionNode *function = alloc_node(); + FunctionNode *previous_function = current_function; + current_function = function; + + function->identifier = parse_identifier(); + function->is_static = _static; + + push_multiline(true); + consume(GDScriptTokenizer::Token::PARENTHESIS_OPEN, R"(Expected opening "(" after function name.)"); + + if (!check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE) && !is_at_end()) { + bool default_used = false; + do { + if (check(GDScriptTokenizer::Token::PARENTHESIS_CLOSE)) { + // Allow for trailing comma. + break; + } + ParameterNode *parameter = parse_parameter(); + if (parameter == nullptr) { + break; } - if (!replaced) { - if (!_parse_arguments(op, op->arguments, p_static, true, p_parsing_constant)) { - return nullptr; + if (parameter->default_value != nullptr) { + default_used = true; + } else { + if (default_used) { + push_error("Cannot have a mandatory parameters after optional parameters."); + continue; } - expr = op; } - } else if (tokenizer->is_token_literal(0, true)) { - // We check with is_token_literal, as this allows us to use match/sync/etc. as a name - //identifier (reference) - - const ClassNode *cln = current_class; - bool bfn = false; - StringName identifier; - int id_line = tokenizer->get_token_line(); - if (_get_completable_identifier(COMPLETION_IDENTIFIER, identifier)) { + if (function->parameters_indices.has(parameter->identifier->name)) { + push_error(vformat(R"(Parameter with name "%s" was already declared for this function.)", parameter->identifier->name)); + } else { + function->parameters_indices[parameter->identifier->name] = function->parameters.size(); + function->parameters.push_back(parameter); } + } while (match(GDScriptTokenizer::Token::COMMA)); + } - BlockNode *b = current_block; - while (!bfn && b) { - if (b->variables.has(identifier)) { - IdentifierNode *id = alloc_node(); - id->name = identifier; - id->declared_block = b; - id->line = id_line; - expr = id; - bfn = true; + pop_multiline(); + consume(GDScriptTokenizer::Token::PARENTHESIS_CLOSE, R"*(Expected closing ")" after function parameters.)*"); -#ifdef DEBUG_ENABLED - LocalVarNode *lv = b->variables[identifier]; - switch (tokenizer->get_token()) { - case GDScriptTokenizer::TK_OP_ASSIGN_ADD: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_AND: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_OR: - case GDScriptTokenizer::TK_OP_ASSIGN_BIT_XOR: - case GDScriptTokenizer::TK_OP_ASSIGN_DIV: - case GDScriptTokenizer::TK_OP_ASSIGN_MOD: - case GDScriptTokenizer::TK_OP_ASSIGN_MUL: - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_LEFT: - case GDScriptTokenizer::TK_OP_ASSIGN_SHIFT_RIGHT: - case GDScriptTokenizer::TK_OP_ASSIGN_SUB: { - if (lv->assignments == 0) { - if (!lv->datatype.has_type) { - _set_error("Using assignment with operation on a variable that was never assigned."); - return nullptr; - } - _add_warning(GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, -1, identifier.operator String()); - } - [[fallthrough]]; - } - case GDScriptTokenizer::TK_OP_ASSIGN: { - lv->assignments += 1; - lv->usages--; // Assignment is not really usage - } break; - default: { - lv->usages++; - } - } -#endif // DEBUG_ENABLED - break; - } - b = b->parent_block; - } + if (match(GDScriptTokenizer::Token::FORWARD_ARROW)) { + function->return_type = parse_type(true); + } - if (!bfn && p_parsing_constant) { - if (cln->constant_expressions.has(identifier)) { - expr = cln->constant_expressions[identifier].expression; - bfn = true; - } else if (GDScriptLanguage::get_singleton()->get_global_map().has(identifier)) { - //check from constants - ConstantNode *constant = alloc_node(); - constant->value = GDScriptLanguage::get_singleton()->get_global_array()[GDScriptLanguage::get_singleton()->get_global_map()[identifier]]; - constant->datatype = _type_from_variant(constant->value); - constant->line = id_line; - expr = constant; - bfn = 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, R"(Expected ":" after function declaration.)"); - if (!bfn && GDScriptLanguage::get_singleton()->get_named_globals_map().has(identifier)) { - //check from singletons - ConstantNode *constant = alloc_node(); - constant->value = GDScriptLanguage::get_singleton()->get_named_globals_map()[identifier]; - constant->datatype = _type_from_variant(constant->value); - expr = constant; - bfn = true; - } + function->body = parse_suite("function declaration"); - if (!dependencies_only) { - if (!bfn && ScriptServer::is_global_class(identifier)) { - Ref