GDScript: Implement pattern guards for match statement
Within a match statement, it is now possible to add guards in each branch: var a = 0 match a: 0 when false: print("does not run") 0 when true: print("but this does") This allows more complex logic for deciding which branch to take.
This commit is contained in:
parent
ec62b8a3ee
commit
54a1414500
15 changed files with 163 additions and 1 deletions
|
@ -2415,6 +2415,7 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const {
|
||||||
"return",
|
"return",
|
||||||
"match",
|
"match",
|
||||||
"while",
|
"while",
|
||||||
|
"when",
|
||||||
// These keywords are not implemented currently, but reserved for (potential) future use.
|
// These keywords are not implemented currently, but reserved for (potential) future use.
|
||||||
// We highlight them as keywords to make errors easier to understand.
|
// We highlight them as keywords to make errors easier to understand.
|
||||||
"trait",
|
"trait",
|
||||||
|
@ -2448,6 +2449,7 @@ bool GDScriptLanguage::is_control_flow_keyword(String p_keyword) const {
|
||||||
p_keyword == "match" ||
|
p_keyword == "match" ||
|
||||||
p_keyword == "pass" ||
|
p_keyword == "pass" ||
|
||||||
p_keyword == "return" ||
|
p_keyword == "return" ||
|
||||||
|
p_keyword == "when" ||
|
||||||
p_keyword == "while";
|
p_keyword == "while";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2219,6 +2219,10 @@ void GDScriptAnalyzer::resolve_match_branch(GDScriptParser::MatchBranchNode *p_m
|
||||||
resolve_match_pattern(p_match_branch->patterns[i], p_match_test);
|
resolve_match_pattern(p_match_branch->patterns[i], p_match_test);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (p_match_branch->guard_body) {
|
||||||
|
resolve_suite(p_match_branch->guard_body);
|
||||||
|
}
|
||||||
|
|
||||||
resolve_suite(p_match_branch->block);
|
resolve_suite(p_match_branch->block);
|
||||||
|
|
||||||
decide_suite_type(p_match_branch, p_match_branch->block);
|
decide_suite_type(p_match_branch, p_match_branch->block);
|
||||||
|
|
|
@ -1928,6 +1928,26 @@ Error GDScriptCompiler::_parse_block(CodeGen &codegen, const GDScriptParser::Sui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's a guard, check its condition too.
|
||||||
|
if (branch->guard_body != nullptr) {
|
||||||
|
// Do this first so the guard does not run unless the pattern matched.
|
||||||
|
gen->write_and_left_operand(pattern_result);
|
||||||
|
|
||||||
|
// Don't actually use the block for the guard.
|
||||||
|
// The binds are already in the locals and we don't want to clear the result of the guard condition before we check the actual match.
|
||||||
|
GDScriptCodeGenerator::Address guard_result = _parse_expression(codegen, err, static_cast<GDScriptParser::ExpressionNode *>(branch->guard_body->statements[0]));
|
||||||
|
if (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
gen->write_and_right_operand(guard_result);
|
||||||
|
gen->write_end_and(pattern_result);
|
||||||
|
|
||||||
|
if (guard_result.mode == GDScriptCodeGenerator::Address::TEMPORARY) {
|
||||||
|
codegen.generator->pop_temporary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if pattern did match.
|
// Check if pattern did match.
|
||||||
gen->write_if(pattern_result);
|
gen->write_if(pattern_result);
|
||||||
|
|
||||||
|
|
|
@ -2035,7 +2035,37 @@ GDScriptParser::MatchBranchNode *GDScriptParser::parse_match_branch() {
|
||||||
push_error(R"(No pattern found for "match" branch.)");
|
push_error(R"(No pattern found for "match" branch.)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!consume(GDScriptTokenizer::Token::COLON, R"(Expected ":" after "match" patterns.)")) {
|
bool has_guard = false;
|
||||||
|
if (match(GDScriptTokenizer::Token::WHEN)) {
|
||||||
|
// Pattern guard.
|
||||||
|
// Create block for guard because it also needs to access the bound variables from patterns, and we don't want to add them to the outer scope.
|
||||||
|
branch->guard_body = alloc_node<SuiteNode>();
|
||||||
|
if (branch->patterns.size() > 0) {
|
||||||
|
for (const KeyValue<StringName, IdentifierNode *> &E : branch->patterns[0]->binds) {
|
||||||
|
SuiteNode::Local local(E.value, current_function);
|
||||||
|
local.type = SuiteNode::Local::PATTERN_BIND;
|
||||||
|
branch->guard_body->add_local(local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SuiteNode *parent_block = current_suite;
|
||||||
|
branch->guard_body->parent_block = parent_block;
|
||||||
|
current_suite = branch->guard_body;
|
||||||
|
|
||||||
|
ExpressionNode *guard = parse_expression(false);
|
||||||
|
if (guard == nullptr) {
|
||||||
|
push_error(R"(Expected expression for pattern guard after "when".)");
|
||||||
|
} else {
|
||||||
|
branch->guard_body->statements.append(guard);
|
||||||
|
}
|
||||||
|
current_suite = parent_block;
|
||||||
|
complete_extents(branch->guard_body);
|
||||||
|
|
||||||
|
has_guard = true;
|
||||||
|
branch->has_wildcard = false; // If it has a guard, the wildcard might still not match.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!consume(GDScriptTokenizer::Token::COLON, vformat(R"(Expected ":"%s after "match" %s.)", has_guard ? "" : R"( or "when")", has_guard ? "pattern guard" : "patterns"))) {
|
||||||
complete_extents(branch);
|
complete_extents(branch);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
@ -3674,6 +3704,7 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty
|
||||||
{ nullptr, nullptr, PREC_NONE }, // PASS,
|
{ nullptr, nullptr, PREC_NONE }, // PASS,
|
||||||
{ nullptr, nullptr, PREC_NONE }, // RETURN,
|
{ nullptr, nullptr, PREC_NONE }, // RETURN,
|
||||||
{ nullptr, nullptr, PREC_NONE }, // MATCH,
|
{ nullptr, nullptr, PREC_NONE }, // MATCH,
|
||||||
|
{ nullptr, nullptr, PREC_NONE }, // WHEN,
|
||||||
// Keywords
|
// Keywords
|
||||||
{ nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS,
|
{ nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS,
|
||||||
{ nullptr, nullptr, PREC_NONE }, // ASSERT,
|
{ nullptr, nullptr, PREC_NONE }, // ASSERT,
|
||||||
|
|
|
@ -948,6 +948,7 @@ public:
|
||||||
Vector<PatternNode *> patterns;
|
Vector<PatternNode *> patterns;
|
||||||
SuiteNode *block = nullptr;
|
SuiteNode *block = nullptr;
|
||||||
bool has_wildcard = false;
|
bool has_wildcard = false;
|
||||||
|
SuiteNode *guard_body = nullptr;
|
||||||
|
|
||||||
MatchBranchNode() {
|
MatchBranchNode() {
|
||||||
type = MATCH_BRANCH;
|
type = MATCH_BRANCH;
|
||||||
|
|
|
@ -99,6 +99,7 @@ static const char *token_names[] = {
|
||||||
"pass", // PASS,
|
"pass", // PASS,
|
||||||
"return", // RETURN,
|
"return", // RETURN,
|
||||||
"match", // MATCH,
|
"match", // MATCH,
|
||||||
|
"when", // WHEN,
|
||||||
// Keywords
|
// Keywords
|
||||||
"as", // AS,
|
"as", // AS,
|
||||||
"assert", // ASSERT,
|
"assert", // ASSERT,
|
||||||
|
@ -187,6 +188,7 @@ bool GDScriptTokenizer::Token::is_identifier() const {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case IDENTIFIER:
|
case IDENTIFIER:
|
||||||
case MATCH: // Used in String.match().
|
case MATCH: // Used in String.match().
|
||||||
|
case WHEN: // New keyword, avoid breaking existing code.
|
||||||
// Allow constants to be treated as regular identifiers.
|
// Allow constants to be treated as regular identifiers.
|
||||||
case CONST_PI:
|
case CONST_PI:
|
||||||
case CONST_INF:
|
case CONST_INF:
|
||||||
|
@ -241,6 +243,7 @@ bool GDScriptTokenizer::Token::is_node_name() const {
|
||||||
case VAR:
|
case VAR:
|
||||||
case VOID:
|
case VOID:
|
||||||
case WHILE:
|
case WHILE:
|
||||||
|
case WHEN:
|
||||||
case YIELD:
|
case YIELD:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
|
@ -531,6 +534,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::annotation() {
|
||||||
KEYWORD("void", Token::VOID) \
|
KEYWORD("void", Token::VOID) \
|
||||||
KEYWORD_GROUP('w') \
|
KEYWORD_GROUP('w') \
|
||||||
KEYWORD("while", Token::WHILE) \
|
KEYWORD("while", Token::WHILE) \
|
||||||
|
KEYWORD("when", Token::WHEN) \
|
||||||
KEYWORD_GROUP('y') \
|
KEYWORD_GROUP('y') \
|
||||||
KEYWORD("yield", Token::YIELD) \
|
KEYWORD("yield", Token::YIELD) \
|
||||||
KEYWORD_GROUP('I') \
|
KEYWORD_GROUP('I') \
|
||||||
|
|
|
@ -105,6 +105,7 @@ public:
|
||||||
PASS,
|
PASS,
|
||||||
RETURN,
|
RETURN,
|
||||||
MATCH,
|
MATCH,
|
||||||
|
WHEN,
|
||||||
// Keywords
|
// Keywords
|
||||||
AS,
|
AS,
|
||||||
ASSERT,
|
ASSERT,
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
func test():
|
||||||
|
match 0:
|
||||||
|
_ when a == 0:
|
||||||
|
print("a does not exist")
|
|
@ -0,0 +1,2 @@
|
||||||
|
GDTEST_ANALYZER_ERROR
|
||||||
|
Identifier "a" not declared in the current scope.
|
|
@ -0,0 +1,5 @@
|
||||||
|
func test():
|
||||||
|
var a = 0
|
||||||
|
match a:
|
||||||
|
0 when a = 1:
|
||||||
|
print("assignment not allowed on pattern guard")
|
|
@ -0,0 +1,2 @@
|
||||||
|
GDTEST_PARSER_ERROR
|
||||||
|
Assignment is not allowed inside an expression.
|
|
@ -14,3 +14,7 @@ func test():
|
||||||
|
|
||||||
var TAU = "TAU"
|
var TAU = "TAU"
|
||||||
print(TAU)
|
print(TAU)
|
||||||
|
|
||||||
|
# New keyword for pattern guards.
|
||||||
|
var when = "when"
|
||||||
|
print(when)
|
||||||
|
|
|
@ -4,3 +4,4 @@ PI
|
||||||
INF
|
INF
|
||||||
NAN
|
NAN
|
||||||
TAU
|
TAU
|
||||||
|
when
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
var global := 0
|
||||||
|
|
||||||
|
func test():
|
||||||
|
var a = 0
|
||||||
|
var b = 1
|
||||||
|
|
||||||
|
match a:
|
||||||
|
0 when b == 0:
|
||||||
|
print("does not run" if true else "")
|
||||||
|
0 when b == 1:
|
||||||
|
print("guards work")
|
||||||
|
_:
|
||||||
|
print("does not run")
|
||||||
|
|
||||||
|
match a:
|
||||||
|
var a_bind when b == 0:
|
||||||
|
prints("a is", a_bind, "and b is 0")
|
||||||
|
var a_bind when b == 1:
|
||||||
|
prints("a is", a_bind, "and b is 1")
|
||||||
|
_:
|
||||||
|
print("does not run")
|
||||||
|
|
||||||
|
match a:
|
||||||
|
var a_bind when a_bind < 0:
|
||||||
|
print("a is less than zero")
|
||||||
|
var a_bind when a_bind == 0:
|
||||||
|
print("a is equal to zero")
|
||||||
|
_:
|
||||||
|
print("a is more than zero")
|
||||||
|
|
||||||
|
match [1, 2, 3]:
|
||||||
|
[1, 2, var element] when element == 0:
|
||||||
|
print("does not run")
|
||||||
|
[1, 2, var element] when element == 3:
|
||||||
|
print("3rd element is 3")
|
||||||
|
|
||||||
|
match a:
|
||||||
|
_ when b == 0:
|
||||||
|
print("does not run")
|
||||||
|
_ when b == 1:
|
||||||
|
print("works with wildcard too.")
|
||||||
|
_:
|
||||||
|
print("does not run")
|
||||||
|
|
||||||
|
match a:
|
||||||
|
0, 1 when b == 0:
|
||||||
|
print("does not run")
|
||||||
|
0, 1 when b == 1:
|
||||||
|
print("guard with multiple patterns")
|
||||||
|
_:
|
||||||
|
print("does not run")
|
||||||
|
|
||||||
|
match a:
|
||||||
|
0 when b == 0:
|
||||||
|
print("does not run")
|
||||||
|
0:
|
||||||
|
print("regular pattern after guard mismatch")
|
||||||
|
|
||||||
|
match a:
|
||||||
|
1 when side_effect():
|
||||||
|
print("should not run the side effect call")
|
||||||
|
0 when side_effect():
|
||||||
|
print("will run the side effect call, but not this")
|
||||||
|
_:
|
||||||
|
assert(global == 1)
|
||||||
|
print("side effect only ran once")
|
||||||
|
|
||||||
|
func side_effect():
|
||||||
|
print("side effect")
|
||||||
|
global += 1
|
||||||
|
return false
|
|
@ -0,0 +1,10 @@
|
||||||
|
GDTEST_OK
|
||||||
|
guards work
|
||||||
|
a is 0 and b is 1
|
||||||
|
a is equal to zero
|
||||||
|
3rd element is 3
|
||||||
|
works with wildcard too.
|
||||||
|
guard with multiple patterns
|
||||||
|
regular pattern after guard mismatch
|
||||||
|
side effect
|
||||||
|
side effect only ran once
|
Loading…
Add table
Reference in a new issue