Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GDScript: Implement pattern guards for match statement #80085

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2415,6 +2415,7 @@ void GDScriptLanguage::get_reserved_words(List<String> *p_words) const {
"return",
"match",
"while",
"when",
// These keywords are not implemented currently, but reserved for (potential) future use.
// We highlight them as keywords to make errors easier to understand.
"trait",
Expand Down Expand Up @@ -2448,6 +2449,7 @@ bool GDScriptLanguage::is_control_flow_keyword(String p_keyword) const {
p_keyword == "match" ||
p_keyword == "pass" ||
p_keyword == "return" ||
p_keyword == "when" ||
p_keyword == "while";
}

Expand Down
4 changes: 4 additions & 0 deletions modules/gdscript/gdscript_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2219,6 +2219,10 @@ void GDScriptAnalyzer::resolve_match_branch(GDScriptParser::MatchBranchNode *p_m
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);

decide_suite_type(p_match_branch, p_match_branch->block);
Expand Down
20 changes: 20 additions & 0 deletions modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
gen->write_if(pattern_result);

Expand Down
33 changes: 32 additions & 1 deletion modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2035,7 +2035,37 @@ GDScriptParser::MatchBranchNode *GDScriptParser::parse_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);
return nullptr;
}
Expand Down Expand Up @@ -3674,6 +3704,7 @@ GDScriptParser::ParseRule *GDScriptParser::get_rule(GDScriptTokenizer::Token::Ty
{ nullptr, nullptr, PREC_NONE }, // PASS,
{ nullptr, nullptr, PREC_NONE }, // RETURN,
{ nullptr, nullptr, PREC_NONE }, // MATCH,
{ nullptr, nullptr, PREC_NONE }, // WHEN,
// Keywords
{ nullptr, &GDScriptParser::parse_cast, PREC_CAST }, // AS,
{ nullptr, nullptr, PREC_NONE }, // ASSERT,
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,7 @@ class GDScriptParser {
Vector<PatternNode *> patterns;
SuiteNode *block = nullptr;
bool has_wildcard = false;
SuiteNode *guard_body = nullptr;

MatchBranchNode() {
type = MATCH_BRANCH;
Expand Down
4 changes: 4 additions & 0 deletions modules/gdscript/gdscript_tokenizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ static const char *token_names[] = {
"pass", // PASS,
"return", // RETURN,
"match", // MATCH,
"when", // WHEN,
// Keywords
"as", // AS,
"assert", // ASSERT,
Expand Down Expand Up @@ -187,6 +188,7 @@ bool GDScriptTokenizer::Token::is_identifier() const {
switch (type) {
case IDENTIFIER:
case MATCH: // Used in String.match().
case WHEN: // New keyword, avoid breaking existing code.
// Allow constants to be treated as regular identifiers.
case CONST_PI:
case CONST_INF:
Expand Down Expand Up @@ -241,6 +243,7 @@ bool GDScriptTokenizer::Token::is_node_name() const {
case VAR:
case VOID:
case WHILE:
case WHEN:
case YIELD:
return true;
default:
Expand Down Expand Up @@ -531,6 +534,7 @@ GDScriptTokenizer::Token GDScriptTokenizer::annotation() {
KEYWORD("void", Token::VOID) \
KEYWORD_GROUP('w') \
KEYWORD("while", Token::WHILE) \
KEYWORD("when", Token::WHEN) \
KEYWORD_GROUP('y') \
KEYWORD("yield", Token::YIELD) \
KEYWORD_GROUP('I') \
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript_tokenizer.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class GDScriptTokenizer {
PASS,
RETURN,
MATCH,
WHEN,
// Keywords
AS,
ASSERT,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
func test():
match 0:
_ when a == 0:
print("a does not exist")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
Identifier "a" not declared in the current scope.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
func test():
var a = 0
match a:
0 when a = 1:
print("assignment not allowed on pattern guard")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
GDTEST_PARSER_ERROR
Assignment is not allowed inside an expression.
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ func test():

var TAU = "TAU"
print(TAU)

# New keyword for pattern guards.
var when = "when"
print(when)
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ PI
INF
NAN
TAU
when
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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