diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index e027a18df4..8fdb3cd35f 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -119,6 +119,8 @@ namespace OpenRA.Support OpenParen, CloseParen, Not, + Negate, + OnesComplement, And, Or, Equals, @@ -127,6 +129,11 @@ namespace OpenRA.Support LessThanOrEqual, GreaterThan, GreaterThanOrEqual, + Add, + Subtract, + Multiply, + Divide, + Modulo, Invalid } @@ -134,6 +141,8 @@ namespace OpenRA.Support enum Precedence { Unary = 16, + Multiplication = 12, + Addition = 11, Relation = 9, Equality = 8, And = 4, @@ -204,6 +213,12 @@ namespace OpenRA.Support case TokenType.Not: yield return new TokenTypeInfo("!", Precedence.Unary, OperandSides.Right, Associativity.Right); continue; + case TokenType.OnesComplement: + yield return new TokenTypeInfo("~", Precedence.Unary, OperandSides.Right, Associativity.Right); + continue; + case TokenType.Negate: + yield return new TokenTypeInfo("-", Precedence.Unary, OperandSides.Right, Associativity.Right); + continue; case TokenType.And: yield return new TokenTypeInfo("&&", Precedence.And, OperandSides.Both); continue; @@ -228,6 +243,21 @@ namespace OpenRA.Support case TokenType.GreaterThanOrEqual: yield return new TokenTypeInfo(">=", Precedence.Relation, OperandSides.Both); continue; + case TokenType.Add: + yield return new TokenTypeInfo("+", Precedence.Addition, OperandSides.Both); + continue; + case TokenType.Subtract: + yield return new TokenTypeInfo("-", Precedence.Addition, OperandSides.Both); + continue; + case TokenType.Multiply: + yield return new TokenTypeInfo("*", Precedence.Multiplication, OperandSides.Both); + continue; + case TokenType.Divide: + yield return new TokenTypeInfo("/", Precedence.Multiplication, OperandSides.Both); + continue; + case TokenType.Modulo: + yield return new TokenTypeInfo("%", Precedence.Multiplication, OperandSides.Both); + continue; } throw new InvalidProgramException("CreateTokenTypeInfoEnumeration is missing a TokenTypeInfo entry for TokenType.{0}".F( @@ -237,6 +267,16 @@ namespace OpenRA.Support static readonly TokenTypeInfo[] TokenTypeInfos = CreateTokenTypeInfoEnumeration().ToArray(); + static bool HasRightOperand(TokenType type) + { + return ((int)TokenTypeInfos[(int)type].OperandSides & (int)OperandSides.Right) != 0; + } + + static bool IsLeftOperandOrNone(TokenType type) + { + return type == TokenType.Invalid || HasRightOperand(type); + } + class Token { public readonly TokenType Type; @@ -259,7 +299,34 @@ namespace OpenRA.Support Index = index; } - public static TokenType GetNextType(string expression, ref int i) + static bool ScanIsNumber(string expression, int start, ref int i) + { + var cc = CharClassOf(expression[i]); + + // Scan forwards until we find an non-digit character + if (cc == CharClass.Digit) + { + i++; + for (; i < expression.Length; i++) + { + cc = CharClassOf(expression[i]); + if (cc != CharClass.Digit) + { + if (cc != CharClass.Whitespace && cc != CharClass.Operator) + throw new InvalidDataException("Number {0} and variable merged at index {1}".F( + int.Parse(expression.Substring(start, i - start)), start)); + + return true; + } + } + + return true; + } + + return false; + } + + public static TokenType GetNextType(string expression, ref int i, TokenType lastType = TokenType.Invalid) { var start = i; @@ -332,30 +399,41 @@ namespace OpenRA.Support case ')': i++; return TokenType.CloseParen; + + case '~': + i++; + return TokenType.OnesComplement; + case '+': + i++; + return TokenType.Add; + + case '-': + if (++i < expression.Length && ScanIsNumber(expression, start, ref i)) + return TokenType.Number; + + i = start + 1; + if (IsLeftOperandOrNone(lastType)) + return TokenType.Negate; + return TokenType.Subtract; + + case '*': + i++; + return TokenType.Multiply; + + case '/': + i++; + return TokenType.Divide; + + case '%': + i++; + return TokenType.Modulo; } + if (ScanIsNumber(expression, start, ref i)) + return TokenType.Number; + var cc = CharClassOf(expression[start]); - // Scan forwards until we find an non-digit character - if (expression[start] == '-' || cc == CharClass.Digit) - { - i++; - for (; i < expression.Length; i++) - { - cc = CharClassOf(expression[i]); - if (cc != CharClass.Digit) - { - if (cc != CharClass.Whitespace && cc != CharClass.Operator) - throw new InvalidDataException("Number {0} and variable merged at index {1}".F( - int.Parse(expression.Substring(start, i - start)), start)); - - return TokenType.Number; - } - } - - return TokenType.Number; - } - if (cc != CharClass.Id) throw new InvalidDataException("Invalid character '{0}' at index {1}".F(expression[i], start)); @@ -370,7 +448,7 @@ namespace OpenRA.Support return TokenType.Variable; } - public static Token GetNext(string expression, ref int i) + public static Token GetNext(string expression, ref int i, TokenType lastType = TokenType.Invalid) { if (i == expression.Length) return null; @@ -384,7 +462,7 @@ namespace OpenRA.Support var start = i; - var type = GetNextType(expression, ref i); + var type = GetNextType(expression, ref i, lastType); switch (type) { case TokenType.Number: @@ -431,7 +509,7 @@ namespace OpenRA.Support Token lastToken = null; for (var i = 0;;) { - var token = Token.GetNext(expression, ref i); + var token = Token.GetNext(expression, ref i, lastToken != null ? lastToken.Type : TokenType.Invalid); if (token == null) { // Sanity check parsed tree @@ -662,6 +740,18 @@ namespace OpenRA.Support continue; } + case TokenType.Negate: + { + ast.Push(Expressions.Expression.Negate(ast.Pop(ExpressionType.Int))); + continue; + } + + case TokenType.OnesComplement: + { + ast.Push(Expressions.Expression.OnesComplement(ast.Pop(ExpressionType.Int))); + continue; + } + case TokenType.LessThan: { var y = ast.Pop(ExpressionType.Int); @@ -694,6 +784,50 @@ namespace OpenRA.Support continue; } + case TokenType.Add: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.Add(x, y)); + continue; + } + + case TokenType.Subtract: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.Subtract(x, y)); + continue; + } + + case TokenType.Multiply: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.Multiply(x, y)); + continue; + } + + case TokenType.Divide: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + var isNotZero = Expressions.Expression.NotEqual(y, Zero); + var divide = Expressions.Expression.Divide(x, y); + ast.Push(IfThenElse(isNotZero, divide, Zero)); + continue; + } + + case TokenType.Modulo: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + var isNotZero = Expressions.Expression.NotEqual(y, Zero); + var modulo = Expressions.Expression.Modulo(x, y); + ast.Push(IfThenElse(isNotZero, modulo, Zero)); + continue; + } + case TokenType.Number: { ast.Push(Expressions.Expression.Constant(((NumberToken)t).Value)); diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index 78d6af181d..1cb61887cb 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -223,6 +223,58 @@ namespace OpenRA.Test AssertFalse("((false))"); } + [TestCase(TestName = "Arithmetic")] + public void TestArithmetic() + { + AssertValue("~0", ~0); + AssertValue("-0", 0); + AssertValue("-a", 0); + AssertValue("-true", -1); + AssertValue("~-0", -1); + AssertValue("2 + 3", 5); + AssertValue("2 + 0", 2); + AssertValue("2 + 3", 5); + AssertValue("5 - 3", 2); + AssertValue("5 - -3", 8); + AssertValue("5 - 0", 5); + AssertValue("2 * 3", 6); + AssertValue("2 * 0", 0); + AssertValue("2 * -3", -6); + AssertValue("-2 * 3", -6); + AssertValue("-2 * -3", 6); + AssertValue("6 / 3", 2); + AssertValue("7 / 3", 2); + AssertValue("-6 / 3", -2); + AssertValue("6 / -3", -2); + AssertValue("-6 / -3", 2); + AssertValue("8 / 3", 2); + AssertValue("6 % 3", 0); + AssertValue("7 % 3", 1); + AssertValue("8 % 3", 2); + AssertValue("7 % 0", 0); + AssertValue("-7 % 3", -1); + AssertValue("7 % -3", 1); + AssertValue("-7 % -3", -1); + AssertValue("8 / 0", 0); + } + + [TestCase(TestName = "Arithmetic Mixed")] + public void TestArithmeticMixed() + { + AssertValue("~~0", 0); + AssertValue("-~0", 1); + AssertValue("~- 0", -1); + AssertValue("2 * 3 + 4", 10); + AssertValue("2 * 3 - 4", 2); + AssertValue("2 + 3 * 4", 14); + AssertValue("2 + 3 % 4", 5); + AssertValue("2 + 3 / 4", 2); + AssertValue("2 * 3 / 4", 1); + AssertValue("8 / 2 == 4", 1); + AssertValue("~2 + ~3", -7); + AssertValue("~(~2 + ~3)", 6); + } + [TestCase(TestName = "Parenthesis and mixed operations")] public void TestMixedParens() { @@ -259,6 +311,9 @@ namespace OpenRA.Test 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"); + AssertParseFailure("1 <", "Missing value or sub-expression at end for `<` operator"); + AssertParseFailure("-1a", "Number -1 and variable merged at index 0"); + AssertParseFailure("-", "Missing value or sub-expression at end for `-` operator"); } [TestCase(TestName = "Undefined symbols are treated as `false` (0) values")]