From f7b7eb1f25284283fed50f3c2c2da45e46d1f4ae Mon Sep 17 00:00:00 2001 From: Danil Alexeev Date: Mon, 26 Jun 2023 17:06:20 +0300 Subject: [PATCH] GDScript: Optimize `match` --- doc/classes/ProjectSettings.xml | 2 + modules/gdscript/gdscript_analyzer.cpp | 17 +- modules/gdscript/gdscript_byte_codegen.cpp | 85 +++ modules/gdscript/gdscript_byte_codegen.h | 37 ++ modules/gdscript/gdscript_codegen.h | 6 + modules/gdscript/gdscript_compiler.cpp | 667 ++++++--------------- modules/gdscript/gdscript_compiler.h | 13 + modules/gdscript/gdscript_disassembler.cpp | 49 ++ modules/gdscript/gdscript_function.cpp | 5 + modules/gdscript/gdscript_function.h | 22 + modules/gdscript/gdscript_parser.cpp | 31 +- modules/gdscript/gdscript_vm.cpp | 40 ++ modules/gdscript/gdscript_warning.cpp | 3 + modules/gdscript/gdscript_warning.h | 2 + 14 files changed, 494 insertions(+), 485 deletions(-) diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 87c642c6d30c..ee5b945132cf 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -496,6 +496,8 @@ When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a function that is not a coroutine is called with await. + + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when the [code]@static_unload[/code] annotation is used in a script without any static variables. diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 077b30a7fed2..91517d07ef27 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -2266,7 +2266,9 @@ void GDScriptAnalyzer::resolve_match_pattern(GDScriptParser::PatternNode *p_matc resolve_match_pattern(p_match_pattern->array[i], nullptr); decide_suite_type(p_match_pattern, p_match_pattern->array[i]); } - result = p_match_pattern->get_datatype(); + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::ARRAY; break; case GDScriptParser::PatternNode::PT_DICTIONARY: for (int i = 0; i < p_match_pattern->dictionary.size(); i++) { @@ -2282,7 +2284,9 @@ void GDScriptAnalyzer::resolve_match_pattern(GDScriptParser::PatternNode *p_matc decide_suite_type(p_match_pattern, p_match_pattern->dictionary[i].value_pattern); } } - result = p_match_pattern->get_datatype(); + result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT; + result.kind = GDScriptParser::DataType::BUILTIN; + result.builtin_type = Variant::DICTIONARY; break; case GDScriptParser::PatternNode::PT_WILDCARD: case GDScriptParser::PatternNode::PT_REST: @@ -2290,6 +2294,15 @@ void GDScriptAnalyzer::resolve_match_pattern(GDScriptParser::PatternNode *p_matc break; } + if (p_match_test != nullptr) { + GDScriptParser::DataType test_type = p_match_test->get_datatype(); + if (!result.is_variant() && result.is_hard_type() && !test_type.is_variant() && test_type.is_hard_type() && !is_type_compatible(result, test_type) && !is_type_compatible(test_type, result)) { + if (!(result.builtin_type == Variant::STRING && test_type.builtin_type == Variant::STRING_NAME) && !(result.builtin_type == Variant::STRING_NAME && test_type.builtin_type == Variant::STRING)) { + push_error(vformat(R"(Expression of type "%s" cannot match a pattern of type "%s".)", test_type.to_string(), result.to_string()), p_match_pattern); + } + } + } + p_match_pattern->set_datatype(result); } diff --git a/modules/gdscript/gdscript_byte_codegen.cpp b/modules/gdscript/gdscript_byte_codegen.cpp index 8394fce9b353..bbc805ee06f2 100644 --- a/modules/gdscript/gdscript_byte_codegen.cpp +++ b/modules/gdscript/gdscript_byte_codegen.cpp @@ -61,6 +61,10 @@ uint32_t GDScriptByteCodeGenerator::add_or_get_constant(const Variant &p_constan return get_constant_pos(p_constant); } +uint32_t GDScriptByteCodeGenerator::add_or_get_variant_vector_constant(const Vector &p_constant) { + return get_variant_vector_constant_pos(p_constant); +} + uint32_t GDScriptByteCodeGenerator::add_or_get_name(const StringName &p_name) { return get_name_map_pos(p_name); } @@ -208,6 +212,11 @@ GDScriptFunction *GDScriptByteCodeGenerator::write_end() { function->_constant_count = 0; } + function->variant_vector_constants.resize(variant_vector_constant_map.size()); + for (const KeyValue, int> &K : variant_vector_constant_map) { + function->variant_vector_constants.write[K.value] = K.key; + } + if (name_map.size()) { function->global_names.resize(name_map.size()); function->_global_names_ptr = &function->global_names[0]; @@ -1482,6 +1491,82 @@ void GDScriptByteCodeGenerator::write_end_jump_if_shared() { if_jmp_addrs.pop_back(); } +void GDScriptByteCodeGenerator::write_jump_table_range(const Address &p_value, int p_offset, int p_size, int p_branch_count) { + ERR_FAIL_COND(p_size < 0); + + append_opcode(GDScriptFunction::OPCODE_JUMP_TABLE_RANGE); + append(p_value); + append(p_offset); + append(p_size); + + int start_addr = opcodes.size(); + for (int i = 0; i < p_size; i++) { + append(-1); // Jump destination, will be patched. + } + append(-1); // Jump destination, will be patched. + + jmp_tables.push_back(JumpTable(start_addr, p_size + 1, p_branch_count)); +} + +void GDScriptByteCodeGenerator::write_jump_table_bsearch(const Address &p_value, const Vector &p_array, int p_branch_count) { + append_opcode(GDScriptFunction::OPCODE_JUMP_TABLE_BSEARCH); + append(p_value); + append(add_or_get_variant_vector_constant(p_array)); + + int start_addr = opcodes.size(); + for (int i = 0; i < p_array.size(); i++) { + append(-1); // Jump destination, will be patched. + } + append(-1); // Jump destination, will be patched. + + jmp_tables.push_back(JumpTable(start_addr, p_array.size() + 1, p_branch_count)); +} + +void GDScriptByteCodeGenerator::write_jump_table_branch() { + JumpTable &jmp_table = jmp_tables.back()->get(); + jmp_table.current_branch++; + + ERR_FAIL_COND(jmp_table.current_branch >= jmp_table.branch_count); + + if (jmp_table.current_branch > 0) { + // End previous branch. + append_opcode(GDScriptFunction::OPCODE_JUMP); // Jump to end. + jmp_table.jmp_to_end_addrs.push_back(opcodes.size()); + append(0); // Jump destination, will be patched. + } + + jmp_table.branch_start_addrs.write[jmp_table.current_branch] = opcodes.size(); +} + +// Unset cases jump to the end. +void GDScriptByteCodeGenerator::write_jump_table_set_case_branch(int p_case, int p_branch) { + JumpTable &jmp_table = jmp_tables.back()->get(); + + ERR_FAIL_INDEX(p_case, jmp_table.case_count); + ERR_FAIL_INDEX(p_branch, jmp_table.branch_count); + ERR_FAIL_COND(p_branch > jmp_table.current_branch); + + opcodes.write[jmp_table.start_addr + p_case] = jmp_table.branch_start_addrs[p_branch]; +} + +void GDScriptByteCodeGenerator::write_end_jump_table() { + JumpTable &jmp_table = jmp_tables.back()->get(); + + ERR_FAIL_COND(jmp_table.current_branch != jmp_table.branch_count - 1); + + for (int i = 0; i < jmp_table.case_count; i++) { + if (opcodes[jmp_table.start_addr + i] < 0) { + patch_jump(jmp_table.start_addr + i); + } + } + + for (List::Element *E = jmp_table.jmp_to_end_addrs.front(); E; E = E->next()) { + patch_jump(E->get()); + } + + jmp_tables.pop_back(); +} + void GDScriptByteCodeGenerator::start_for(const GDScriptDataType &p_iterator_type, const GDScriptDataType &p_list_type) { Address counter(Address::LOCAL_VARIABLE, add_local("@counter_pos", p_iterator_type), p_iterator_type); Address container(Address::LOCAL_VARIABLE, add_local("@container_pos", p_list_type), p_list_type); diff --git a/modules/gdscript/gdscript_byte_codegen.h b/modules/gdscript/gdscript_byte_codegen.h index 671dea5d6d67..11c9247ada96 100644 --- a/modules/gdscript/gdscript_byte_codegen.h +++ b/modules/gdscript/gdscript_byte_codegen.h @@ -74,6 +74,26 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { CallTarget &operator=(CallTarget &) = delete; }; + struct JumpTable { + int start_addr = 0; + int case_count = 0; // Including the default case. + int branch_count = 0; // Including the default branch (if any). + int current_branch = -1; + Vector branch_start_addrs; + List jmp_to_end_addrs; + + JumpTable() {} + JumpTable(int p_start_addr, int p_case_count, int p_branch_count) { + ERR_FAIL_COND(p_case_count < 0); + ERR_FAIL_COND(p_branch_count < 0 || p_branch_count > p_case_count); + + start_addr = p_start_addr; + case_count = p_case_count; + branch_count = p_branch_count; + branch_start_addrs.resize(p_branch_count); + } + }; + bool ended = false; GDScriptFunction *function = nullptr; bool debug_stack = false; @@ -104,6 +124,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { #endif HashMap constant_map; + HashMap, int, VariantHasher, VariantComparator> variant_vector_constant_map; RBMap name_map; #ifdef TOOLS_ENABLED Vector named_globals; @@ -147,6 +168,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { List
for_container_variables; List while_jmp_addrs; List continue_addrs; + List jmp_tables; // Used to patch jumps with `and` and `or` operators with short-circuit. List logic_op_jump_pos1; @@ -229,6 +251,15 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { return pos; } + int get_variant_vector_constant_pos(const Vector &p_constant) { + if (variant_vector_constant_map.has(p_constant)) { + return variant_vector_constant_map[p_constant]; + } + int pos = variant_vector_constant_map.size(); + variant_vector_constant_map[p_constant] = pos; + return pos; + } + int get_operation_pos(const Variant::ValidatedOperatorEvaluator p_operation) { if (operator_func_map.has(p_operation)) { return operator_func_map[p_operation]; @@ -459,6 +490,7 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { virtual uint32_t add_local(const StringName &p_name, const GDScriptDataType &p_type) override; virtual uint32_t add_local_constant(const StringName &p_name, const Variant &p_constant) override; virtual uint32_t add_or_get_constant(const Variant &p_constant) override; + virtual uint32_t add_or_get_variant_vector_constant(const Vector &p_constant) override; virtual uint32_t add_or_get_name(const StringName &p_name) override; virtual uint32_t add_temporary(const GDScriptDataType &p_type) override; virtual void pop_temporary() override; @@ -534,6 +566,11 @@ class GDScriptByteCodeGenerator : public GDScriptCodeGenerator { virtual void write_endif() override; virtual void write_jump_if_shared(const Address &p_value) override; virtual void write_end_jump_if_shared() override; + virtual void write_jump_table_range(const Address &p_value, int p_offset, int p_size, int p_branch_count) override; + virtual void write_jump_table_bsearch(const Address &p_value, const Vector &p_array, int p_branch_count) override; + virtual void write_jump_table_branch() override; + virtual void write_jump_table_set_case_branch(int p_case, int p_branch) override; + virtual void write_end_jump_table() override; virtual void start_for(const GDScriptDataType &p_iterator_type, const GDScriptDataType &p_list_type) override; virtual void write_for_assignment(const Address &p_list) override; virtual void write_for(const Address &p_variable, bool p_use_conversion) override; diff --git a/modules/gdscript/gdscript_codegen.h b/modules/gdscript/gdscript_codegen.h index cf17353dec19..001cdd83e3e1 100644 --- a/modules/gdscript/gdscript_codegen.h +++ b/modules/gdscript/gdscript_codegen.h @@ -70,6 +70,7 @@ class GDScriptCodeGenerator { virtual uint32_t add_local(const StringName &p_name, const GDScriptDataType &p_type) = 0; virtual uint32_t add_local_constant(const StringName &p_name, const Variant &p_constant) = 0; virtual uint32_t add_or_get_constant(const Variant &p_constant) = 0; + virtual uint32_t add_or_get_variant_vector_constant(const Vector &p_constant) = 0; virtual uint32_t add_or_get_name(const StringName &p_name) = 0; virtual uint32_t add_temporary(const GDScriptDataType &p_type) = 0; virtual void pop_temporary() = 0; @@ -144,6 +145,11 @@ class GDScriptCodeGenerator { virtual void write_endif() = 0; virtual void write_jump_if_shared(const Address &p_value) = 0; virtual void write_end_jump_if_shared() = 0; + virtual void write_jump_table_range(const Address &p_value, int p_offset, int p_size, int p_branch_count) = 0; + virtual void write_jump_table_bsearch(const Address &p_value, const Vector &p_array, int p_branch_count) = 0; + virtual void write_jump_table_branch() = 0; + virtual void write_jump_table_set_case_branch(int p_case, int p_branch) = 0; + virtual void write_end_jump_table() = 0; virtual void start_for(const GDScriptDataType &p_iterator_type, const GDScriptDataType &p_list_type) = 0; virtual void write_for_assignment(const Address &p_list) = 0; virtual void write_for(const Address &p_variable, bool p_use_conversion) = 0; diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 7f2c401afc51..afaf868a1821 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -1379,439 +1379,235 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code } } -GDScriptCodeGenerator::Address GDScriptCompiler::_parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested) { - switch (p_pattern->pattern_type) { - case GDScriptParser::PatternNode::PT_LITERAL: { - if (p_is_nested) { - codegen.generator->write_and_left_operand(p_previous_test); - } else if (!p_is_first) { - codegen.generator->write_or_left_operand(p_previous_test); - } - - // Get literal type into constant map. - Variant::Type literal_type = p_pattern->literal->value.get_type(); - GDScriptCodeGenerator::Address literal_type_addr = codegen.add_constant(literal_type); - - // Equality is always a boolean. - GDScriptDataType equality_type; - equality_type.has_type = true; - equality_type.kind = GDScriptDataType::BUILTIN; - equality_type.builtin_type = Variant::BOOL; - - // Check type equality. - GDScriptCodeGenerator::Address type_equality_addr = codegen.add_temporary(equality_type); - codegen.generator->write_binary_operator(type_equality_addr, Variant::OP_EQUAL, p_type_addr, literal_type_addr); - - if (literal_type == Variant::STRING) { - GDScriptCodeGenerator::Address type_stringname_addr = codegen.add_constant(Variant::STRING_NAME); - - // Check StringName <-> String type equality. - GDScriptCodeGenerator::Address tmp_comp_addr = codegen.add_temporary(equality_type); - - codegen.generator->write_binary_operator(tmp_comp_addr, Variant::OP_EQUAL, p_type_addr, type_stringname_addr); - codegen.generator->write_binary_operator(type_equality_addr, Variant::OP_OR, type_equality_addr, tmp_comp_addr); - - codegen.generator->pop_temporary(); // Remove tmp_comp_addr from stack. - } else if (literal_type == Variant::STRING_NAME) { - GDScriptCodeGenerator::Address type_string_addr = codegen.add_constant(Variant::STRING); - - // Check String <-> StringName type equality. - GDScriptCodeGenerator::Address tmp_comp_addr = codegen.add_temporary(equality_type); - - codegen.generator->write_binary_operator(tmp_comp_addr, Variant::OP_EQUAL, p_type_addr, type_string_addr); - codegen.generator->write_binary_operator(type_equality_addr, Variant::OP_OR, type_equality_addr, tmp_comp_addr); +Error GDScriptCompiler::_parse_match(CodeGen &codegen, const GDScriptParser::MatchNode *p_match) { + Error err = OK; + GDScriptCodeGenerator *gen = codegen.generator; - codegen.generator->pop_temporary(); // Remove tmp_comp_addr from stack. - } + codegen.start_block(); - codegen.generator->write_and_left_operand(type_equality_addr); + // Evaluate the match expression. + GDScriptDataType value_type = _gdtype_from_datatype(p_match->test->get_datatype(), codegen.script); + GDScriptCodeGenerator::Address value = codegen.add_local("@match_value", value_type); + GDScriptCodeGenerator::Address value_expr = _parse_expression(codegen, err, p_match->test); + if (err) { + return err; + } - // Get literal. - GDScriptCodeGenerator::Address literal_addr = _parse_expression(codegen, r_error, p_pattern->literal); - if (r_error) { - return GDScriptCodeGenerator::Address(); - } + // Assign to local. + // TODO: This can be improved by passing the target to parse_expression(). + gen->write_assign(value, value_expr); + if (value_expr.mode == GDScriptCodeGenerator::Address::TEMPORARY) { + gen->pop_temporary(); + } - // Check value equality. - GDScriptCodeGenerator::Address equality_addr = codegen.add_temporary(equality_type); - codegen.generator->write_binary_operator(equality_addr, Variant::OP_EQUAL, p_value_addr, literal_addr); - codegen.generator->write_and_right_operand(equality_addr); + // Check the type if needed. + GDScriptDataType typeof_type; + typeof_type.has_type = true; + typeof_type.kind = GDScriptDataType::BUILTIN; + typeof_type.builtin_type = Variant::INT; + GDScriptCodeGenerator::Address type; + if (!value_type.has_type) { + type = codegen.add_local("@match_type", typeof_type); + + Vector typeof_args; + typeof_args.push_back(value); + gen->write_call_utility(type, "typeof", typeof_args); + } - // AND both together (reuse temporary location). - codegen.generator->write_end_and(type_equality_addr); + // Collect constant case branches to write the group using a jump table. + // Variable and destructuring patterns break `match` into several groups. + List branches; + HashSet handled_const_cases; + for (int i = 0; i < p_match->branches.size(); i++) { + const GDScriptParser::MatchBranchNode *branch_node = p_match->branches[i]; + MatchBranch branch(branch_node); - codegen.generator->pop_temporary(); // Remove equality_addr from stack. + if (branch_node->has_wildcard) { + branches.push_back(branch); + break; + } - if (literal_addr.mode == GDScriptCodeGenerator::Address::TEMPORARY) { - codegen.generator->pop_temporary(); - } + bool all_constant = true; + for (int j = 0; j < branch_node->patterns.size(); j++) { + const GDScriptParser::PatternNode *pattern_node = branch_node->patterns[j]; + bool is_constant = false; + Variant pt_value; - // If this isn't the first, we need to OR with the previous pattern. If it's nested, we use AND instead. - if (p_is_nested) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_and_right_operand(type_equality_addr); - codegen.generator->write_end_and(p_previous_test); - } else if (!p_is_first) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_or_right_operand(type_equality_addr); - codegen.generator->write_end_or(p_previous_test); - } else { - // Just assign this value to the accumulator temporary. - codegen.generator->write_assign(p_previous_test, type_equality_addr); + switch (pattern_node->pattern_type) { + case GDScriptParser::PatternNode::PT_LITERAL: + is_constant = true; + pt_value = pattern_node->literal->value; + break; + case GDScriptParser::PatternNode::PT_EXPRESSION: + if (pattern_node->expression->is_constant) { + is_constant = true; + pt_value = pattern_node->expression->reduced_value; + } else { + all_constant = false; + } + break; + case GDScriptParser::PatternNode::PT_ARRAY: + case GDScriptParser::PatternNode::PT_DICTIONARY: + all_constant = false; + break; + case GDScriptParser::PatternNode::PT_BIND: + case GDScriptParser::PatternNode::PT_REST: + case GDScriptParser::PatternNode::PT_WILDCARD: + return ERR_BUG; // This shouldn't happen. } - codegen.generator->pop_temporary(); // Remove type_equality_addr. - return p_previous_test; - } break; - case GDScriptParser::PatternNode::PT_EXPRESSION: { - if (p_is_nested) { - codegen.generator->write_and_left_operand(p_previous_test); - } else if (!p_is_first) { - codegen.generator->write_or_left_operand(p_previous_test); - } - - GDScriptCodeGenerator::Address type_string_addr = codegen.add_constant(Variant::STRING); - GDScriptCodeGenerator::Address type_stringname_addr = codegen.add_constant(Variant::STRING_NAME); - - // Equality is always a boolean. - GDScriptDataType equality_type; - equality_type.has_type = true; - equality_type.kind = GDScriptDataType::BUILTIN; - equality_type.builtin_type = Variant::BOOL; - - // Create the result temps first since it's the last to go away. - GDScriptCodeGenerator::Address result_addr = codegen.add_temporary(equality_type); - GDScriptCodeGenerator::Address equality_test_addr = codegen.add_temporary(equality_type); - GDScriptCodeGenerator::Address stringy_comp_addr = codegen.add_temporary(equality_type); - GDScriptCodeGenerator::Address stringy_comp_addr_2 = codegen.add_temporary(equality_type); - GDScriptCodeGenerator::Address expr_type_addr = codegen.add_temporary(); - - // Evaluate expression. - GDScriptCodeGenerator::Address expr_addr; - expr_addr = _parse_expression(codegen, r_error, p_pattern->expression); - if (r_error) { - return GDScriptCodeGenerator::Address(); + if (is_constant && !handled_const_cases.has(pt_value)) { + handled_const_cases.insert(pt_value); + branch.cases.push_back(pt_value); } + } - // Evaluate expression type. - Vector typeof_args; - typeof_args.push_back(expr_addr); - codegen.generator->write_call_utility(expr_type_addr, "typeof", typeof_args); - - // Check type equality. - codegen.generator->write_binary_operator(result_addr, Variant::OP_EQUAL, p_type_addr, expr_type_addr); - - // Check for String <-> StringName comparison. - codegen.generator->write_binary_operator(stringy_comp_addr, Variant::OP_EQUAL, p_type_addr, type_string_addr); - codegen.generator->write_binary_operator(stringy_comp_addr_2, Variant::OP_EQUAL, expr_type_addr, type_stringname_addr); - codegen.generator->write_binary_operator(stringy_comp_addr, Variant::OP_AND, stringy_comp_addr, stringy_comp_addr_2); - codegen.generator->write_binary_operator(result_addr, Variant::OP_OR, result_addr, stringy_comp_addr); - - // Check for StringName <-> String comparison. - codegen.generator->write_binary_operator(stringy_comp_addr, Variant::OP_EQUAL, p_type_addr, type_stringname_addr); - codegen.generator->write_binary_operator(stringy_comp_addr_2, Variant::OP_EQUAL, expr_type_addr, type_string_addr); - codegen.generator->write_binary_operator(stringy_comp_addr, Variant::OP_AND, stringy_comp_addr, stringy_comp_addr_2); - codegen.generator->write_binary_operator(result_addr, Variant::OP_OR, result_addr, stringy_comp_addr); - - codegen.generator->pop_temporary(); // Remove expr_type_addr from stack. - codegen.generator->pop_temporary(); // Remove stringy_comp_addr_2 from stack. - codegen.generator->pop_temporary(); // Remove stringy_comp_addr from stack. - - codegen.generator->write_and_left_operand(result_addr); - - // Check value equality. - codegen.generator->write_binary_operator(equality_test_addr, Variant::OP_EQUAL, p_value_addr, expr_addr); - codegen.generator->write_and_right_operand(equality_test_addr); - - // AND both type and value equality. - codegen.generator->write_end_and(result_addr); - - // We don't need the expression temporary anymore. - if (expr_addr.mode == GDScriptCodeGenerator::Address::TEMPORARY) { - codegen.generator->pop_temporary(); + if (all_constant) { + if (!branch.cases.is_empty()) { + branches.push_back(branch); } - codegen.generator->pop_temporary(); // Remove equality_test_addr from stack. - - // If this isn't the first, we need to OR with the previous pattern. If it's nested, we use AND instead. - if (p_is_nested) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_and_right_operand(result_addr); - codegen.generator->write_end_and(p_previous_test); - } else if (!p_is_first) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_or_right_operand(result_addr); - codegen.generator->write_end_or(p_previous_test); - } else { - // Just assign this value to the accumulator temporary. - codegen.generator->write_assign(p_previous_test, result_addr); - } - codegen.generator->pop_temporary(); // Remove temp result addr. - - return p_previous_test; - } break; - case GDScriptParser::PatternNode::PT_ARRAY: { - if (p_is_nested) { - codegen.generator->write_and_left_operand(p_previous_test); - } else if (!p_is_first) { - codegen.generator->write_or_left_operand(p_previous_test); - } - // Get array type into constant map. - GDScriptCodeGenerator::Address array_type_addr = codegen.add_constant((int)Variant::ARRAY); - - // Equality is always a boolean. - GDScriptDataType temp_type; - temp_type.has_type = true; - temp_type.kind = GDScriptDataType::BUILTIN; - temp_type.builtin_type = Variant::BOOL; - - // Check type equality. - GDScriptCodeGenerator::Address result_addr = codegen.add_temporary(temp_type); - codegen.generator->write_binary_operator(result_addr, Variant::OP_EQUAL, p_type_addr, array_type_addr); - codegen.generator->write_and_left_operand(result_addr); - - // Store pattern length in constant map. - GDScriptCodeGenerator::Address array_length_addr = codegen.add_constant(p_pattern->rest_used ? p_pattern->array.size() - 1 : p_pattern->array.size()); - - // Get value length. - temp_type.builtin_type = Variant::INT; - GDScriptCodeGenerator::Address value_length_addr = codegen.add_temporary(temp_type); - Vector len_args; - len_args.push_back(p_value_addr); - codegen.generator->write_call_gdscript_utility(value_length_addr, "len", len_args); - - // Test length compatibility. - temp_type.builtin_type = Variant::BOOL; - GDScriptCodeGenerator::Address length_compat_addr = codegen.add_temporary(temp_type); - codegen.generator->write_binary_operator(length_compat_addr, p_pattern->rest_used ? Variant::OP_GREATER_EQUAL : Variant::OP_EQUAL, value_length_addr, array_length_addr); - codegen.generator->write_and_right_operand(length_compat_addr); - - // AND type and length check. - codegen.generator->write_end_and(result_addr); - - // Remove length temporaries. - codegen.generator->pop_temporary(); - codegen.generator->pop_temporary(); - - // Create temporaries outside the loop so they can be reused. - GDScriptCodeGenerator::Address element_addr = codegen.add_temporary(); - GDScriptCodeGenerator::Address element_type_addr = codegen.add_temporary(); - - // Evaluate element by element. - for (int i = 0; i < p_pattern->array.size(); i++) { - if (p_pattern->array[i]->pattern_type == GDScriptParser::PatternNode::PT_REST) { - // Don't want to access an extra element of the user array. - break; + } else { + // The constant patterns **before the first variable/destructuring pattern** + // can be included in the jump table (and the jump addresses patched later). + // But this is a rare case, so let's skip the optimization. + if (!branches.is_empty()) { + err = _parse_match_branch_group(codegen, value, branches); + if (err) { + return err; } + branches.clear(); + } + /*err = _parse_match_branch(codegen, value, branch); + if (err) { + return err; + }*/ + all_constant = true; // Resume. + } + } - // Use AND here too, as we don't want to be checking elements if previous test failed (which means this might be an invalid get). - codegen.generator->write_and_left_operand(result_addr); + if (!branches.is_empty()) { + err = _parse_match_branch_group(codegen, value, branches); + if (err) { + return err; + } + } - // Add index to constant map. - GDScriptCodeGenerator::Address index_addr = codegen.add_constant(i); + codegen.end_block(); - // Get the actual element from the user-sent array. - codegen.generator->write_get(element_addr, index_addr, p_value_addr); + return OK; +} - // Also get type of element. - Vector typeof_args; - typeof_args.push_back(element_addr); - codegen.generator->write_call_utility(element_type_addr, "typeof", typeof_args); +// TODO +Error GDScriptCompiler::_parse_match_branch_group(CodeGen &codegen, const GDScriptCodeGenerator::Address &p_value, const List &p_branches) { + Error err = OK; + GDScriptCodeGenerator *gen = codegen.generator; - // Try the pattern inside the element. - result_addr = _parse_match_pattern(codegen, r_error, p_pattern->array[i], element_addr, element_type_addr, result_addr, false, true); - if (r_error != OK) { - return GDScriptCodeGenerator::Address(); + constexpr int RANGE_TABLE_MAX_SIZE = 256; + + int branch_index = 0; + int min_int = 0; + int max_int = 0; + bool use_bsearch = false; + HashMap case_to_branch_index; + Vector array; + + // TODO: Use `if` if there are few cases. + // TODO: always use bsearch if value is not hard int (enum). + // TODO: default branch + // TODO: make correct Variant comparator for sort and bsearch! + // For example, if types are different, first compare their Variant::Type. + // NB: String vs StringName. + + for (const List::Element *E = p_branches.front(); E; E = E->next()) { + const MatchBranch &branch = E->get(); + for (const List::Element *F = branch.cases.front(); F; F = F->next()) { + const Variant &_case = F->get(); + if (_case.get_type() == Variant::INT) { + int value = _case; + if (array.is_empty()) { + min_int = value; + max_int = value; + } else if (value > max_int) { + max_int = value; + } else if (value < min_int) { + min_int = value; } - - codegen.generator->write_and_right_operand(result_addr); - codegen.generator->write_end_and(result_addr); - } - // Remove element temporaries. - codegen.generator->pop_temporary(); - codegen.generator->pop_temporary(); - - // If this isn't the first, we need to OR with the previous pattern. If it's nested, we use AND instead. - if (p_is_nested) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_and_right_operand(result_addr); - codegen.generator->write_end_and(p_previous_test); - } else if (!p_is_first) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_or_right_operand(result_addr); - codegen.generator->write_end_or(p_previous_test); } else { - // Just assign this value to the accumulator temporary. - codegen.generator->write_assign(p_previous_test, result_addr); + use_bsearch = true; } - codegen.generator->pop_temporary(); // Remove temp result addr. - - return p_previous_test; - } break; - case GDScriptParser::PatternNode::PT_DICTIONARY: { - if (p_is_nested) { - codegen.generator->write_and_left_operand(p_previous_test); - } else if (!p_is_first) { - codegen.generator->write_or_left_operand(p_previous_test); - } - // Get dictionary type into constant map. - GDScriptCodeGenerator::Address dict_type_addr = codegen.add_constant((int)Variant::DICTIONARY); - - // Equality is always a boolean. - GDScriptDataType temp_type; - temp_type.has_type = true; - temp_type.kind = GDScriptDataType::BUILTIN; - temp_type.builtin_type = Variant::BOOL; - - // Check type equality. - GDScriptCodeGenerator::Address result_addr = codegen.add_temporary(temp_type); - codegen.generator->write_binary_operator(result_addr, Variant::OP_EQUAL, p_type_addr, dict_type_addr); - codegen.generator->write_and_left_operand(result_addr); - - // Store pattern length in constant map. - GDScriptCodeGenerator::Address dict_length_addr = codegen.add_constant(p_pattern->rest_used ? p_pattern->dictionary.size() - 1 : p_pattern->dictionary.size()); - - // Get user's dictionary length. - temp_type.builtin_type = Variant::INT; - GDScriptCodeGenerator::Address value_length_addr = codegen.add_temporary(temp_type); - Vector func_args; - func_args.push_back(p_value_addr); - codegen.generator->write_call_gdscript_utility(value_length_addr, "len", func_args); - - // Test length compatibility. - temp_type.builtin_type = Variant::BOOL; - GDScriptCodeGenerator::Address length_compat_addr = codegen.add_temporary(temp_type); - codegen.generator->write_binary_operator(length_compat_addr, p_pattern->rest_used ? Variant::OP_GREATER_EQUAL : Variant::OP_EQUAL, value_length_addr, dict_length_addr); - codegen.generator->write_and_right_operand(length_compat_addr); - - // AND type and length check. - codegen.generator->write_end_and(result_addr); - - // Remove length temporaries. - codegen.generator->pop_temporary(); - codegen.generator->pop_temporary(); + case_to_branch_index[_case] = branch_index; + array.push_back(_case); + } + branch_index++; + } - // Create temporaries outside the loop so they can be reused. - GDScriptCodeGenerator::Address element_addr = codegen.add_temporary(); - GDScriptCodeGenerator::Address element_type_addr = codegen.add_temporary(); + if (max_int - min_int > RANGE_TABLE_MAX_SIZE) { + use_bsearch = true; + } - // Evaluate element by element. - for (int i = 0; i < p_pattern->dictionary.size(); i++) { - const GDScriptParser::PatternNode::Pair &element = p_pattern->dictionary[i]; - if (element.value_pattern && element.value_pattern->pattern_type == GDScriptParser::PatternNode::PT_REST) { - // Ignore rest pattern. - break; - } + int default_case_index = 0; + if (use_bsearch) { + default_case_index = array.size(); + array.sort_custom(); + gen->write_jump_table_bsearch(p_value, array, p_branches.size()); + } else { + default_case_index = max_int - min_int + 1; + gen->write_jump_table_range(p_value, min_int, max_int - min_int + 1, p_branches.size()); + } - // Use AND here too, as we don't want to be checking elements if previous test failed (which means this might be an invalid get). - codegen.generator->write_and_left_operand(result_addr); + for (const List::Element *E = p_branches.front(); E; E = E->next()) { + const MatchBranch &branch = E->get(); + gen->write_jump_table_branch(); - // Get the pattern key. - GDScriptCodeGenerator::Address pattern_key_addr = _parse_expression(codegen, r_error, element.key); - if (r_error) { - return GDScriptCodeGenerator::Address(); - } + codegen.start_block(); // Create an extra block around for binds. - // Check if pattern key exists in user's dictionary. This will be AND-ed with next result. - func_args.clear(); - func_args.push_back(pattern_key_addr); - codegen.generator->write_call(result_addr, p_value_addr, "has", func_args); + // Add locals in block before patterns, so temporaries don't use the stack address for binds. + List branch_locals = _add_locals_in_block(codegen, branch.branch->block); - if (element.value_pattern != nullptr) { - // Use AND here too, as we don't want to be checking elements if previous test failed (which means this might be an invalid get). - codegen.generator->write_and_left_operand(result_addr); +#ifdef DEBUG_ENABLED + // Add a newline before each branch, since the debugger needs those. + gen->write_newline(branch.branch->start_line); +#endif - // Get actual value from user dictionary. - codegen.generator->write_get(element_addr, pattern_key_addr, p_value_addr); + err = _parse_block(codegen, branch.branch->block, false); // Don't add locals again. + if (err) { + return err; + } - // Also get type of value. - func_args.clear(); - func_args.push_back(element_addr); - codegen.generator->write_call_utility(element_type_addr, "typeof", func_args); + _clear_addresses(codegen, branch_locals); - // Try the pattern inside the value. - result_addr = _parse_match_pattern(codegen, r_error, element.value_pattern, element_addr, element_type_addr, result_addr, false, true); - if (r_error != OK) { - return GDScriptCodeGenerator::Address(); - } - codegen.generator->write_and_right_operand(result_addr); - codegen.generator->write_end_and(result_addr); - } - - codegen.generator->write_and_right_operand(result_addr); - codegen.generator->write_end_and(result_addr); + codegen.end_block(); // Get out of extra block. + } - // Remove pattern key temporary. - if (pattern_key_addr.mode == GDScriptCodeGenerator::Address::TEMPORARY) { - codegen.generator->pop_temporary(); - } + if (use_bsearch) { + for (int i = 0; i < array.size(); i++) { + HashMap::ConstIterator E = case_to_branch_index.find(array[i]); + if (E) { + gen->write_jump_table_set_case_branch(i, E->value); } + } + } else { + for (int i = min_int; i <= max_int; i++) { + HashMap::ConstIterator E = case_to_branch_index.find(i); + if (E) { + gen->write_jump_table_set_case_branch(i - min_int, E->value); + } + } + } - // Remove element temporaries. - codegen.generator->pop_temporary(); - codegen.generator->pop_temporary(); + const MatchBranch &last_branch = p_branches.back()->get(); + if (last_branch.branch->has_wildcard) { + // Set default case to default branch. + gen->write_jump_table_set_case_branch(default_case_index, p_branches.size() - 1); + } - // If this isn't the first, we need to OR with the previous pattern. If it's nested, we use AND instead. - if (p_is_nested) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_and_right_operand(result_addr); - codegen.generator->write_end_and(p_previous_test); - } else if (!p_is_first) { - // Use the previous value as target, since we only need one temporary variable. - codegen.generator->write_or_right_operand(result_addr); - codegen.generator->write_end_or(p_previous_test); - } else { - // Just assign this value to the accumulator temporary. - codegen.generator->write_assign(p_previous_test, result_addr); - } - codegen.generator->pop_temporary(); // Remove temp result addr. + gen->write_end_jump_table(); - return p_previous_test; - } break; - case GDScriptParser::PatternNode::PT_REST: - // Do nothing. - return p_previous_test; - break; - case GDScriptParser::PatternNode::PT_BIND: { - if (p_is_nested) { - codegen.generator->write_and_left_operand(p_previous_test); - } else if (!p_is_first) { - codegen.generator->write_or_left_operand(p_previous_test); - } - // Get the bind address. - GDScriptCodeGenerator::Address bind = codegen.locals[p_pattern->bind->name]; + return OK; +} - // Assign value to bound variable. - codegen.generator->write_assign(bind, p_value_addr); - } - [[fallthrough]]; // Act like matching anything too. - case GDScriptParser::PatternNode::PT_WILDCARD: - // If this is a fall through we don't want to do this again. - if (p_pattern->pattern_type != GDScriptParser::PatternNode::PT_BIND) { - if (p_is_nested) { - codegen.generator->write_and_left_operand(p_previous_test); - } else if (!p_is_first) { - codegen.generator->write_or_left_operand(p_previous_test); - } - } - // This matches anything so just do the same as `if(true)`. - // If this isn't the first, we need to OR with the previous pattern. If it's nested, we use AND instead. - if (p_is_nested) { - // Use the operator with the `true` constant so it works as always matching. - GDScriptCodeGenerator::Address constant = codegen.add_constant(true); - codegen.generator->write_and_right_operand(constant); - codegen.generator->write_end_and(p_previous_test); - } else if (!p_is_first) { - // Use the operator with the `true` constant so it works as always matching. - GDScriptCodeGenerator::Address constant = codegen.add_constant(true); - codegen.generator->write_or_right_operand(constant); - codegen.generator->write_end_or(p_previous_test); - } else { - // Just assign this value to the accumulator temporary. - codegen.generator->write_assign_true(p_previous_test); - } - return p_previous_test; - } - ERR_FAIL_V_MSG(p_previous_test, "Reaching the end of pattern compilation without matching a pattern."); +GDScriptCodeGenerator::Address GDScriptCompiler::_parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested) { + // TODO } List GDScriptCompiler::_add_locals_in_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block) { @@ -1859,85 +1655,10 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui switch (s->type) { case GDScriptParser::Node::MATCH: { - const GDScriptParser::MatchNode *match = static_cast(s); - - codegen.start_block(); - - // Evaluate the match expression. - GDScriptCodeGenerator::Address value = codegen.add_local("@match_value", _gdtype_from_datatype(match->test->get_datatype(), codegen.script)); - GDScriptCodeGenerator::Address value_expr = _parse_expression(codegen, err, match->test); - if (err) { + err = _parse_match(codegen, static_cast(s)); + if (err != OK) { return err; } - - // Assign to local. - // TODO: This can be improved by passing the target to parse_expression(). - gen->write_assign(value, value_expr); - - if (value_expr.mode == GDScriptCodeGenerator::Address::TEMPORARY) { - codegen.generator->pop_temporary(); - } - - // Then, let's save the type of the value in the stack too, so we can reuse for later comparisons. - GDScriptDataType typeof_type; - typeof_type.has_type = true; - typeof_type.kind = GDScriptDataType::BUILTIN; - typeof_type.builtin_type = Variant::INT; - GDScriptCodeGenerator::Address type = codegen.add_local("@match_type", typeof_type); - - Vector typeof_args; - typeof_args.push_back(value); - gen->write_call_utility(type, "typeof", typeof_args); - - // Now we can actually start testing. - // For each branch. - for (int j = 0; j < match->branches.size(); j++) { - if (j > 0) { - // Use `else` to not check the next branch after matching. - gen->write_else(); - } - - const GDScriptParser::MatchBranchNode *branch = match->branches[j]; - - codegen.start_block(); // Create an extra block around for binds. - - // Add locals in block before patterns, so temporaries don't use the stack address for binds. - List branch_locals = _add_locals_in_block(codegen, branch->block); - -#ifdef DEBUG_ENABLED - // Add a newline before each branch, since the debugger needs those. - gen->write_newline(branch->start_line); -#endif - // For each pattern in branch. - GDScriptCodeGenerator::Address pattern_result = codegen.add_temporary(); - for (int k = 0; k < branch->patterns.size(); k++) { - pattern_result = _parse_match_pattern(codegen, err, branch->patterns[k], value, type, pattern_result, k == 0, false); - if (err != OK) { - return err; - } - } - - // Check if pattern did match. - gen->write_if(pattern_result); - - // Remove the result from stack. - gen->pop_temporary(); - - // Parse the branch block. - err = _parse_block(codegen, branch->block, false); // Don't add locals again. - if (err) { - return err; - } - - _clear_addresses(codegen, branch_locals); - - codegen.end_block(); // Get out of extra block. - } - - // End all nested `if`s. - for (int j = 0; j < match->branches.size(); j++) { - gen->write_endif(); - } } break; case GDScriptParser::Node::IF: { const GDScriptParser::IfNode *if_n = static_cast(s); diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h index 099bd00a2ea1..497b17d935bc 100644 --- a/modules/gdscript/gdscript_compiler.h +++ b/modules/gdscript/gdscript_compiler.h @@ -115,6 +115,17 @@ class GDScriptCompiler { } }; + struct MatchBranch { + const GDScriptParser::MatchBranchNode *branch = nullptr; + // Constant cases that are possible for the branch (duplicates excluded). + List cases; + + MatchBranch() {} + MatchBranch(const GDScriptParser::MatchBranchNode *p_branch) { + branch = p_branch; + } + }; + bool _is_class_member_property(CodeGen &codegen, const StringName &p_name); bool _is_class_member_property(GDScript *owner, const StringName &p_name); bool _is_local_or_parameter(CodeGen &codegen, const StringName &p_name); @@ -128,6 +139,8 @@ class GDScriptCompiler { GDScriptCodeGenerator::Address _parse_assign_right_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::AssignmentNode *p_assignmentint, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address()); GDScriptCodeGenerator::Address _parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root = false, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address()); + Error _parse_match(CodeGen &codegen, const GDScriptParser::MatchNode *p_match); + Error _parse_match_branch_group(CodeGen &codegen, const GDScriptCodeGenerator::Address &p_value, const List &p_branches); GDScriptCodeGenerator::Address _parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested); List _add_locals_in_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block); void _clear_addresses(CodeGen &codegen, const List &p_addresses); diff --git a/modules/gdscript/gdscript_disassembler.cpp b/modules/gdscript/gdscript_disassembler.cpp index 438ec0274063..15fa4e9b220c 100644 --- a/modules/gdscript/gdscript_disassembler.cpp +++ b/modules/gdscript/gdscript_disassembler.cpp @@ -947,6 +947,55 @@ void GDScriptFunction::disassemble(const Vector &p_code_lines) const { incr = 3; } break; + case OPCODE_JUMP_TABLE_RANGE: { + text += "jump-table-range "; + text += DADDR(1); + text += "\n"; + + int offset = _code_ptr[ip + 2]; + int size = _code_ptr[ip + 3]; + + String indent = String(" ").repeat(7 + itos(ip).length()); + + for (int i = 0; i < size; i++) { + text += indent; + text += "case "; + text += itos(offset + i); + text += " to "; + text += itos(_code_ptr[ip + 4 + i]); + text += "\n"; + } + + text += indent; + text += "default to "; + text += itos(_code_ptr[ip + 4 + size]); + + incr = 4 + size + 1; + } break; + case OPCODE_JUMP_TABLE_BSEARCH: { + text += "jump-table-bsearch "; + text += DADDR(1); + text += "\n"; + + Vector array = get_variant_vector_constant(_code_ptr[ip + 2] & ADDR_MASK); + + String indent = String(" ").repeat(7 + itos(ip).length()); + + for (int i = 0; i < array.size(); i++) { + text += indent; + text += "case "; + text += _get_variant_string(array[i]); + text += " to "; + text += itos(_code_ptr[ip + 3 + i]); + text += "\n"; + } + + text += indent; + text += "default to "; + text += itos(_code_ptr[ip + 3 + array.size()]); + + incr = 3 + array.size() + 1; + } break; case OPCODE_RETURN: { text += "return "; text += DADDR(1); diff --git a/modules/gdscript/gdscript_function.cpp b/modules/gdscript/gdscript_function.cpp index 4f5a65a70964..2008dc857b99 100644 --- a/modules/gdscript/gdscript_function.cpp +++ b/modules/gdscript/gdscript_function.cpp @@ -37,6 +37,11 @@ Variant GDScriptFunction::get_constant(int p_idx) const { return constants[p_idx]; } +Vector GDScriptFunction::get_variant_vector_constant(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, variant_vector_constants.size(), Vector()); + return variant_vector_constants[p_idx]; +} + StringName GDScriptFunction::get_global_name(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, global_names.size(), ""); return global_names[p_idx]; diff --git a/modules/gdscript/gdscript_function.h b/modules/gdscript/gdscript_function.h index 31da70f9ae0c..3444b91c48d7 100644 --- a/modules/gdscript/gdscript_function.h +++ b/modules/gdscript/gdscript_function.h @@ -289,6 +289,8 @@ class GDScriptFunction { OPCODE_JUMP_IF_NOT, OPCODE_JUMP_TO_DEF_ARGUMENT, OPCODE_JUMP_IF_SHARED, + OPCODE_JUMP_TABLE_RANGE, + OPCODE_JUMP_TABLE_BSEARCH, OPCODE_RETURN, OPCODE_RETURN_TYPED_BUILTIN, OPCODE_RETURN_TYPED_ARRAY, @@ -406,6 +408,23 @@ class GDScriptFunction { StringName identifier; }; + // For binary search. + struct SafeVariantSort { + _FORCE_INLINE_ bool operator()(const Variant &p_l, const Variant &p_r) const { + if (p_l.get_type() != p_r.get_type()) { + return p_l.get_type() < p_r.get_type(); + } + bool valid = false; + Variant res; + Variant::evaluate(Variant::OP_LESS, p_l, p_r, res, valid); + if (!valid) { + ERR_PRINT(vformat("Failed to compare %s and %s.", p_l, p_r)); + res = false; + } + return res; + } + }; + private: friend class GDScript; friend class GDScriptCompiler; @@ -435,6 +454,8 @@ class GDScriptFunction { Vector code; Vector default_arguments; Vector constants; + // For OPCODE_JUMP_TABLE_BSEARCH to avoid Array <-> Vector conversion overhead. + Vector> variant_vector_constants; Vector global_names; Vector operator_funcs; Vector setters; @@ -543,6 +564,7 @@ class GDScriptFunction { _FORCE_INLINE_ int get_max_stack_size() const { return _stack_size; } Variant get_constant(int p_idx) const; + Vector get_variant_vector_constant(int p_idx) const; StringName get_global_name(int p_idx) const; Variant call(GDScriptInstance *p_instance, const Variant **p_args, int p_argcount, Callable::CallError &r_err, CallState *p_state = nullptr); diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp index 52c1a5b141e2..730f9cffdafd 100644 --- a/modules/gdscript/gdscript_parser.cpp +++ b/modules/gdscript/gdscript_parser.cpp @@ -1959,10 +1959,10 @@ GDScriptParser::IfNode *GDScriptParser::parse_if(const String &p_token) { } GDScriptParser::MatchNode *GDScriptParser::parse_match() { - MatchNode *match = alloc_node(); + MatchNode *match_node = alloc_node(); - match->test = parse_expression(false); - if (match->test == nullptr) { + match_node->test = parse_expression(false); + if (match_node->test == nullptr) { push_error(R"(Expected expression to test after "match".)"); } @@ -1970,14 +1970,19 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected a newline after "match" statement.)"); if (!consume(GDScriptTokenizer::Token::INDENT, R"(Expected an indented block after "match" statement.)")) { - complete_extents(match); - return match; + complete_extents(match_node); + return match_node; } bool all_have_return = true; bool have_wildcard = false; while (!check(GDScriptTokenizer::Token::DEDENT) && !is_at_end()) { + if (match(GDScriptTokenizer::Token::PASS)) { + consume(GDScriptTokenizer::Token::NEWLINE, R"(Expected newline after "pass".)"); + continue; + } + MatchBranchNode *branch = parse_match_branch(); if (branch == nullptr) { advance(); @@ -1992,9 +1997,9 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { have_wildcard = have_wildcard || branch->has_wildcard; all_have_return = all_have_return && branch->block->has_return; - match->branches.push_back(branch); + match_node->branches.push_back(branch); } - complete_extents(match); + complete_extents(match_node); consume(GDScriptTokenizer::Token::DEDENT, R"(Expected an indented block after "match" statement.)"); @@ -2002,7 +2007,7 @@ GDScriptParser::MatchNode *GDScriptParser::parse_match() { current_suite->has_return = true; } - return match; + return match_node; } GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() { @@ -2019,8 +2024,14 @@ GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() { if (pattern->binds.size() > 0) { has_bind = true; } - if (branch->patterns.size() > 0 && has_bind) { - push_error(R"(Cannot use a variable bind with multiple patterns.)"); + if (branch->patterns.size() > 0) { + if (has_bind) { + push_error(R"(Cannot use a variable bind with multiple patterns.)"); +#ifdef DEBUG_ENABLED + } else if (branch->has_wildcard || pattern->pattern_type == PatternNode::PT_BIND || pattern->pattern_type == PatternNode::PT_WILDCARD) { + push_warning(branch->has_wildcard ? pattern : branch->patterns[0], GDScriptWarning::REDUNDANT_PATTERN); +#endif + } } if (pattern->pattern_type == PatternNode::PT_REST) { push_error(R"(Rest pattern can only be used inside array and dictionary patterns.)"); diff --git a/modules/gdscript/gdscript_vm.cpp b/modules/gdscript/gdscript_vm.cpp index c0644e089cdc..80c9339822b9 100644 --- a/modules/gdscript/gdscript_vm.cpp +++ b/modules/gdscript/gdscript_vm.cpp @@ -283,6 +283,8 @@ void (*type_init_function_table[])(Variant *) = { &&OPCODE_JUMP_IF_NOT, \ &&OPCODE_JUMP_TO_DEF_ARGUMENT, \ &&OPCODE_JUMP_IF_SHARED, \ + &&OPCODE_JUMP_TABLE_RANGE, \ + &&OPCODE_JUMP_TABLE_BSEARCH, \ &&OPCODE_RETURN, \ &&OPCODE_RETURN_TYPED_BUILTIN, \ &&OPCODE_RETURN_TYPED_ARRAY, \ @@ -2590,6 +2592,44 @@ Variant GDScriptFunction::call(GDScriptInstance *p_instance, const Variant **p_a } DISPATCH_OPCODE; + OPCODE(OPCODE_JUMP_TABLE_RANGE) { + CHECK_SPACE(4); + + GET_VARIANT_PTR(val, 0); + GD_ERR_BREAK(val->get_type() != Variant::INT); + + int value = *val; + int offset = _code_ptr[ip + 2]; + int size = _code_ptr[ip + 3]; + + CHECK_SPACE(4 + size + 1); + + int index = value - offset; + ip = (index >= 0 && index < size) ? _code_ptr[ip + 4 + index] : _code_ptr[ip + 4 + size]; + GD_ERR_BREAK(ip < 0 || ip >= _code_size); + } + DISPATCH_OPCODE; + + OPCODE(OPCODE_JUMP_TABLE_BSEARCH) { + CHECK_SPACE(3); + + GET_VARIANT_PTR(val, 0); + int arr_idx = _code_ptr[ip + 2]; + GD_ERR_BREAK(arr_idx < 0 || arr_idx >= variant_vector_constants.size()); + + Variant value = *val; + Vector array = variant_vector_constants[arr_idx]; + + CHECK_SPACE(3 + array.size() + 1); + + SearchArray sa; + int index = sa.bisect(array.ptr(), array.size(), value, true); + bool found = index < array.size() && array[index].get_type() == value.get_type() && array[index] == value; + ip = found ? _code_ptr[ip + 3 + index] : _code_ptr[ip + 3 + array.size()]; + GD_ERR_BREAK(ip < 0 || ip >= _code_size); + } + DISPATCH_OPCODE; + OPCODE(OPCODE_RETURN) { CHECK_SPACE(2); GET_VARIANT_PTR(r, 0); diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index ef2913926cbb..a33c264419e1 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -119,6 +119,8 @@ String GDScriptWarning::get_message() const { return R"(The "@static_unload" annotation is redundant because the file does not have a class with static variables.)"; case REDUNDANT_AWAIT: return R"("await" keyword not needed in this case, because the expression isn't a coroutine nor a signal.)"; + case REDUNDANT_PATTERN: + return R"(There is no point to use multiple patterns with a wildcard pattern.)"; case ASSERT_ALWAYS_TRUE: return "Assert statement is redundant because the expression is always true."; case ASSERT_ALWAYS_FALSE: @@ -216,6 +218,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "STATIC_CALLED_ON_INSTANCE", "REDUNDANT_STATIC_UNLOAD", "REDUNDANT_AWAIT", + "REDUNDANT_PATTERN", "ASSERT_ALWAYS_TRUE", "ASSERT_ALWAYS_FALSE", "INTEGER_DIVISION", diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index 2b177093388b..212fe8763e52 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -74,6 +74,7 @@ class GDScriptWarning { STATIC_CALLED_ON_INSTANCE, // A static method was called on an instance of a class instead of on the class itself. REDUNDANT_STATIC_UNLOAD, // The `@static_unload` annotation is used but the class does not have static data. REDUNDANT_AWAIT, // await is used but expression is synchronous (not a signal nor a coroutine). + REDUNDANT_PATTERN, // Pattern in a match statement with a wildcard pattern. ASSERT_ALWAYS_TRUE, // Expression for assert argument is always true. ASSERT_ALWAYS_FALSE, // Expression for assert argument is always false. INTEGER_DIVISION, // Integer divide by integer, decimal part is discarded. @@ -122,6 +123,7 @@ class GDScriptWarning { WARN, // STATIC_CALLED_ON_INSTANCE WARN, // REDUNDANT_STATIC_UNLOAD WARN, // REDUNDANT_AWAIT + WARN, // REDUNDANT_PATTERN WARN, // ASSERT_ALWAYS_TRUE WARN, // ASSERT_ALWAYS_FALSE WARN, // INTEGER_DIVISION