ConditionExpression: Replaced Token sub-classing with TokenTypeInfo data.

This commit is contained in:
atlimit8
2017-02-12 17:58:02 -06:00
parent 828b13a11e
commit 6e393f99cb
2 changed files with 88 additions and 93 deletions

View File

@@ -171,18 +171,6 @@ namespace OpenRA.Support
}
}
class BinaryOperationToken : Token
{
public BinaryOperationToken(TokenType type, int index) : base(type, index) { }
}
class UnaryOperationToken : Token
{
public UnaryOperationToken(TokenType type, int index) : base(type, index) { }
}
class OpenParenToken : Token { public OpenParenToken(int index) : base(TokenType.OpenParen, index) { } }
class CloseParenToken : Token { public CloseParenToken(int index) : base(TokenType.CloseParen, index) { } }
class VariableToken : Token
{
public readonly string Name;
@@ -207,12 +195,6 @@ namespace OpenRA.Support
}
}
class AndToken : BinaryOperationToken { public AndToken(int index) : base(TokenType.And, index) { } }
class OrToken : BinaryOperationToken { public OrToken(int index) : base(TokenType.Or, index) { } }
class EqualsToken : BinaryOperationToken { public EqualsToken(int index) : base(TokenType.Equals, index) { } }
class NotEqualsToken : BinaryOperationToken { public NotEqualsToken(int index) : base(TokenType.NotEquals, index) { } }
class NotToken : UnaryOperationToken { public NotToken(int index) : base(TokenType.Not, index) { } }
public ConditionExpression(string expression)
{
Expression = expression;
@@ -225,14 +207,14 @@ namespace OpenRA.Support
{
case '(':
{
tokens.Add(new OpenParenToken(i));
tokens.Add(new Token(TokenType.OpenParen, i));
openParens++;
break;
}
case ')':
{
tokens.Add(new CloseParenToken(i));
tokens.Add(new Token(TokenType.CloseParen, i));
if (++closeParens > openParens)
throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(i));
@@ -248,9 +230,8 @@ namespace OpenRA.Support
var token = ParseSymbol(expression, ref i);
tokens.Add(token);
var variable = token as VariableToken;
if (variable != null)
variables.Add(variable.Symbol);
if (token.Type == TokenType.Variable)
variables.Add(token.Symbol);
break;
}
@@ -264,39 +245,31 @@ namespace OpenRA.Support
if (closeParens != openParens)
throw new InvalidDataException("Mismatched opening and closing parentheses");
// Expressions can't start with a binary or unary postfix operation or closer
if (tokens[0].LeftOperand)
throw new InvalidDataException("Missing value or sub-expression at beginning for `{0}` operator".F(tokens[0].Symbol));
for (var i = 0; i < tokens.Count - 1; i++)
{
// Unary tokens must be followed by a variable, number, another unary token, or an opening parenthesis
if (tokens[i] is UnaryOperationToken && !(tokens[i + 1] is VariableToken || tokens[i + 1] is NumberToken
|| tokens[i + 1] is UnaryOperationToken || tokens[i + 1] is OpenParenToken))
throw new InvalidDataException("Unexpected token `{0}` at index {1}".F(tokens[i].Symbol, tokens[i].Index));
// Disallow empty parentheses
if (tokens[i] is OpenParenToken && tokens[i + 1] is CloseParenToken)
if (tokens[i].Opens != Grouping.None && tokens[i + 1].Closes != Grouping.None)
throw new InvalidDataException("Empty parenthesis at index {0}".F(tokens[i].Index));
// A variable or number must be followed by a binary operation or by a closing parenthesis
if ((tokens[i] is VariableToken || tokens[i] is NumberToken) && !(tokens[i + 1] is BinaryOperationToken || tokens[i + 1] is CloseParenToken))
throw new InvalidDataException("Missing binary operation at index {0}".F(tokens[i + 1].Index));
}
// Expressions can't start with an operation
if (tokens[0] is BinaryOperationToken)
throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[0].Symbol, tokens[0].Index));
// Expressions can't end with a binary or unary operation
if (tokens[tokens.Count - 1] is BinaryOperationToken || tokens[tokens.Count - 1] is UnaryOperationToken)
throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[tokens.Count - 1].Symbol, tokens[tokens.Count - 1].Index));
// Binary operations must be preceeded by a closing paren or a variable
// Binary operations must be followed by an opening paren, a variable, or a unary operation
for (var i = 1; i < tokens.Count - 1; i++)
// Exactly one of two consective tokens must take the other's sub-expression evaluation as an operand
if (tokens[i].RightOperand == tokens[i + 1].LeftOperand)
{
if (tokens[i] is BinaryOperationToken && (
!(tokens[i - 1] is CloseParenToken || tokens[i - 1] is VariableToken || tokens[i - 1] is NumberToken) ||
!(tokens[i + 1] is OpenParenToken || tokens[i + 1] is VariableToken || tokens[i + 1] is NumberToken || tokens[i + 1] is UnaryOperationToken)))
throw new InvalidDataException("Unexpected token `{0}` at index `{1}`".F(tokens[i].Symbol, tokens[i].Index));
if (tokens[i].RightOperand)
throw new InvalidDataException(
"Missing value or sub-expression or there is an extra operator `{0}` at index {1} or `{2}` at index {3}".F(
tokens[i].Symbol, tokens[i].Index, tokens[i + 1].Symbol, tokens[i + 1].Index));
throw new InvalidDataException("Missing binary operation before `{0}` at index {1}".F(
tokens[i + 1].Symbol, tokens[i + 1].Index));
}
}
// Expressions can't end with a binary or unary prefix operation
if (tokens[tokens.Count - 1].RightOperand)
throw new InvalidDataException("Missing value or sub-expression at end for `{0}` operator".F(tokens[tokens.Count - 1].Symbol));
// Convert to postfix (discarding parentheses) ready for evaluation
postfix = ToPostfix(tokens).ToArray();
@@ -377,10 +350,10 @@ namespace OpenRA.Support
if (i < expression.Length - 1 && expression[start + 1] == '=')
{
i++;
return new NotEqualsToken(start);
return new Token(TokenType.NotEquals, start);
}
return new NotToken(start);
return new Token(TokenType.Not, start);
}
case '=':
@@ -388,10 +361,10 @@ namespace OpenRA.Support
if (i < expression.Length - 1 && expression[start + 1] == '=')
{
i++;
return new EqualsToken(start);
return new Token(TokenType.Equals, start);
}
throw new InvalidDataException("Unexpected character '=' at index {0}".F(start));
throw new InvalidDataException("Unexpected character '=' at index {0} - should it be `==`?".F(start));
}
case '&':
@@ -399,10 +372,10 @@ namespace OpenRA.Support
if (i < expression.Length - 1 && expression[start + 1] == '&')
{
i++;
return new AndToken(start);
return new Token(TokenType.And, start);
}
throw new InvalidDataException("Unexpected character '&' at index {0}".F(start));
throw new InvalidDataException("Unexpected character '&' at index {0} - should it be `&&`?".F(start));
}
case '|':
@@ -410,10 +383,10 @@ namespace OpenRA.Support
if (i < expression.Length - 1 && expression[start + 1] == '|')
{
i++;
return new OrToken(start);
return new Token(TokenType.Or, start);
}
throw new InvalidDataException("Unexpected character '|' at index {0}".F(start));
throw new InvalidDataException("Unexpected character '|' at index {0} - should it be `||`?".F(start));
}
}
@@ -429,7 +402,8 @@ namespace OpenRA.Support
if (cc != CharClass.Digit)
{
if (cc != CharClass.Whitespace && cc != CharClass.Operator)
throw new InvalidDataException("Number and variable merged at index {0}".F(start));
throw new InvalidDataException("Number {0} and variable merged at index {1}".F(
int.Parse(expression.Substring(start, i - start)), start));
// Put the bad character back for the next parse attempt
i--;
@@ -441,7 +415,7 @@ namespace OpenRA.Support
}
if (cc != CharClass.Id)
throw new InvalidDataException("Invalid character at index {0}".F(start));
throw new InvalidDataException("Invalid character '{0}' at index {1}".F(expression[i], start));
// Scan forwards until we find an invalid name character
for (; i < expression.Length; i++)
@@ -484,15 +458,15 @@ namespace OpenRA.Support
var s = new Stack<Token>();
foreach (var t in tokens)
{
if (t is OpenParenToken)
if (t.Opens != Grouping.None)
s.Push(t);
else if (t is CloseParenToken)
else if (t.Closes != Grouping.None)
{
Token temp;
while (!((temp = s.Pop()) is OpenParenToken))
while (!((temp = s.Pop()).Opens != Grouping.None))
yield return temp;
}
else if (t is VariableToken || t is NumberToken)
else if (t.OperandSides == OperandSides.None)
yield return t;
else
{
@@ -513,20 +487,33 @@ namespace OpenRA.Support
var s = new Stack<int>();
foreach (var t in postfix)
{
if (t is AndToken)
switch (t.Type)
{
case TokenType.And:
ApplyBinaryOperation(s, (x, y) => y > 0 ? x : y);
else if (t is NotEqualsToken)
continue;
case TokenType.NotEquals:
ApplyBinaryOperation(s, (x, y) => (y != x) ? 1 : 0);
else if (t is OrToken)
continue;
case TokenType.Or:
ApplyBinaryOperation(s, (x, y) => y > 0 ? y : x);
else if (t is EqualsToken)
continue;
case TokenType.Equals:
ApplyBinaryOperation(s, (x, y) => (y == x) ? 1 : 0);
else if (t is NotToken)
continue;
case TokenType.Not:
ApplyUnaryOperation(s, x => (x > 0) ? 0 : 1);
else if (t is NumberToken)
continue;
case TokenType.Number:
s.Push(((NumberToken)t).Value);
else if (t is VariableToken)
continue;
case TokenType.Variable:
s.Push(ParseSymbol((VariableToken)t, symbols));
continue;
default:
throw new InvalidProgramException("Evaluate is missing an evaluator for TokenType.{0}".F(
Enum<TokenType>.GetValues()[(int)t.Type]));
}
}
return s.Pop();

View File

@@ -47,10 +47,18 @@ namespace OpenRA.Test
Assert.Throws(typeof(InvalidDataException), () => new ConditionExpression(expression).Evaluate(testValues), expression);
}
void AssertParseFailure(string expression, string errorMessage)
{
var actualErrorMessage = Assert.Throws(typeof(InvalidDataException),
() => new ConditionExpression(expression).Evaluate(testValues),
expression).Message;
Assert.AreEqual(errorMessage, actualErrorMessage, expression + " ===> " + actualErrorMessage);
}
[TestCase(TestName = "Numbers")]
public void TestNumbers()
{
AssertParseFailure("1a");
AssertParseFailure("1a", "Number 1 and variable merged at index 0");
AssertValue("0", 0);
AssertValue("1", 1);
AssertValue("12", 12);
@@ -168,21 +176,21 @@ namespace OpenRA.Test
[TestCase(TestName = "Test parser errors")]
public void TestParseErrors()
{
AssertParseFailure("()");
AssertParseFailure("! && true");
AssertParseFailure("(true");
AssertParseFailure(")true");
AssertParseFailure("false)");
AssertParseFailure("false(");
AssertParseFailure("false!");
AssertParseFailure("true false");
AssertParseFailure("true & false");
AssertParseFailure("true | false");
AssertParseFailure("true : false");
AssertParseFailure("true & false && !");
AssertParseFailure("(true && !)");
AssertParseFailure("&& false");
AssertParseFailure("false ||");
AssertParseFailure("()", "Empty parenthesis at index 0");
AssertParseFailure("! && true", "Missing value or sub-expression or there is an extra operator `!` at index 0 or `&&` at index 2");
AssertParseFailure("(true", "Mismatched opening and closing parentheses");
AssertParseFailure(")true", "Unmatched closing parenthesis at index 0");
AssertParseFailure("false)", "Unmatched closing parenthesis at index 5");
AssertParseFailure("false(", "Mismatched opening and closing parentheses");
AssertParseFailure("false!", "Missing binary operation before `!` at index 5");
AssertParseFailure("true false", "Missing binary operation before `false` at index 5");
AssertParseFailure("true & false", "Unexpected character '&' at index 5 - should it be `&&`?");
AssertParseFailure("true | false", "Unexpected character '|' at index 5 - should it be `||`?");
AssertParseFailure("true : false", "Invalid character ':' at index 5");
AssertParseFailure("true & false && !", "Unexpected character '&' at index 5 - should it be `&&`?");
AssertParseFailure("(true && !)", "Missing value or sub-expression or there is an extra operator `!` at index 9 or `)` at index 10");
AssertParseFailure("&& false", "Missing value or sub-expression at beginning for `&&` operator");
AssertParseFailure("false ||", "Missing value or sub-expression at end for `||` operator");
}
[TestCase(TestName = "Undefined symbols are treated as `false` (0) values")]