From ac2b1a4a0369378e155c03ee2a9400dd30762a23 Mon Sep 17 00:00:00 2001 From: Semenov Dmitry Date: Mon, 15 Jan 2024 01:44:06 +0400 Subject: [PATCH] Added guard condition parsing --- src/GDShrapt.Reader.Tests/ParsingTests.cs | 187 ++++++++++++++++++ .../Declarations/GDMatchCaseDeclaration.cs | 111 +++++++++-- .../Lists/GDCommaSeparatedList.cs | 9 +- .../Lists/GDExpressionsList.cs | 7 +- .../Resolvers/GDExpressionResolver.cs | 18 +- .../SimpleTokens/Keywords/GDForKeyword.cs | 6 +- .../SimpleTokens/Keywords/GDIfKeyword.cs | 12 -- .../SimpleTokens/Keywords/GDIfKeyword_.cs | 12 ++ .../SimpleTokens/Keywords/GDWhenKeyword.cs | 12 ++ .../SimpleTokens/Keywords/GDWhileKeyword.cs | 6 +- 10 files changed, 343 insertions(+), 37 deletions(-) delete mode 100644 src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword.cs create mode 100644 src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword_.cs create mode 100644 src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhenKeyword.cs diff --git a/src/GDShrapt.Reader.Tests/ParsingTests.cs b/src/GDShrapt.Reader.Tests/ParsingTests.cs index 4353aed..3f77906 100644 --- a/src/GDShrapt.Reader.Tests/ParsingTests.cs +++ b/src/GDShrapt.Reader.Tests/ParsingTests.cs @@ -1,5 +1,8 @@ +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; +using System.Threading; +using static GDShrapt.Reader.GD; namespace GDShrapt.Reader.Tests { @@ -537,6 +540,190 @@ public void MatchStatementTest2() AssertHelper.NoInvalidTokens(statement); } + [TestMethod] + public void MatchStatementMultiplePatternsTest() + { + var reader = new GDScriptReader(); + + var code = @"match x: + 1, 2, 3: + print(""It's 1 - 3"") + ""Sword"", ""Splash potion"", ""Fist"": + print(""Yep, you've taken damage"")"; + + var statement = reader.ParseStatement(code); + + Assert.IsNotNull(statement); + Assert.IsInstanceOfType(statement, typeof(GDMatchStatement)); + + var matchStatement = (GDMatchStatement)statement; + + matchStatement.Cases.Select(x => x.Conditions.ToString()).Should().BeEquivalentTo(new string[] + { + "1, 2, 3", + "\"Sword\", \"Splash potion\", \"Fist\"" + }); + + AssertHelper.CompareCodeStrings(code, statement.ToString()); + AssertHelper.NoInvalidTokens(statement); + } + + [TestMethod] + public void MatchStatementWildcardPatternTest() + { + var reader = new GDScriptReader(); + + var code = @"match x: + 1: + print(""It's one!"") + 2: + print(""It's one times two!"") + _: + print(""It's not 1 or 2. I don't care to be honest."")"; + + var statement = reader.ParseStatement(code); + + Assert.IsNotNull(statement); + Assert.IsInstanceOfType(statement, typeof(GDMatchStatement)); + + var matchStatement = (GDMatchStatement)statement; + + matchStatement.Cases.Select(x => x.Conditions.ToString()).Should().BeEquivalentTo(new string[] + { + "1", + "2", + "_" + }); + + AssertHelper.CompareCodeStrings(code, statement.ToString()); + AssertHelper.NoInvalidTokens(statement); + } + + [TestMethod] + public void MatchStatementArrayPatternTest() + { + var reader = new GDScriptReader(); + + var code = @"match x: + []: + print(""Empty array"") + [1, 3, ""test"", null]: + print(""Very specific array"") + [var start, _, ""test""]: + print(""First element is "", start, "", and the last is \""test\"""") + [42, ..]: + print(""Open ended array"")"; + + var statement = reader.ParseStatement(code); + + Assert.IsNotNull(statement); + Assert.IsInstanceOfType(statement, typeof(GDMatchStatement)); + + var matchStatement = (GDMatchStatement)statement; + + matchStatement.Cases.Select(x => x.Conditions.ToString()).Should().BeEquivalentTo(new string[] + { + "[]", + "[1, 3, \"test\", null]", + "[var start, _, \"test\"]", + "[42, ..]" + }); + + AssertHelper.CompareCodeStrings(code, statement.ToString()); + AssertHelper.NoInvalidTokens(statement); + } + + [TestMethod] + public void MatchStatementDictionaryPatternTest() + { + var reader = new GDScriptReader(); + + var code = @"match x: + {}: + print(""Empty dict"") + {""name"": ""Dennis""}: + print(""The name is Dennis"") + {""name"": ""Dennis"", ""age"": var age}: + print(""Dennis is "", age, "" years old."") + {""name"", ""age""}: + print(""Has a name and an age, but it's not Dennis :("") + {""key"": ""godotisawesome"", ..}: + print(""I only checked for one entry and ignored the rest"")"; + + var statement = reader.ParseStatement(code); + + Assert.IsNotNull(statement); + Assert.IsInstanceOfType(statement, typeof(GDMatchStatement)); + + var matchStatement = (GDMatchStatement)statement; + + matchStatement.Cases.Select(x => x.Conditions.ToString()).Should().BeEquivalentTo(new string[] + { + "{}", + "{\"name\": \"Dennis\"}", + "{\"name\": \"Dennis\", \"age\": var age}", + "{\"name\", \"age\"}", + "{\"key\": \"godotisawesome\", ..}" + }); + + AssertHelper.CompareCodeStrings(code, statement.ToString()); + AssertHelper.NoInvalidTokens(statement); + } + + [TestMethod] + public void MatchStatementPatternGuardTest() + { + var reader = new GDScriptReader(); + + var code = @"match point: + [0, 0]: + print(""Origin"") + [_, 0]: + print(""Point on X-axis"") + [0, _]: + print(""Point on Y-axis"") + [var x, var y] when y == x: + print(""Point on line y = x"") + [var x, var y] when y == -x: + print(""Point on line y = -x"") + [var x, var y]: + print(""Point (%s, %s)"" % [x, y])"; + + var statement = reader.ParseStatement(code); + + Assert.IsNotNull(statement); + Assert.IsInstanceOfType(statement, typeof(GDMatchStatement)); + + var matchStatement = (GDMatchStatement)statement; + + var cases = matchStatement.Cases; + + Assert.AreEqual(6, cases.Count); + + var one = cases[3]; + + Assert.IsNotNull(one.When); + Assert.IsNotNull(one.GuardCondition); + Assert.AreEqual("y == x", one.GuardCondition.ToString()); + Assert.AreEqual("[var x, var y] ", one.Conditions.ToString()); + + var two = cases[4]; + + Assert.IsNotNull(two.When); + Assert.IsNotNull(two.GuardCondition); + Assert.AreEqual("y == -x", two.GuardCondition.ToString()); + Assert.AreEqual("[var x, var y] ", two.Conditions.ToString()); + + var three = cases[5]; + Assert.AreEqual("[var x, var y]", three.Conditions.ToString()); + + Assert.AreEqual(1, three.Statements.Count); + Assert.AreEqual("print(\"Point (%s, %s)\" % [x, y])", three.Statements[0].ToString()); + + AssertHelper.CompareCodeStrings(code, statement.ToString()); + AssertHelper.NoInvalidTokens(statement); + } + [TestMethod] public void ArrayTest() { diff --git a/src/GDShrapt.Reader/Declarations/GDMatchCaseDeclaration.cs b/src/GDShrapt.Reader/Declarations/GDMatchCaseDeclaration.cs index 8dc8452..ad3b9a0 100644 --- a/src/GDShrapt.Reader/Declarations/GDMatchCaseDeclaration.cs +++ b/src/GDShrapt.Reader/Declarations/GDMatchCaseDeclaration.cs @@ -5,6 +5,8 @@ namespace GDShrapt.Reader { public sealed class GDMatchCaseDeclaration : GDIntendedNode, ITokenOrSkipReceiver, + ITokenOrSkipReceiver, + ITokenOrSkipReceiver, ITokenOrSkipReceiver, ITokenOrSkipReceiver { @@ -13,39 +15,55 @@ public GDExpressionsList Conditions get => _form.Token0 ?? (_form.Token0 = new GDExpressionsList()); set => _form.Token0 = value; } - - public GDColon Colon + public GDWhenKeyword When { get => _form.Token1; set => _form.Token1 = value; } - + public GDExpression GuardCondition + { + get => _form.Token2; + set => _form.Token2 = value; + } + public GDColon Colon + { + get => _form.Token3; + set => _form.Token3 = value; + } + public GDExpression Expression + { + get => _form.Token4; + set => _form.Token4 = value; + } public GDStatementsList Statements { - get => _form.Token2 ?? (_form.Token2 = new GDStatementsList(Intendation + 1)); - set => _form.Token2 = value; + get => _form.Token5 ?? (_form.Token5 = new GDStatementsList(Intendation + 1)); + set => _form.Token5 = value; } public enum State { Conditions, + When, + GuardCondition, Colon, + Expression, Statements, Completed } - readonly GDTokensForm _form; + readonly GDTokensForm _form; public override GDTokensForm Form => _form; - public GDTokensForm TypedForm => _form; + public GDTokensForm TypedForm => _form; internal GDMatchCaseDeclaration(int lineIntendation) : base(lineIntendation) { - _form = new GDTokensForm(this); + _form = new GDTokensForm(this); } public GDMatchCaseDeclaration() { - _form = new GDTokensForm(this); + _form = new GDTokensForm(this); } internal override void HandleChar(char c, GDReadingState state) @@ -56,9 +74,16 @@ internal override void HandleChar(char c, GDReadingState state) switch (_form.State) { case State.Conditions: - _form.State = State.Colon; + _form.State = State.When; state.PushAndPass(Conditions, c); break; + case State.When: + this.ResolveKeyword(c, state); + break; + case State.Expression: + case State.GuardCondition: + this.ResolveExpression(c, state, Intendation); + break; case State.Colon: this.ResolveColon(c, state); break; @@ -76,7 +101,10 @@ internal override void HandleNewLineChar(GDReadingState state) switch (_form.State) { case State.Conditions: + case State.When: + case State.GuardCondition: case State.Colon: + case State.Expression: case State.Statements: _form.State = State.Completed; state.PushAndPassNewLine(Statements); @@ -137,7 +165,7 @@ void ITokenReceiver.HandleReceivedToken(GDExpressionsList tok { if (_form.IsOrLowerState(State.Conditions)) { - _form.State = State.Colon; + _form.State = State.When; Conditions = token; return; } @@ -149,7 +177,7 @@ void ITokenSkipReceiver.HandleReceivedTokenSkip() { if (_form.IsOrLowerState(State.Conditions)) { - _form.State = State.Colon; + _form.State = State.When; return; } @@ -178,5 +206,64 @@ void ITokenSkipReceiver.HandleReceivedTokenSkip() throw new GDInvalidStateException(); } + + void ITokenReceiver.HandleReceivedToken(GDWhenKeyword token) + { + if (_form.IsOrLowerState(State.When)) + { + _form.State = State.GuardCondition; + When = token; + return; + } + + throw new GDInvalidStateException(); + } + + void ITokenSkipReceiver.HandleReceivedTokenSkip() + { + if (_form.IsOrLowerState(State.When)) + { + _form.State = State.Colon; + return; + } + + throw new GDInvalidStateException(); + } + + void ITokenReceiver.HandleReceivedToken(GDExpression token) + { + if (_form.IsOrLowerState(State.GuardCondition)) + { + _form.State = State.Colon; + GuardCondition = token; + return; + } + + if (_form.IsOrLowerState(State.Expression)) + { + _form.State = State.Statements; + Expression = token; + return; + } + + throw new GDInvalidStateException(); + } + + void ITokenSkipReceiver.HandleReceivedTokenSkip() + { + if (_form.IsOrLowerState(State.GuardCondition)) + { + _form.State = State.Colon; + return; + } + + if (_form.IsOrLowerState(State.Expression)) + { + _form.State = State.Statements; + return; + } + + throw new GDInvalidStateException(); + } } } diff --git a/src/GDShrapt.Reader/Lists/GDCommaSeparatedList.cs b/src/GDShrapt.Reader/Lists/GDCommaSeparatedList.cs index 1c86459..7a656b2 100644 --- a/src/GDShrapt.Reader/Lists/GDCommaSeparatedList.cs +++ b/src/GDShrapt.Reader/Lists/GDCommaSeparatedList.cs @@ -6,6 +6,8 @@ public abstract class GDCommaSeparatedList : GDSeparatedList where NODE : GDSyntaxToken { + bool _completed; + internal abstract GDReader ResolveNode(); internal abstract bool IsStopChar(char c); @@ -25,7 +27,7 @@ internal override void HandleChar(char c, GDReadingState state) } else { - if (!IsStopChar(c)) + if (!_completed && !IsStopChar(c)) { state.PushAndPass(ResolveNode(), c); return; @@ -40,6 +42,11 @@ internal override void HandleNewLineChar(GDReadingState state) ListForm.AddToEnd(new GDNewLine()); } + internal void SetAsCompleted() + { + _completed = true; + } + void INewLineReceiver.HandleReceivedToken(GDNewLine token) { ListForm.AddToEnd(token); diff --git a/src/GDShrapt.Reader/Lists/GDExpressionsList.cs b/src/GDShrapt.Reader/Lists/GDExpressionsList.cs index 1737c65..47795d9 100644 --- a/src/GDShrapt.Reader/Lists/GDExpressionsList.cs +++ b/src/GDShrapt.Reader/Lists/GDExpressionsList.cs @@ -1,7 +1,7 @@ namespace GDShrapt.Reader { public sealed class GDExpressionsList : GDCommaSeparatedList, - ITokenReceiver + ITokenOrSkipReceiver { readonly int _intendation; @@ -43,5 +43,10 @@ void ITokenReceiver.HandleReceivedToken(GDExpression token) { ListForm.AddToEnd(token); } + + void ITokenSkipReceiver.HandleReceivedTokenSkip() + { + SetAsCompleted(); + } } } diff --git a/src/GDShrapt.Reader/Resolvers/GDExpressionResolver.cs b/src/GDShrapt.Reader/Resolvers/GDExpressionResolver.cs index 729cef9..958aa2c 100644 --- a/src/GDShrapt.Reader/Resolvers/GDExpressionResolver.cs +++ b/src/GDShrapt.Reader/Resolvers/GDExpressionResolver.cs @@ -233,19 +233,20 @@ private bool CheckKeywords(GDReadingState state) { case "if": case "else": + case "when": { _expression = null; - if (_lastSpace != null) - { - Owner.HandleReceivedToken(_lastSpace); - _lastSpace = null; - } + var space = _lastSpace; + _lastSpace = null; CompleteExpression(state); for (int i = 0; i < s.Length; i++) state.PassChar(s[i]); + + if (space != null) + state.PassString(space.Sequence); } return true; case "setget": @@ -285,6 +286,13 @@ private bool CheckKeywords(GDReadingState state) PushAndSave(state, e); return true; } + case "break": + { + var e = new GDBreakExpression(); + e.Add(new GDBreakKeyword()); + PushAndSave(state, e); + return true; + } case "continue": { var e = new GDContinueExpression(); diff --git a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDForKeyword.cs b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDForKeyword.cs index d22fb48..d66f34c 100644 --- a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDForKeyword.cs +++ b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDForKeyword.cs @@ -1,12 +1,12 @@ namespace GDShrapt.Reader { - public sealed class GDWhileKeyword : GDKeyword + public sealed class GDForKeyword : GDKeyword { - public override string Sequence => "while"; + public override string Sequence => "for"; public override GDSyntaxToken Clone() { - return new GDWhileKeyword(); + return new GDForKeyword(); } } } diff --git a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword.cs b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword.cs deleted file mode 100644 index d66f34c..0000000 --- a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace GDShrapt.Reader -{ - public sealed class GDForKeyword : GDKeyword - { - public override string Sequence => "for"; - - public override GDSyntaxToken Clone() - { - return new GDForKeyword(); - } - } -} diff --git a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword_.cs b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword_.cs new file mode 100644 index 0000000..0d94a47 --- /dev/null +++ b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDIfKeyword_.cs @@ -0,0 +1,12 @@ +namespace GDShrapt.Reader +{ + public sealed class GDIfKeyword : GDKeyword + { + public override string Sequence => "if"; + + public override GDSyntaxToken Clone() + { + return new GDIfKeyword(); + } + } +} diff --git a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhenKeyword.cs b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhenKeyword.cs new file mode 100644 index 0000000..0ba23d4 --- /dev/null +++ b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhenKeyword.cs @@ -0,0 +1,12 @@ +namespace GDShrapt.Reader +{ + public sealed class GDWhenKeyword : GDKeyword + { + public override string Sequence => "when"; + + public override GDSyntaxToken Clone() + { + return new GDWhenKeyword(); + } + } +} diff --git a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhileKeyword.cs b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhileKeyword.cs index 0d94a47..d22fb48 100644 --- a/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhileKeyword.cs +++ b/src/GDShrapt.Reader/SimpleTokens/Keywords/GDWhileKeyword.cs @@ -1,12 +1,12 @@ namespace GDShrapt.Reader { - public sealed class GDIfKeyword : GDKeyword + public sealed class GDWhileKeyword : GDKeyword { - public override string Sequence => "if"; + public override string Sequence => "while"; public override GDSyntaxToken Clone() { - return new GDIfKeyword(); + return new GDWhileKeyword(); } } }