From d83dae5587c7b4e193ad7a08e7e2c1377e518dd1 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sun, 5 Feb 2017 20:25:34 -0600 Subject: [PATCH 01/17] Rename BooleanExpression => ConditionExpression --- OpenRA.Game/FieldLoader.cs | 4 ++-- OpenRA.Game/OpenRA.Game.csproj | 2 +- .../{BooleanExpression.cs => ConditionExpression.cs} | 4 ++-- OpenRA.Mods.Common/Lint/LintExts.cs | 8 ++++---- OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs | 2 +- ...ooleanExpressionTest.cs => ConditionExpressionTest.cs} | 8 ++++---- OpenRA.Test/OpenRA.Test.csproj | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) rename OpenRA.Game/Support/{BooleanExpression.cs => ConditionExpression.cs} (99%) rename OpenRA.Test/OpenRA.Game/{BooleanExpressionTest.cs => ConditionExpressionTest.cs} (91%) diff --git a/OpenRA.Game/FieldLoader.cs b/OpenRA.Game/FieldLoader.cs index fad93c4462..6a725c02c0 100644 --- a/OpenRA.Game/FieldLoader.cs +++ b/OpenRA.Game/FieldLoader.cs @@ -398,13 +398,13 @@ namespace OpenRA return InvalidValueAction(value, fieldType, fieldName); } - else if (fieldType == typeof(BooleanExpression)) + else if (fieldType == typeof(ConditionExpression)) { if (value != null) { try { - return new BooleanExpression(value); + return new ConditionExpression(value); } catch (InvalidDataException e) { diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 54e5e3518d..30a49ca89d 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -241,7 +241,7 @@ - + diff --git a/OpenRA.Game/Support/BooleanExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs similarity index 99% rename from OpenRA.Game/Support/BooleanExpression.cs rename to OpenRA.Game/Support/ConditionExpression.cs index bdd4ca785c..984b5c4ee9 100644 --- a/OpenRA.Game/Support/BooleanExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -16,7 +16,7 @@ using System.Linq; namespace OpenRA.Support { - public class BooleanExpression + public class ConditionExpression { public readonly string Expression; readonly HashSet variables = new HashSet(); @@ -67,7 +67,7 @@ namespace OpenRA.Support class NotEqualsToken : BinaryOperationToken { public NotEqualsToken(int index) : base("!=", index) { } } class NotToken : UnaryOperationToken { public NotToken(int index) : base("!", index) { } } - public BooleanExpression(string expression) + public ConditionExpression(string expression) { Expression = expression; var openParens = 0; diff --git a/OpenRA.Mods.Common/Lint/LintExts.cs b/OpenRA.Mods.Common/Lint/LintExts.cs index 7267c7f539..a8b1ee57fa 100644 --- a/OpenRA.Mods.Common/Lint/LintExts.cs +++ b/OpenRA.Mods.Common/Lint/LintExts.cs @@ -29,9 +29,9 @@ namespace OpenRA.Mods.Common.Lint if (typeof(IEnumerable).IsAssignableFrom(type)) return fieldInfo.GetValue(ruleInfo) as IEnumerable; - if (type == typeof(BooleanExpression)) + if (type == typeof(ConditionExpression)) { - var expr = (BooleanExpression)fieldInfo.GetValue(ruleInfo); + var expr = (ConditionExpression)fieldInfo.GetValue(ruleInfo); return expr != null ? expr.Variables : Enumerable.Empty(); } @@ -48,9 +48,9 @@ namespace OpenRA.Mods.Common.Lint if (typeof(IEnumerable).IsAssignableFrom(type)) return (IEnumerable)propertyInfo.GetValue(ruleInfo); - if (type == typeof(BooleanExpression)) + if (type == typeof(ConditionExpression)) { - var expr = (BooleanExpression)propertyInfo.GetValue(ruleInfo); + var expr = (ConditionExpression)propertyInfo.GetValue(ruleInfo); return expr != null ? expr.Variables : Enumerable.Empty(); } diff --git a/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs b/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs index 7a848d3991..c2315f99b4 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Traits [ConsumedConditionReference] [Desc("Boolean expression defining the condition to enable this trait.")] - public readonly BooleanExpression RequiresCondition = null; + public readonly ConditionExpression RequiresCondition = null; public abstract object Create(ActorInitializer init); diff --git a/OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs similarity index 91% rename from OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs rename to OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index 25fb284f7b..c3063c07b2 100644 --- a/OpenRA.Test/OpenRA.Game/BooleanExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -19,7 +19,7 @@ using OpenRA.Support; namespace OpenRA.Test { [TestFixture] - public class BooleanExpressionTest + public class ConditionExpressionTest { IReadOnlyDictionary testValues = new ReadOnlyDictionary(new Dictionary() { @@ -29,17 +29,17 @@ namespace OpenRA.Test void AssertFalse(string expression) { - Assert.False(new BooleanExpression(expression).Evaluate(testValues), expression); + Assert.False(new ConditionExpression(expression).Evaluate(testValues), expression); } void AssertTrue(string expression) { - Assert.True(new BooleanExpression(expression).Evaluate(testValues), expression); + Assert.True(new ConditionExpression(expression).Evaluate(testValues), expression); } void AssertParseFailure(string expression) { - Assert.Throws(typeof(InvalidDataException), () => new BooleanExpression(expression).Evaluate(testValues), expression); + Assert.Throws(typeof(InvalidDataException), () => new ConditionExpression(expression).Evaluate(testValues), expression); } [TestCase(TestName = "AND operation")] diff --git a/OpenRA.Test/OpenRA.Test.csproj b/OpenRA.Test/OpenRA.Test.csproj index 4caa55258f..b9df93b6ab 100644 --- a/OpenRA.Test/OpenRA.Test.csproj +++ b/OpenRA.Test/OpenRA.Test.csproj @@ -55,7 +55,7 @@ - + From 65725efd0487ae8ad247581e9de72db7f73b98f8 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sun, 5 Feb 2017 21:13:50 -0600 Subject: [PATCH 02/17] Make ConditionExpression use counts. --- OpenRA.Game/Support/ConditionExpression.cs | 22 +++++++++---------- .../Traits/Conditions/ConditionManager.cs | 12 +++++----- .../Traits/Conditions/ConditionalTrait.cs | 8 +++---- OpenRA.Mods.Common/TraitsInterfaces.cs | 2 +- .../OpenRA.Game/ConditionExpressionTest.cs | 14 ++++++------ 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 984b5c4ee9..bf406ead9c 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -222,21 +222,21 @@ namespace OpenRA.Support return new VariableToken(start, expression.Substring(start)); } - static bool ParseSymbol(VariableToken t, IReadOnlyDictionary symbols) + static int ParseSymbol(VariableToken t, IReadOnlyDictionary symbols) { - bool value; + int value; symbols.TryGetValue(t.Symbol, out value); return value; } - static void ApplyBinaryOperation(Stack s, Func f) + static void ApplyBinaryOperation(Stack s, Func f) { var x = s.Pop(); var y = s.Pop(); s.Push(f(x, y)); } - static void ApplyUnaryOperation(Stack s, Func f) + static void ApplyUnaryOperation(Stack s, Func f) { var x = s.Pop(); s.Push(f(x)); @@ -271,21 +271,21 @@ namespace OpenRA.Support yield return s.Pop(); } - public bool Evaluate(IReadOnlyDictionary symbols) + public int Evaluate(IReadOnlyDictionary symbols) { - var s = new Stack(); + var s = new Stack(); foreach (var t in postfix) { if (t is AndToken) - ApplyBinaryOperation(s, (x, y) => y & x); + ApplyBinaryOperation(s, (x, y) => y > 0 ? x : y); else if (t is NotEqualsToken) - ApplyBinaryOperation(s, (x, y) => y ^ x); + ApplyBinaryOperation(s, (x, y) => (y != x) ? 1 : 0); else if (t is OrToken) - ApplyBinaryOperation(s, (x, y) => y | x); + ApplyBinaryOperation(s, (x, y) => y > 0 ? y : x); else if (t is EqualsToken) - ApplyBinaryOperation(s, (x, y) => y == x); + ApplyBinaryOperation(s, (x, y) => (y == x) ? 1 : 0); else if (t is NotToken) - ApplyUnaryOperation(s, x => !x); + ApplyUnaryOperation(s, x => (x > 0) ? 0 : 1); else if (t is VariableToken) s.Push(ParseSymbol((VariableToken)t, symbols)); } diff --git a/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs b/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs index b70f5460ae..d626c183c8 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs @@ -72,15 +72,15 @@ namespace OpenRA.Mods.Common.Traits int nextToken = 1; /// Cache of condition -> enabled state for quick evaluation of boolean conditions. - readonly Dictionary conditionCache = new Dictionary(); + readonly Dictionary conditionCache = new Dictionary(); /// Read-only version of conditionCache that is passed to IConditionConsumers. - IReadOnlyDictionary readOnlyConditionCache; + IReadOnlyDictionary readOnlyConditionCache; void INotifyCreated.Created(Actor self) { state = new Dictionary(); - readOnlyConditionCache = new ReadOnlyDictionary(conditionCache); + readOnlyConditionCache = new ReadOnlyDictionary(conditionCache); var allConsumers = new HashSet(); var allWatchers = self.TraitsImplementing().ToList(); @@ -96,7 +96,7 @@ namespace OpenRA.Mods.Common.Traits if (w.Condition == condition) cs.Watchers.Add(w); - conditionCache[condition] = false; + conditionCache[condition] = 0; } } @@ -108,7 +108,7 @@ namespace OpenRA.Mods.Common.Traits continue; conditionState.Tokens.Add(kv.Key); - conditionCache[kv.Value] = conditionState.Tokens.Count > 0; + conditionCache[kv.Value] = conditionState.Tokens.Count; } foreach (var sc in self.Info.TraitInfos()) @@ -133,7 +133,7 @@ namespace OpenRA.Mods.Common.Traits else conditionState.Tokens.Add(token); - conditionCache[condition] = conditionState.Tokens.Count > 0; + conditionCache[condition] = conditionState.Tokens.Count; foreach (var t in conditionState.Consumers) t.ConditionsChanged(self, readOnlyConditionCache); diff --git a/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs b/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs index c2315f99b4..3f197345b0 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/ConditionalTrait.cs @@ -19,7 +19,7 @@ namespace OpenRA.Mods.Common.Traits /// Use as base class for *Info to subclass of UpgradableTrait. (See UpgradableTrait.) public abstract class ConditionalTraitInfo : IConditionConsumerInfo, IRulesetLoaded { - static readonly IReadOnlyDictionary NoConditions = new ReadOnlyDictionary(new Dictionary()); + static readonly IReadOnlyDictionary NoConditions = new ReadOnlyDictionary(new Dictionary()); [ConsumedConditionReference] [Desc("Boolean expression defining the condition to enable this trait.")] @@ -34,7 +34,7 @@ namespace OpenRA.Mods.Common.Traits public virtual void RulesetLoaded(Ruleset rules, ActorInfo ai) { - EnabledByDefault = RequiresCondition != null ? RequiresCondition.Evaluate(NoConditions) : true; + EnabledByDefault = RequiresCondition != null ? RequiresCondition.Evaluate(NoConditions) > 0 : true; } } @@ -77,13 +77,13 @@ namespace OpenRA.Mods.Common.Traits void INotifyCreated.Created(Actor self) { Created(self); } - void IConditionConsumer.ConditionsChanged(Actor self, IReadOnlyDictionary conditions) + void IConditionConsumer.ConditionsChanged(Actor self, IReadOnlyDictionary conditions) { if (Info.RequiresCondition == null) return; var wasDisabled = IsTraitDisabled; - IsTraitDisabled = !Info.RequiresCondition.Evaluate(conditions); + IsTraitDisabled = Info.RequiresCondition.Evaluate(conditions) <= 0; if (IsTraitDisabled != wasDisabled) { diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 14cc4bfb93..175dedf20c 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -110,7 +110,7 @@ namespace OpenRA.Mods.Common.Traits public interface IConditionConsumer { IEnumerable Conditions { get; } - void ConditionsChanged(Actor self, IReadOnlyDictionary conditions); + void ConditionsChanged(Actor self, IReadOnlyDictionary conditions); } public interface INotifyHarvesterAction diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index c3063c07b2..3dccfb5489 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -21,20 +21,20 @@ namespace OpenRA.Test [TestFixture] public class ConditionExpressionTest { - IReadOnlyDictionary testValues = new ReadOnlyDictionary(new Dictionary() + IReadOnlyDictionary testValues = new ReadOnlyDictionary(new Dictionary() { - { "true", true }, - { "false", false } + { "true", 1 }, + { "false", 0 } }); void AssertFalse(string expression) { - Assert.False(new ConditionExpression(expression).Evaluate(testValues), expression); + Assert.False(new ConditionExpression(expression).Evaluate(testValues) > 0, expression); } void AssertTrue(string expression) { - Assert.True(new ConditionExpression(expression).Evaluate(testValues), expression); + Assert.True(new ConditionExpression(expression).Evaluate(testValues) > 0, expression); } void AssertParseFailure(string expression) @@ -69,7 +69,7 @@ namespace OpenRA.Test AssertFalse("false == true"); } - [TestCase(TestName = "Not-equals (XOR) operation")] + [TestCase(TestName = "Not-equals operation")] public void TestNotEquals() { AssertFalse("true != true"); @@ -141,7 +141,7 @@ namespace OpenRA.Test AssertParseFailure("false ||"); } - [TestCase(TestName = "Undefined symbols are treated as `false` values")] + [TestCase(TestName = "Undefined symbols are treated as `false` (0) values")] public void TestUndefinedSymbols() { AssertFalse("undef1 || undef2"); From 73895d07e25a5f63d0cb292036eb24802fd4e900 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Mon, 6 Feb 2017 12:13:00 -0600 Subject: [PATCH 03/17] Numeric constants for ConditionExpression --- OpenRA.Game/Support/ConditionExpression.cs | 46 +++++++++++++--- .../OpenRA.Game/ConditionExpressionTest.cs | 54 +++++++++++++++++-- mods/cnc/rules/defaults.yaml | 47 ++++++++-------- mods/d2k/rules/defaults.yaml | 47 ++++++++-------- mods/ra/maps/soviet-03/rules.yaml | 9 ++-- mods/ra/rules/defaults.yaml | 47 ++++++++-------- mods/ts/rules/defaults.yaml | 20 ++++--- 7 files changed, 178 insertions(+), 92 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index bf406ead9c..90cbc52a0b 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -61,6 +61,13 @@ namespace OpenRA.Support : base(symbol, index, Associativity.Left, 0) { } } + class NumberToken : Token + { + public readonly int Value; + public NumberToken(int index, string symbol) + : base(symbol, index, Associativity.Left, 0) { Value = int.Parse(symbol); } + } + class AndToken : BinaryOperationToken { public AndToken(int index) : base("&&", index) { } } class OrToken : BinaryOperationToken { public OrToken(int index) : base("||", index) { } } class EqualsToken : BinaryOperationToken { public EqualsToken(int index) : base("==", index) { } } @@ -120,17 +127,17 @@ namespace OpenRA.Support for (var i = 0; i < tokens.Count - 1; i++) { - // Unary tokens must be followed by a variable, another unary token, or an opening parenthesis - if (tokens[i] is UnaryOperationToken && !(tokens[i + 1] is VariableToken || tokens[i + 1] is UnaryOperationToken - || tokens[i + 1] is OpenParenToken)) + // 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) throw new InvalidDataException("Empty parenthesis at index {0}".F(tokens[i].Index)); - // A variable must be followed by a binary operation or by a closing parenthesis - if (tokens[i] is VariableToken && !(tokens[i + 1] is BinaryOperationToken || tokens[i + 1] is CloseParenToken)) + // 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)); } @@ -147,8 +154,8 @@ namespace OpenRA.Support for (var i = 1; i < tokens.Count - 1; i++) { if (tokens[i] is BinaryOperationToken && ( - !(tokens[i - 1] is CloseParenToken || tokens[i - 1] is VariableToken) || - !(tokens[i + 1] is OpenParenToken || tokens[i + 1] is VariableToken || tokens[i + 1] is UnaryOperationToken))) + !(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)); } @@ -206,6 +213,27 @@ namespace OpenRA.Support throw new InvalidDataException("Unexpected character '|' at index {0}".F(start)); } + // Scan forwards until we find an non-digit character + if (c == '-' || char.IsDigit(expression[i])) + { + i++; + for (; i < expression.Length; i++) + { + c = expression[i]; + if (!char.IsDigit(c)) + { + if (!char.IsWhiteSpace(c) && c != '(' && c != ')' && c != '!' && c != '&' && c != '|' && c != '=' && c != '+') + throw new InvalidDataException("Number and variable merged at index {0}".F(start)); + + // Put the bad character back for the next parse attempt + i--; + return new NumberToken(start, expression.Substring(start, i - start + 1)); + } + } + + return new NumberToken(start, expression.Substring(start)); + } + // Scan forwards until we find an invalid name character for (; i < expression.Length; i++) { @@ -255,7 +283,7 @@ namespace OpenRA.Support while (!((temp = s.Pop()) is OpenParenToken)) yield return temp; } - else if (t is VariableToken) + else if (t is VariableToken || t is NumberToken) yield return t; else { @@ -286,6 +314,8 @@ namespace OpenRA.Support ApplyBinaryOperation(s, (x, y) => (y == x) ? 1 : 0); else if (t is NotToken) ApplyUnaryOperation(s, x => (x > 0) ? 0 : 1); + else if (t is NumberToken) + s.Push(((NumberToken)t).Value); else if (t is VariableToken) s.Push(ParseSymbol((VariableToken)t, symbols)); } diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index 3dccfb5489..94a8c06e1b 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -37,11 +37,27 @@ namespace OpenRA.Test Assert.True(new ConditionExpression(expression).Evaluate(testValues) > 0, expression); } + void AssertValue(string expression, int value) + { + Assert.AreEqual(value, new ConditionExpression(expression).Evaluate(testValues), expression); + } + void AssertParseFailure(string expression) { Assert.Throws(typeof(InvalidDataException), () => new ConditionExpression(expression).Evaluate(testValues), expression); } + [TestCase(TestName = "Numbers")] + public void TestNumbers() + { + AssertParseFailure("1a"); + AssertValue("0", 0); + AssertValue("1", 1); + AssertValue("12", 12); + AssertValue("-1", -1); + AssertValue("-12", -12); + } + [TestCase(TestName = "AND operation")] public void TestAnd() { @@ -49,6 +65,10 @@ namespace OpenRA.Test AssertFalse("false && false"); AssertFalse("true && false"); AssertFalse("false && true"); + AssertValue("2 && false", 0); + AssertValue("false && 2", 0); + AssertValue("3 && 2", 2); + AssertValue("2 && 3", 3); } [TestCase(TestName = "OR operation")] @@ -58,6 +78,10 @@ namespace OpenRA.Test AssertFalse("false || false"); AssertTrue("true || false"); AssertTrue("false || true"); + AssertValue("2 || false", 2); + AssertValue("false || 2", 2); + AssertValue("3 || 2", 3); + AssertValue("2 || 3", 2); } [TestCase(TestName = "Equals operation")] @@ -67,6 +91,15 @@ namespace OpenRA.Test AssertTrue("false == false"); AssertFalse("true == false"); AssertFalse("false == true"); + AssertTrue("1 == 1"); + AssertTrue("0 == 0"); + AssertFalse("1 == 0"); + AssertTrue("1 == true"); + AssertFalse("1 == false"); + AssertTrue("0 == false"); + AssertFalse("0 == true"); + AssertValue("12 == 12", 1); + AssertValue("1 == 12", 0); } [TestCase(TestName = "Not-equals operation")] @@ -76,15 +109,26 @@ namespace OpenRA.Test AssertFalse("false != false"); AssertTrue("true != false"); AssertTrue("false != true"); + AssertValue("1 != 2", 1); + AssertValue("1 != 1", 0); + AssertFalse("1 != true"); + AssertFalse("0 != false"); + AssertTrue("1 != false"); + AssertTrue("0 != true"); } [TestCase(TestName = "NOT operation")] public void TestNOT() { - AssertFalse("!true"); - AssertTrue("!false"); - AssertTrue("!!true"); - AssertFalse("!!false"); + AssertValue("!true", 0); + AssertValue("!false", 1); + AssertValue("!!true", 1); + AssertValue("!!false", 0); + AssertValue("!0", 1); + AssertValue("!1", 0); + AssertValue("!5", 0); + AssertValue("!!5", 1); + AssertValue("!-5", 1); } [TestCase(TestName = "Precedence")] @@ -134,7 +178,7 @@ namespace OpenRA.Test AssertParseFailure("true false"); AssertParseFailure("true & false"); AssertParseFailure("true | false"); - AssertParseFailure("true / false"); + AssertParseFailure("true : false"); AssertParseFailure("true & false && !"); AssertParseFailure("(true && !)"); AssertParseFailure("&& false"); diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 60eee515ef..622e799649 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -17,66 +17,69 @@ ^GainsExperience: GainsExperience: Conditions: - 200: rank-veteran-1 - 400: rank-veteran-2 - 800: rank-veteran-3 - 1600: rank-elite + 200: rank-veteran + 400: rank-veteran + 800: rank-veteran + 1600: rank-veteran + GrantCondition@RANK-ELITE: + RequiresCondition: rank-veteran == 4 + Condition: rank-elite DamageMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 95 DamageMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 90 DamageMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 85 DamageMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 75 FirepowerMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 105 FirepowerMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 110 FirepowerMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 120 FirepowerMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 130 SpeedMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 105 SpeedMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 110 SpeedMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 120 SpeedMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 140 ReloadDelayMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 95 ReloadDelayMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 90 ReloadDelayMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 85 ReloadDelayMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 75 InaccuracyMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 90 InaccuracyMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 80 InaccuracyMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 70 InaccuracyMultiplier@RANK-ELITE: RequiresCondition: rank-elite @@ -92,21 +95,21 @@ Sequence: rank-veteran-1 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 ZOffset: 256 WithDecoration@RANK-2: Image: rank Sequence: rank-veteran-2 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 ZOffset: 256 WithDecoration@RANK-3: Image: rank Sequence: rank-veteran-3 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 ZOffset: 256 WithDecoration@RANK-ELITE: Image: rank diff --git a/mods/d2k/rules/defaults.yaml b/mods/d2k/rules/defaults.yaml index 01ce63a580..1febaaa6f7 100644 --- a/mods/d2k/rules/defaults.yaml +++ b/mods/d2k/rules/defaults.yaml @@ -17,66 +17,69 @@ ^GainsExperience: GainsExperience: Conditions: - 200: rank-veteran-1 - 400: rank-veteran-2 - 800: rank-veteran-3 - 1600: rank-elite + 200: rank-veteran + 400: rank-veteran + 800: rank-veteran + 1600: rank-veteran + GrantCondition@RANK-ELITE: + RequiresCondition: rank-veteran == 4 + Condition: rank-elite DamageMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 96 DamageMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 92 DamageMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 88 DamageMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 80 FirepowerMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 105 FirepowerMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 110 FirepowerMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 115 FirepowerMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 125 SpeedMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 105 SpeedMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 110 SpeedMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 115 SpeedMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 125 ReloadDelayMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 96 ReloadDelayMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 92 ReloadDelayMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 88 ReloadDelayMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 80 InaccuracyMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 90 InaccuracyMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 80 InaccuracyMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 70 InaccuracyMultiplier@RANK-ELITE: RequiresCondition: rank-elite @@ -93,21 +96,21 @@ Sequence: rank-veteran-1 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 ZOffset: 256 WithDecoration@RANK-2: Image: rank Sequence: rank-veteran-2 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 ZOffset: 256 WithDecoration@RANK-3: Image: rank Sequence: rank-veteran-3 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 ZOffset: 256 WithDecoration@RANK-ELITE: Image: rank diff --git a/mods/ra/maps/soviet-03/rules.yaml b/mods/ra/maps/soviet-03/rules.yaml index 13514e9f62..84073817b3 100644 --- a/mods/ra/maps/soviet-03/rules.yaml +++ b/mods/ra/maps/soviet-03/rules.yaml @@ -59,14 +59,11 @@ V05: DOG: # HACK: Disable experience without killing the linter -GainsExperience: - ExternalCondition@RANK-VETERAN-1: - Condition: rank-veteran-1 - ExternalCondition@RANK-VETERAN-2: - Condition: rank-veteran-2 - ExternalCondition@RANK-VETERAN-3: - Condition: rank-veteran-3 + ExternalCondition@RANK-VETERAN: + Condition: rank-veteran ExternalCondition@RANK-ELITE: Condition: rank-elite + -GrantCondition@RANK-ELITE: SPY: Mobile: diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml index 42ff6e71ec..87595c442c 100644 --- a/mods/ra/rules/defaults.yaml +++ b/mods/ra/rules/defaults.yaml @@ -16,66 +16,69 @@ ^GainsExperience: GainsExperience: Conditions: - 200: rank-veteran-1 - 400: rank-veteran-2 - 800: rank-veteran-3 - 1600: rank-elite + 200: rank-veteran + 400: rank-veteran + 800: rank-veteran + 1600: rank-veteran + GrantCondition@RANK-ELITE: + RequiresCondition: rank-veteran == 4 + Condition: rank-elite DamageMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 95 DamageMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 90 DamageMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 85 DamageMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 75 FirepowerMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 105 FirepowerMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 110 FirepowerMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 120 FirepowerMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 130 SpeedMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 105 SpeedMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 110 SpeedMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 120 SpeedMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 140 ReloadDelayMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 95 ReloadDelayMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 90 ReloadDelayMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 85 ReloadDelayMultiplier@RANK-ELITE: RequiresCondition: rank-elite Modifier: 75 InaccuracyMultiplier@RANK-1: - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 Modifier: 90 InaccuracyMultiplier@RANK-2: - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 Modifier: 80 InaccuracyMultiplier@RANK-3: - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 Modifier: 70 InaccuracyMultiplier@RANK-ELITE: RequiresCondition: rank-elite @@ -91,21 +94,21 @@ Sequence: rank-veteran-1 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-1 && !rank-veteran-2 + RequiresCondition: rank-veteran == 1 ZOffset: 256 WithDecoration@RANK-2: Image: rank Sequence: rank-veteran-2 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-2 && !rank-veteran-3 + RequiresCondition: rank-veteran == 2 ZOffset: 256 WithDecoration@RANK-3: Image: rank Sequence: rank-veteran-3 Palette: effect ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran-3 && !rank-elite + RequiresCondition: rank-veteran == 3 ZOffset: 256 WithDecoration@RANK-ELITE: Image: rank diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index f9c218adf4..5e65fde983 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -18,28 +18,34 @@ ^GainsExperience: GainsExperience: Conditions: - 500: rank-veteran - 1000: rank-elite + 500: rank + 1000: rank + GrantCondition@RANK-VETERAN: + RequiresCondition: rank == 1 + Condition: rank-veteran + GrantCondition@RANK-ELITE: + RequiresCondition: rank == 2 + Condition: rank-elite FirepowerMultiplier@VETERAN: - RequiresCondition: rank-veteran && !rank-elite + RequiresCondition: rank-veteran Modifier: 110 FirepowerMultiplier@ELITE: RequiresCondition: rank-elite Modifier: 130 DamageMultiplier@VETERAN: - RequiresCondition: rank-veteran && !rank-elite + RequiresCondition: rank-veteran Modifier: 90 DamageMultiplier@ELITE: RequiresCondition: rank-elite Modifier: 75 SpeedMultiplier@VETERAN: - RequiresCondition: rank-veteran && !rank-elite + RequiresCondition: rank-veteran Modifier: 120 SpeedMultiplier@ELITE: RequiresCondition: rank-elite Modifier: 140 ReloadDelayMultiplier@VETERAN: - RequiresCondition: rank-veteran && !rank-elite + RequiresCondition: rank-veteran Modifier: 90 ReloadDelayMultiplier@ELITE: RequiresCondition: rank-elite @@ -55,7 +61,7 @@ Sequence: veteran Palette: ra ReferencePoint: Bottom, Right - RequiresCondition: rank-veteran && !rank-elite + RequiresCondition: rank-veteran ZOffset: 256 WithDecoration@ELITE: Image: rank From c70442b15ed178485feb37a7e1e6fbc4cb54d847 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sat, 11 Feb 2017 23:42:22 -0600 Subject: [PATCH 04/17] Use switch statement for operator tokenization. --- OpenRA.Game/Support/ConditionExpression.cs | 66 ++++++++++++---------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 90cbc52a0b..2a95507f07 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -166,53 +166,57 @@ namespace OpenRA.Support static Token ParseSymbol(string expression, ref int i) { var start = i; - var c = expression[start]; // Parse operators - if (c == '!') + switch (expression[start]) { - if (i < expression.Length - 1 && expression[start + 1] == '=') + case '!': { - i++; - return new NotEqualsToken(start); + if (i < expression.Length - 1 && expression[start + 1] == '=') + { + i++; + return new NotEqualsToken(start); + } + + return new NotToken(start); } - return new NotToken(start); - } - - if (c == '=') - { - if (i < expression.Length - 1 && expression[start + 1] == '=') + case '=': { - i++; - return new EqualsToken(start); + if (i < expression.Length - 1 && expression[start + 1] == '=') + { + i++; + return new EqualsToken(start); + } + + throw new InvalidDataException("Unexpected character '=' at index {0}".F(start)); } - throw new InvalidDataException("Unexpected character '=' at index {0}".F(start)); - } - - if (c == '&') - { - if (i < expression.Length - 1 && expression[start + 1] == '&') + case '&': { - i++; - return new AndToken(start); + if (i < expression.Length - 1 && expression[start + 1] == '&') + { + i++; + return new AndToken(start); + } + + throw new InvalidDataException("Unexpected character '&' at index {0}".F(start)); } - throw new InvalidDataException("Unexpected character '&' at index {0}".F(start)); - } - - if (c == '|') - { - if (i < expression.Length - 1 && expression[start + 1] == '|') + case '|': { - i++; - return new OrToken(start); - } + if (i < expression.Length - 1 && expression[start + 1] == '|') + { + i++; + return new OrToken(start); + } - throw new InvalidDataException("Unexpected character '|' at index {0}".F(start)); + throw new InvalidDataException("Unexpected character '|' at index {0}".F(start)); + } } + var c = expression[start]; + // Scan forwards until we find an non-digit character if (c == '-' || char.IsDigit(expression[i])) { From f9e47cd0ca689d6ad4fe82f0403614a38e84e2ee Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sun, 12 Feb 2017 12:23:53 -0600 Subject: [PATCH 05/17] ConditionExpression: Added CharClass enum for tokenizing --- OpenRA.Game/Support/ConditionExpression.cs | 82 +++++++++++++++++++--- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 2a95507f07..1f80a832e8 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -103,7 +103,7 @@ namespace OpenRA.Support default: { // Ignore whitespace - if (char.IsWhiteSpace(expression[i])) + if (CharClassOf(expression[i]) == CharClass.Whitespace) break; var token = ParseSymbol(expression, ref i); @@ -163,6 +163,69 @@ namespace OpenRA.Support postfix = ToPostfix(tokens).ToArray(); } + enum CharClass { Whitespace, Operator, Mixed, Id, Digit } + + static CharClass CharClassOf(char c) + { + switch (c) + { + case '~': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '+': + case '=': + case '[': + case ']': + case '{': + case '}': + case '|': + case ':': + case ';': + case '\'': + case '"': + case '<': + case '>': + case '?': + case ',': + case '/': + return CharClass.Operator; + + case '.': + case '$': + case '-': + case '@': + return CharClass.Mixed; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return CharClass.Digit; + + // Fast-track normal whitespace + case ' ': + case '\t': + case '\n': + case '\r': + return CharClass.Whitespace; + + // Should other whitespace be tested? + default: + return char.IsWhiteSpace(c) ? CharClass.Whitespace : CharClass.Id; + } + } + static Token ParseSymbol(string expression, ref int i) { var start = i; @@ -215,18 +278,18 @@ namespace OpenRA.Support } } - var c = expression[start]; + var cc = CharClassOf(expression[start]); // Scan forwards until we find an non-digit character - if (c == '-' || char.IsDigit(expression[i])) + if (expression[start] == '-' || cc == CharClass.Digit) { i++; for (; i < expression.Length; i++) { - c = expression[i]; - if (!char.IsDigit(c)) + cc = CharClassOf(expression[i]); + if (cc != CharClass.Digit) { - if (!char.IsWhiteSpace(c) && c != '(' && c != ')' && c != '!' && c != '&' && c != '|' && c != '=' && c != '+') + if (cc != CharClass.Whitespace && cc != CharClass.Operator) throw new InvalidDataException("Number and variable merged at index {0}".F(start)); // Put the bad character back for the next parse attempt @@ -238,11 +301,14 @@ namespace OpenRA.Support return new NumberToken(start, expression.Substring(start)); } + if (cc != CharClass.Id) + throw new InvalidDataException("Invalid character at index {0}".F(start)); + // Scan forwards until we find an invalid name character for (; i < expression.Length; i++) { - c = expression[i]; - if (char.IsWhiteSpace(c) || c == '(' || c == ')' || c == '!' || c == '&' || c == '|' || c == '=') + cc = CharClassOf(expression[i]); + if (cc == CharClass.Whitespace || cc == CharClass.Operator) { // Put the bad character back for the next parse attempt i--; From 828b13a11e133277211b7f71da1c58ddcf09af0b Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sun, 12 Feb 2017 14:09:44 -0600 Subject: [PATCH 06/17] ConditionExpress: added TokenTypeInfo table --- OpenRA.Game/Support/ConditionExpression.cs | 179 ++++++++++++++++++--- 1 file changed, 159 insertions(+), 20 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 1f80a832e8..d2e56c561a 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -25,54 +25,193 @@ namespace OpenRA.Support readonly Token[] postfix; enum Associativity { Left, Right } - class Token + + [Flags] + enum OperandSides + { + // Value type + None = 0, + + // Postfix unary operator and/or group closer + Left = 1, + + // Prefix unary operator and/or group opener + Right = 2, + + // Binary+ operator + Both = Left | Right + } + + enum Grouping { None, Parens } + + enum TokenType + { + Number, + Variable, + OpenParen, + CloseParen, + Not, + And, + Or, + Equals, + NotEquals, + Invalid + } + + enum Precedence + { + Invalid = ~0, + Parens = -1, + Value = 0, + Unary = 1, + Binary = 0 + } + + struct TokenTypeInfo { public readonly string Symbol; - public readonly int Index; - public readonly int Precedence; + public readonly Precedence Precedence; + public readonly OperandSides OperandSides; public readonly Associativity Associativity; + public readonly Grouping Opens; + public readonly Grouping Closes; - public Token(string symbol, int index, Associativity associativity, int precedence) + public TokenTypeInfo(string symbol, Precedence precedence, OperandSides operandSides = OperandSides.None, + Associativity associativity = Associativity.Left, + Grouping opens = Grouping.None, Grouping closes = Grouping.None) { Symbol = symbol; - Index = index; - Associativity = associativity; Precedence = precedence; + OperandSides = operandSides; + Associativity = associativity; + Opens = opens; + Closes = closes; + } + + public TokenTypeInfo(string symbol, Precedence precedence, Grouping opens, Grouping closes = Grouping.None, + Associativity associativity = Associativity.Left) + { + Symbol = symbol; + Precedence = precedence; + OperandSides = opens == Grouping.None ? + (closes == Grouping.None ? OperandSides.None : OperandSides.Left) + : + (closes == Grouping.None ? OperandSides.Right : OperandSides.Both); + Associativity = associativity; + Opens = opens; + Closes = closes; + } + } + + static IEnumerable CreateTokenTypeInfoEnumeration() + { + for (var i = 0; i <= (int)TokenType.Invalid; i++) + { + switch ((TokenType)i) + { + case TokenType.Invalid: + yield return new TokenTypeInfo("()", Precedence.Invalid); + continue; + case TokenType.Number: + yield return new TokenTypeInfo("()", Precedence.Value); + continue; + case TokenType.Variable: + yield return new TokenTypeInfo("()", Precedence.Value); + continue; + case TokenType.OpenParen: + yield return new TokenTypeInfo("(", Precedence.Parens, Grouping.Parens); + continue; + case TokenType.CloseParen: + yield return new TokenTypeInfo(")", Precedence.Parens, Grouping.None, Grouping.Parens); + continue; + case TokenType.Not: + yield return new TokenTypeInfo("!", Precedence.Unary, OperandSides.Right, Associativity.Right); + continue; + case TokenType.And: + yield return new TokenTypeInfo("&&", Precedence.Binary, OperandSides.Both); + continue; + case TokenType.Or: + yield return new TokenTypeInfo("||", Precedence.Binary, OperandSides.Both); + continue; + case TokenType.Equals: + yield return new TokenTypeInfo("==", Precedence.Binary, OperandSides.Both); + continue; + case TokenType.NotEquals: + yield return new TokenTypeInfo("!=", Precedence.Binary, OperandSides.Both); + continue; + } + + throw new InvalidProgramException("CreateTokenTypeInfoEnumeration is missing a TokenTypeInfo entry for TokenType.{0}".F( + Enum.GetValues()[i])); + } + } + + static readonly TokenTypeInfo[] TokenTypeInfos = CreateTokenTypeInfoEnumeration().ToArray(); + + class Token + { + public readonly TokenType Type; + public readonly int Index; + + public virtual string Symbol { get { return TokenTypeInfos[(int)Type].Symbol; } } + + public int Precedence { get { return (int)TokenTypeInfos[(int)Type].Precedence; } } + public OperandSides OperandSides { get { return TokenTypeInfos[(int)Type].OperandSides; } } + public Associativity Associativity { get { return TokenTypeInfos[(int)Type].Associativity; } } + public bool LeftOperand { get { return ((int)TokenTypeInfos[(int)Type].OperandSides & (int)OperandSides.Left) != 0; } } + public bool RightOperand { get { return ((int)TokenTypeInfos[(int)Type].OperandSides & (int)OperandSides.Right) != 0; } } + + public Grouping Opens { get { return TokenTypeInfos[(int)Type].Opens; } } + public Grouping Closes { get { return TokenTypeInfos[(int)Type].Closes; } } + + public Token(TokenType type, int index) + { + Type = type; + Index = index; } } class BinaryOperationToken : Token { - public BinaryOperationToken(string symbol, int index, Associativity associativity = Associativity.Left, int precedence = 0) - : base(symbol, index, associativity, precedence) { } + public BinaryOperationToken(TokenType type, int index) : base(type, index) { } } class UnaryOperationToken : Token { - public UnaryOperationToken(string symbol, int index, Associativity associativity = Associativity.Right, int precedence = 1) - : base(symbol, index, associativity, precedence) { } + public UnaryOperationToken(TokenType type, int index) : base(type, index) { } } - class OpenParenToken : Token { public OpenParenToken(int index) : base("(", index, Associativity.Left, -1) { } } - class CloseParenToken : Token { public CloseParenToken(int index) : base(")", index, Associativity.Left, -1) { } } + 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 VariableToken(int index, string symbol) - : base(symbol, index, Associativity.Left, 0) { } + public readonly string Name; + + public override string Symbol { get { return Name; } } + + public VariableToken(int index, string symbol) : base(TokenType.Variable, index) { Name = symbol; } } class NumberToken : Token { public readonly int Value; + readonly string symbol; + + public override string Symbol { get { return symbol; } } + public NumberToken(int index, string symbol) - : base(symbol, index, Associativity.Left, 0) { Value = int.Parse(symbol); } + : base(TokenType.Number, index) + { + Value = int.Parse(symbol); + this.symbol = symbol; + } } - class AndToken : BinaryOperationToken { public AndToken(int index) : base("&&", index) { } } - class OrToken : BinaryOperationToken { public OrToken(int index) : base("||", index) { } } - class EqualsToken : BinaryOperationToken { public EqualsToken(int index) : base("==", index) { } } - class NotEqualsToken : BinaryOperationToken { public NotEqualsToken(int index) : base("!=", index) { } } - class NotToken : UnaryOperationToken { public NotToken(int index) : base("!", index) { } } + 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) { From 6e393f99cb667a41b51c9e51af0b2abedbcf3102 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sun, 12 Feb 2017 17:58:02 -0600 Subject: [PATCH 07/17] ConditionExpression: Replaced Token sub-classing with TokenTypeInfo data. --- OpenRA.Game/Support/ConditionExpression.cs | 141 ++++++++---------- .../OpenRA.Game/ConditionExpressionTest.cs | 40 +++-- 2 files changed, 88 insertions(+), 93 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index d2e56c561a..5eb5b1cd62 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -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)); + // 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].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 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++) - { - 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)); - } + // 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(); 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(); foreach (var t in postfix) { - if (t is AndToken) - ApplyBinaryOperation(s, (x, y) => y > 0 ? x : y); - else if (t is NotEqualsToken) - ApplyBinaryOperation(s, (x, y) => (y != x) ? 1 : 0); - else if (t is OrToken) - ApplyBinaryOperation(s, (x, y) => y > 0 ? y : x); - else if (t is EqualsToken) - ApplyBinaryOperation(s, (x, y) => (y == x) ? 1 : 0); - else if (t is NotToken) - ApplyUnaryOperation(s, x => (x > 0) ? 0 : 1); - else if (t is NumberToken) - s.Push(((NumberToken)t).Value); - else if (t is VariableToken) - s.Push(ParseSymbol((VariableToken)t, symbols)); + switch (t.Type) + { + case TokenType.And: + ApplyBinaryOperation(s, (x, y) => y > 0 ? x : y); + continue; + case TokenType.NotEquals: + ApplyBinaryOperation(s, (x, y) => (y != x) ? 1 : 0); + continue; + case TokenType.Or: + ApplyBinaryOperation(s, (x, y) => y > 0 ? y : x); + continue; + case TokenType.Equals: + ApplyBinaryOperation(s, (x, y) => (y == x) ? 1 : 0); + continue; + case TokenType.Not: + ApplyUnaryOperation(s, x => (x > 0) ? 0 : 1); + continue; + case TokenType.Number: + s.Push(((NumberToken)t).Value); + 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.GetValues()[(int)t.Type])); + } } return s.Pop(); diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index 94a8c06e1b..1fa1311015 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -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")] From 4537c3c7d0663afb497517b69c7690e480d4ba2c Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Mon, 13 Feb 2017 20:54:38 -0600 Subject: [PATCH 08/17] ConditionExpression: reorganize tokenizer --- OpenRA.Game/Support/ConditionExpression.cs | 201 +++++++++++---------- 1 file changed, 102 insertions(+), 99 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 5eb5b1cd62..fb63d3a489 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -24,6 +24,69 @@ namespace OpenRA.Support readonly Token[] postfix; + enum CharClass { Whitespace, Operator, Mixed, Id, Digit } + + static CharClass CharClassOf(char c) + { + switch (c) + { + case '~': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '+': + case '=': + case '[': + case ']': + case '{': + case '}': + case '|': + case ':': + case ';': + case '\'': + case '"': + case '<': + case '>': + case '?': + case ',': + case '/': + return CharClass.Operator; + + case '.': + case '$': + case '-': + case '@': + return CharClass.Mixed; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return CharClass.Digit; + + // Fast-track normal whitespace + case ' ': + case '\t': + case '\n': + case '\r': + return CharClass.Whitespace; + + // Should other whitespace be tested? + default: + return char.IsWhiteSpace(c) ? CharClass.Whitespace : CharClass.Id; + } + } + enum Associativity { Left, Right } [Flags] @@ -201,41 +264,34 @@ namespace OpenRA.Support var openParens = 0; var closeParens = 0; var tokens = new List(); - for (var i = 0; i < expression.Length; i++) + for (var i = 0; i < expression.Length;) { - switch (expression[i]) + // Ignore whitespace + if (CharClassOf(expression[i]) == CharClass.Whitespace) { - case '(': - { - tokens.Add(new Token(TokenType.OpenParen, i)); + i++; + continue; + } + + var token = ParseSymbol(expression, ref i); + switch (token.Type) + { + case TokenType.OpenParen: openParens++; break; - } - case ')': - { - tokens.Add(new Token(TokenType.CloseParen, i)); + case TokenType.CloseParen: if (++closeParens > openParens) - throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(i)); + throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(i - 1)); break; - } - - default: - { - // Ignore whitespace - if (CharClassOf(expression[i]) == CharClass.Whitespace) - break; - - var token = ParseSymbol(expression, ref i); - tokens.Add(token); - - if (token.Type == TokenType.Variable) - variables.Add(token.Symbol); + case TokenType.Variable: + variables.Add(token.Symbol); break; - } } + + tokens.Add(token); } // Sanity check parsed tree @@ -275,69 +331,6 @@ namespace OpenRA.Support postfix = ToPostfix(tokens).ToArray(); } - enum CharClass { Whitespace, Operator, Mixed, Id, Digit } - - static CharClass CharClassOf(char c) - { - switch (c) - { - case '~': - case '!': - case '%': - case '^': - case '&': - case '*': - case '(': - case ')': - case '+': - case '=': - case '[': - case ']': - case '{': - case '}': - case '|': - case ':': - case ';': - case '\'': - case '"': - case '<': - case '>': - case '?': - case ',': - case '/': - return CharClass.Operator; - - case '.': - case '$': - case '-': - case '@': - return CharClass.Mixed; - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - return CharClass.Digit; - - // Fast-track normal whitespace - case ' ': - case '\t': - case '\n': - case '\r': - return CharClass.Whitespace; - - // Should other whitespace be tested? - default: - return char.IsWhiteSpace(c) ? CharClass.Whitespace : CharClass.Id; - } - } - static Token ParseSymbol(string expression, ref int i) { var start = i; @@ -347,7 +340,8 @@ namespace OpenRA.Support { case '!': { - if (i < expression.Length - 1 && expression[start + 1] == '=') + i++; + if (i < expression.Length && expression[start + 1] == '=') { i++; return new Token(TokenType.NotEquals, start); @@ -358,7 +352,8 @@ namespace OpenRA.Support case '=': { - if (i < expression.Length - 1 && expression[start + 1] == '=') + i++; + if (i < expression.Length && expression[start + 1] == '=') { i++; return new Token(TokenType.Equals, start); @@ -369,7 +364,8 @@ namespace OpenRA.Support case '&': { - if (i < expression.Length - 1 && expression[start + 1] == '&') + i++; + if (i < expression.Length && expression[start + 1] == '&') { i++; return new Token(TokenType.And, start); @@ -380,7 +376,8 @@ namespace OpenRA.Support case '|': { - if (i < expression.Length - 1 && expression[start + 1] == '|') + i++; + if (i < expression.Length && expression[start + 1] == '|') { i++; return new Token(TokenType.Or, start); @@ -388,6 +385,18 @@ namespace OpenRA.Support throw new InvalidDataException("Unexpected character '|' at index {0} - should it be `||`?".F(start)); } + + case '(': + { + i++; + return new Token(TokenType.OpenParen, start); + } + + case ')': + { + i++; + return new Token(TokenType.CloseParen, start); + } } var cc = CharClassOf(expression[start]); @@ -405,9 +414,7 @@ namespace OpenRA.Support 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--; - return new NumberToken(start, expression.Substring(start, i - start + 1)); + return new NumberToken(start, expression.Substring(start, i - start)); } } @@ -422,11 +429,7 @@ namespace OpenRA.Support { cc = CharClassOf(expression[i]); if (cc == CharClass.Whitespace || cc == CharClass.Operator) - { - // Put the bad character back for the next parse attempt - i--; - return new VariableToken(start, expression.Substring(start, i - start + 1)); - } + return new VariableToken(start, expression.Substring(start, i - start)); } // Take the rest of the string From 8e6436d71c49ff2cc0828d8a6dfce3e5c270ad54 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 00:42:03 -0600 Subject: [PATCH 09/17] ConditionExpression: move lexing to Token class. --- OpenRA.Game/Support/ConditionExpression.cs | 241 +++++++++++---------- 1 file changed, 128 insertions(+), 113 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index fb63d3a489..82795dd84a 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -109,8 +109,11 @@ namespace OpenRA.Support enum TokenType { + // varying values Number, Variable, + + // operators OpenParen, CloseParen, Not, @@ -118,6 +121,7 @@ namespace OpenRA.Support Or, Equals, NotEquals, + Invalid } @@ -232,6 +236,126 @@ namespace OpenRA.Support Type = type; Index = index; } + + public static TokenType GetNextType(string expression, ref int i) + { + var start = i; + + switch (expression[i]) + { + case '!': + i++; + if (i < expression.Length && expression[i] == '=') + { + i++; + return TokenType.NotEquals; + } + + return TokenType.Not; + + case '=': + i++; + if (i < expression.Length && expression[i] == '=') + { + i++; + return TokenType.Equals; + } + + throw new InvalidDataException("Unexpected character '=' at index {0} - should it be `==`?".F(start)); + + case '&': + i++; + if (i < expression.Length && expression[i] == '&') + { + i++; + return TokenType.And; + } + + throw new InvalidDataException("Unexpected character '&' at index {0} - should it be `&&`?".F(start)); + + case '|': + i++; + if (i < expression.Length && expression[i] == '|') + { + i++; + return TokenType.Or; + } + + throw new InvalidDataException("Unexpected character '|' at index {0} - should it be `||`?".F(start)); + + case '(': + i++; + return TokenType.OpenParen; + + case ')': + i++; + return TokenType.CloseParen; + } + + 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)); + + // Scan forwards until we find an invalid name character + for (; i < expression.Length; i++) + { + cc = CharClassOf(expression[i]); + if (cc == CharClass.Whitespace || cc == CharClass.Operator) + return TokenType.Variable; + } + + // Take the rest of the string + return TokenType.Variable; + } + + public static Token GetNext(string expression, ref int i) + { + if (i == expression.Length) + return null; + + // Ignore whitespace + while (CharClassOf(expression[i]) == CharClass.Whitespace) + { + if (++i == expression.Length) + return null; + } + + var start = i; + + var type = GetNextType(expression, ref i); + switch (type) + { + case TokenType.Number: + return new NumberToken(start, expression.Substring(start, i - start)); + + case TokenType.Variable: + return new VariableToken(start, expression.Substring(start, i - start)); + + default: + return new Token(type, start); + } + } } class VariableToken : Token @@ -264,16 +388,12 @@ namespace OpenRA.Support var openParens = 0; var closeParens = 0; var tokens = new List(); - for (var i = 0; i < expression.Length;) + for (var i = 0;;) { - // Ignore whitespace - if (CharClassOf(expression[i]) == CharClass.Whitespace) - { - i++; - continue; - } + var token = Token.GetNext(expression, ref i); + if (token == null) + break; - var token = ParseSymbol(expression, ref i); switch (token.Type) { case TokenType.OpenParen: @@ -331,111 +451,6 @@ namespace OpenRA.Support postfix = ToPostfix(tokens).ToArray(); } - static Token ParseSymbol(string expression, ref int i) - { - var start = i; - - // Parse operators - switch (expression[start]) - { - case '!': - { - i++; - if (i < expression.Length && expression[start + 1] == '=') - { - i++; - return new Token(TokenType.NotEquals, start); - } - - return new Token(TokenType.Not, start); - } - - case '=': - { - i++; - if (i < expression.Length && expression[start + 1] == '=') - { - i++; - return new Token(TokenType.Equals, start); - } - - throw new InvalidDataException("Unexpected character '=' at index {0} - should it be `==`?".F(start)); - } - - case '&': - { - i++; - if (i < expression.Length && expression[start + 1] == '&') - { - i++; - return new Token(TokenType.And, start); - } - - throw new InvalidDataException("Unexpected character '&' at index {0} - should it be `&&`?".F(start)); - } - - case '|': - { - i++; - if (i < expression.Length && expression[start + 1] == '|') - { - i++; - return new Token(TokenType.Or, start); - } - - throw new InvalidDataException("Unexpected character '|' at index {0} - should it be `||`?".F(start)); - } - - case '(': - { - i++; - return new Token(TokenType.OpenParen, start); - } - - case ')': - { - i++; - return new Token(TokenType.CloseParen, start); - } - } - - 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 new NumberToken(start, expression.Substring(start, i - start)); - } - } - - return new NumberToken(start, expression.Substring(start)); - } - - if (cc != CharClass.Id) - 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++) - { - cc = CharClassOf(expression[i]); - if (cc == CharClass.Whitespace || cc == CharClass.Operator) - return new VariableToken(start, expression.Substring(start, i - start)); - } - - // Take the rest of the string - return new VariableToken(start, expression.Substring(start)); - } - static int ParseSymbol(VariableToken t, IReadOnlyDictionary symbols) { int value; From d752e1079903f2275daac9f236c9bee72dc641fb Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 06:42:05 -0600 Subject: [PATCH 10/17] ConditionExpression: Run syntax checks while lexing --- OpenRA.Game/Support/ConditionExpression.cs | 98 +++++++++---------- .../OpenRA.Game/ConditionExpressionTest.cs | 6 +- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 82795dd84a..aafb972cce 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -325,7 +325,6 @@ namespace OpenRA.Support return TokenType.Variable; } - // Take the rest of the string return TokenType.Variable; } @@ -385,67 +384,68 @@ namespace OpenRA.Support public ConditionExpression(string expression) { Expression = expression; - var openParens = 0; - var closeParens = 0; var tokens = new List(); + var currentOpeners = new Stack(); + Token lastToken = null; for (var i = 0;;) { var token = Token.GetNext(expression, ref i); if (token == null) - break; - - switch (token.Type) { - case TokenType.OpenParen: - openParens++; - break; + // Sanity check parsed tree + if (lastToken == null) + throw new InvalidDataException("Empty expression"); - case TokenType.CloseParen: - if (++closeParens > openParens) - throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(i - 1)); + // Expressions can't end with a binary or unary prefix operation + if (lastToken.RightOperand) + throw new InvalidDataException("Missing value or sub-expression at end for `{0}` operator".F(lastToken.Symbol)); - break; - - case TokenType.Variable: - variables.Add(token.Symbol); - break; + break; } + if (token.Closes != Grouping.None) + { + if (currentOpeners.Count == 0) + throw new InvalidDataException("Unmatched closing parenthesis at index {0}".F(token.Index)); + + currentOpeners.Pop(); + } + + if (token.Opens != Grouping.None) + currentOpeners.Push(token); + + if (lastToken == null) + { + // Expressions can't start with a binary or unary postfix operation or closer + if (token.LeftOperand) + throw new InvalidDataException("Missing value or sub-expression at beginning for `{0}` operator".F(token.Symbol)); + } + else + { + // Disallow empty parentheses + if (lastToken.Opens != Grouping.None && token.Closes != Grouping.None) + throw new InvalidDataException("Empty parenthesis at index {0}".F(lastToken.Index)); + + // Exactly one of two consective tokens must take the other's sub-expression evaluation as an operand + if (lastToken.RightOperand == token.LeftOperand) + { + if (lastToken.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( + lastToken.Symbol, lastToken.Index, token.Symbol, token.Index)); + throw new InvalidDataException("Missing binary operation before `{0}` at index {1}".F(token.Symbol, token.Index)); + } + } + + if (token.Type == TokenType.Variable) + variables.Add(token.Symbol); + tokens.Add(token); + lastToken = token; } - // Sanity check parsed tree - if (!tokens.Any()) - throw new InvalidDataException("Empty expression"); - - 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++) - { - // Disallow empty parentheses - if (tokens[i].Opens != Grouping.None && tokens[i + 1].Closes != Grouping.None) - throw new InvalidDataException("Empty parenthesis at index {0}".F(tokens[i].Index)); - - // 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].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)); + if (currentOpeners.Count > 0) + throw new InvalidDataException("Unclosed opening parenthesis at index {0}".F(currentOpeners.Peek().Index)); // Convert to postfix (discarding parentheses) ready for evaluation postfix = ToPostfix(tokens).ToArray(); diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index 1fa1311015..0eb9118fbd 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -178,10 +178,12 @@ namespace OpenRA.Test { 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", "Unclosed opening parenthesis at index 0"); 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("(", "Missing value or sub-expression at end for `(` operator"); + AssertParseFailure(")", "Unmatched closing parenthesis at index 0"); 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 `&&`?"); From 0fc2008f10110a738d274801c72c528fc15c2552 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 09:03:49 -0600 Subject: [PATCH 11/17] Added delegate creation & evaluation to ConditionExpression --- OpenRA.Game/Support/ConditionExpression.cs | 210 ++++++++++++++---- .../OpenRA.Game/ConditionExpressionTest.cs | 19 +- 2 files changed, 176 insertions(+), 53 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index aafb972cce..260bd42d3e 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -13,6 +13,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; +using Expressions = System.Linq.Expressions; namespace OpenRA.Support { @@ -22,7 +24,7 @@ namespace OpenRA.Support readonly HashSet variables = new HashSet(); public IEnumerable Variables { get { return variables; } } - readonly Token[] postfix; + readonly Func, int> asFunction; enum CharClass { Whitespace, Operator, Mixed, Id, Digit } @@ -447,30 +449,16 @@ namespace OpenRA.Support if (currentOpeners.Count > 0) throw new InvalidDataException("Unclosed opening parenthesis at index {0}".F(currentOpeners.Peek().Index)); - // Convert to postfix (discarding parentheses) ready for evaluation - postfix = ToPostfix(tokens).ToArray(); + asFunction = new Compiler().Compile(ToPostfix(tokens).ToArray()); } - static int ParseSymbol(VariableToken t, IReadOnlyDictionary symbols) + static int ParseSymbol(string symbol, IReadOnlyDictionary symbols) { int value; - symbols.TryGetValue(t.Symbol, out value); + symbols.TryGetValue(symbol, out value); return value; } - static void ApplyBinaryOperation(Stack s, Func f) - { - var x = s.Pop(); - var y = s.Pop(); - s.Push(f(x, y)); - } - - static void ApplyUnaryOperation(Stack s, Func f) - { - var x = s.Pop(); - s.Push(f(x)); - } - static IEnumerable ToPostfix(IEnumerable tokens) { var s = new Stack(); @@ -500,41 +488,169 @@ namespace OpenRA.Support yield return s.Pop(); } - public int Evaluate(IReadOnlyDictionary symbols) + enum ExpressionType { Int, Bool } + + static readonly ParameterExpression SymbolsParam = + Expressions.Expression.Parameter(typeof(IReadOnlyDictionary), "symbols"); + static readonly ConstantExpression Zero = Expressions.Expression.Constant(0); + static readonly ConstantExpression One = Expressions.Expression.Constant(1); + static readonly ConstantExpression False = Expressions.Expression.Constant(false); + static readonly ConstantExpression True = Expressions.Expression.Constant(true); + + static Expression AsBool(Expression expression) { - var s = new Stack(); - foreach (var t in postfix) + return Expressions.Expression.GreaterThan(expression, Zero); + } + + static Expression AsNegBool(Expression expression) + { + return Expressions.Expression.LessThanOrEqual(expression, Zero); + } + + static Expression IfThenElse(Expression test, Expression ifTrue, Expression ifFalse) + { + return Expressions.Expression.Condition(test, ifTrue, ifFalse); + } + + class AstStack + { + readonly List expressions = new List(); + readonly List types = new List(); + + public ExpressionType PeekType() { return types[types.Count - 1]; } + + public Expression Peek(ExpressionType toType) { - switch (t.Type) + var fromType = types[types.Count - 1]; + var expression = expressions[expressions.Count - 1]; + if (toType == fromType) + return expression; + + switch (toType) { - case TokenType.And: - ApplyBinaryOperation(s, (x, y) => y > 0 ? x : y); - continue; - case TokenType.NotEquals: - ApplyBinaryOperation(s, (x, y) => (y != x) ? 1 : 0); - continue; - case TokenType.Or: - ApplyBinaryOperation(s, (x, y) => y > 0 ? y : x); - continue; - case TokenType.Equals: - ApplyBinaryOperation(s, (x, y) => (y == x) ? 1 : 0); - continue; - case TokenType.Not: - ApplyUnaryOperation(s, x => (x > 0) ? 0 : 1); - continue; - case TokenType.Number: - s.Push(((NumberToken)t).Value); - 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.GetValues()[(int)t.Type])); + case ExpressionType.Bool: + return IfThenElse(AsBool(expression), True, False); + case ExpressionType.Int: + return IfThenElse(expression, One, Zero); } + + throw new InvalidProgramException("Unable to convert ExpressionType.{0} to ExpressionType.{1}".F( + Enum.GetValues()[(int)fromType], Enum.GetValues()[(int)toType])); } - return s.Pop(); + public Expression Pop(ExpressionType type) + { + var expression = Peek(type); + expressions.RemoveAt(expressions.Count - 1); + types.RemoveAt(types.Count - 1); + return expression; + } + + public void Push(Expression expression, ExpressionType type) + { + expressions.Add(expression); + if (type == ExpressionType.Int) + if (expression.Type != typeof(int)) + throw new InvalidOperationException("Expected System.Int type instead of {0} for {1}".F(expression.Type, expression)); + + if (type == ExpressionType.Bool) + if (expression.Type != typeof(bool)) + throw new InvalidOperationException("Expected System.Boolean type instead of {0} for {1}".F(expression.Type, expression)); + types.Add(type); + } + + public void Push(Expression expression) + { + expressions.Add(expression); + if (expression.Type == typeof(int)) + types.Add(ExpressionType.Int); + else if (expression.Type == typeof(bool)) + types.Add(ExpressionType.Bool); + else + throw new InvalidOperationException("Unhandled result type {0} for {1}".F(expression.Type, expression)); + } + } + + class Compiler + { + readonly AstStack ast = new AstStack(); + + public Func, int> Compile(Token[] postfix) + { + foreach (var t in postfix) + { + switch (t.Type) + { + case TokenType.And: + { + var y = ast.Pop(ExpressionType.Bool); + var x = ast.Pop(ExpressionType.Bool); + ast.Push(Expressions.Expression.And(x, y)); + continue; + } + + case TokenType.Or: + { + var y = ast.Pop(ExpressionType.Bool); + var x = ast.Pop(ExpressionType.Bool); + ast.Push(Expressions.Expression.Or(x, y)); + continue; + } + + case TokenType.NotEquals: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.NotEqual(x, y)); + continue; + } + + case TokenType.Equals: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.Equal(x, y)); + continue; + } + + case TokenType.Not: + { + if (ast.PeekType() == ExpressionType.Bool) + ast.Push(Expressions.Expression.Not(ast.Pop(ExpressionType.Bool))); + else + ast.Push(AsNegBool(ast.Pop(ExpressionType.Int))); + continue; + } + + case TokenType.Number: + { + ast.Push(Expressions.Expression.Constant(((NumberToken)t).Value)); + continue; + } + + case TokenType.Variable: + { + var symbol = Expressions.Expression.Constant(((VariableToken)t).Symbol); + Func, int> parseSymbol = ParseSymbol; + ast.Push(Expressions.Expression.Call(parseSymbol.Method, symbol, SymbolsParam)); + continue; + } + + default: + throw new InvalidProgramException( + "ConditionExpression.Compiler.Compile() is missing an expression builder for TokenType.{0}".F( + Enum.GetValues()[(int)t.Type])); + } + } + + return Expressions.Expression.Lambda, int>>( + ast.Pop(ExpressionType.Int), SymbolsParam).Compile(); + } + } + + public int Evaluate(IReadOnlyDictionary symbols) + { + return asFunction(symbols); } } } diff --git a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs index 0eb9118fbd..71394a1050 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -66,6 +66,13 @@ namespace OpenRA.Test AssertValue("-12", -12); } + [TestCase(TestName = "Booleans")] + public void TestBooleans() + { + AssertValue("false", 0); + AssertValue("true", 1); + } + [TestCase(TestName = "AND operation")] public void TestAnd() { @@ -75,8 +82,8 @@ namespace OpenRA.Test AssertFalse("false && true"); AssertValue("2 && false", 0); AssertValue("false && 2", 0); - AssertValue("3 && 2", 2); - AssertValue("2 && 3", 3); + AssertValue("3 && 2", 1); + AssertValue("2 && 3", 1); } [TestCase(TestName = "OR operation")] @@ -86,10 +93,10 @@ namespace OpenRA.Test AssertFalse("false || false"); AssertTrue("true || false"); AssertTrue("false || true"); - AssertValue("2 || false", 2); - AssertValue("false || 2", 2); - AssertValue("3 || 2", 3); - AssertValue("2 || 3", 2); + AssertValue("2 || false", 1); + AssertValue("false || 2", 1); + AssertValue("3 || 2", 1); + AssertValue("2 || 3", 1); } [TestCase(TestName = "Equals operation")] From bbea7642fcc320debfc85ae70887ece92ea59d9a Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 10:45:38 -0600 Subject: [PATCH 12/17] ConditionExpression: setup operator precedences --- OpenRA.Game/Support/ConditionExpression.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 260bd42d3e..be4fb98798 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -129,11 +129,14 @@ namespace OpenRA.Support enum Precedence { - Invalid = ~0, - Parens = -1, + Unary = 16, + Equality = 8, + And = 4, + Or = 3, + Binary = 0, Value = 0, - Unary = 1, - Binary = 0 + Parens = -1, + Invalid = ~0 } struct TokenTypeInfo @@ -197,16 +200,16 @@ namespace OpenRA.Support yield return new TokenTypeInfo("!", Precedence.Unary, OperandSides.Right, Associativity.Right); continue; case TokenType.And: - yield return new TokenTypeInfo("&&", Precedence.Binary, OperandSides.Both); + yield return new TokenTypeInfo("&&", Precedence.And, OperandSides.Both); continue; case TokenType.Or: - yield return new TokenTypeInfo("||", Precedence.Binary, OperandSides.Both); + yield return new TokenTypeInfo("||", Precedence.Or, OperandSides.Both); continue; case TokenType.Equals: - yield return new TokenTypeInfo("==", Precedence.Binary, OperandSides.Both); + yield return new TokenTypeInfo("==", Precedence.Equality, OperandSides.Both); continue; case TokenType.NotEquals: - yield return new TokenTypeInfo("!=", Precedence.Binary, OperandSides.Both); + yield return new TokenTypeInfo("!=", Precedence.Equality, OperandSides.Both); continue; } From ac4f73b178780796b22e14bd606ca09670f630b5 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 17:45:20 -0600 Subject: [PATCH 13/17] ConditionExpression: Add relation operators --- OpenRA.Game/Support/ConditionExpression.cs | 69 +++++++++++++++++++ .../OpenRA.Game/ConditionExpressionTest.cs | 63 ++++++++++++++++- mods/cnc/rules/defaults.yaml | 2 +- mods/d2k/rules/defaults.yaml | 2 +- mods/ra/rules/defaults.yaml | 2 +- mods/ts/rules/defaults.yaml | 2 +- 6 files changed, 134 insertions(+), 6 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index be4fb98798..e027a18df4 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -123,6 +123,10 @@ namespace OpenRA.Support Or, Equals, NotEquals, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, Invalid } @@ -130,6 +134,7 @@ namespace OpenRA.Support enum Precedence { Unary = 16, + Relation = 9, Equality = 8, And = 4, Or = 3, @@ -211,6 +216,18 @@ namespace OpenRA.Support case TokenType.NotEquals: yield return new TokenTypeInfo("!=", Precedence.Equality, OperandSides.Both); continue; + case TokenType.LessThan: + yield return new TokenTypeInfo("<", Precedence.Relation, OperandSides.Both); + continue; + case TokenType.LessThanOrEqual: + yield return new TokenTypeInfo("<=", Precedence.Relation, OperandSides.Both); + continue; + case TokenType.GreaterThan: + yield return new TokenTypeInfo(">", Precedence.Relation, OperandSides.Both); + continue; + case TokenType.GreaterThanOrEqual: + yield return new TokenTypeInfo(">=", Precedence.Relation, OperandSides.Both); + continue; } throw new InvalidProgramException("CreateTokenTypeInfoEnumeration is missing a TokenTypeInfo entry for TokenType.{0}".F( @@ -258,6 +275,26 @@ namespace OpenRA.Support return TokenType.Not; + case '<': + i++; + if (i < expression.Length && expression[i] == '=') + { + i++; + return TokenType.LessThanOrEqual; + } + + return TokenType.LessThan; + + case '>': + i++; + if (i < expression.Length && expression[i] == '=') + { + i++; + return TokenType.GreaterThanOrEqual; + } + + return TokenType.GreaterThan; + case '=': i++; if (i < expression.Length && expression[i] == '=') @@ -625,6 +662,38 @@ namespace OpenRA.Support continue; } + case TokenType.LessThan: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.LessThan(x, y)); + continue; + } + + case TokenType.LessThanOrEqual: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.LessThanOrEqual(x, y)); + continue; + } + + case TokenType.GreaterThan: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.GreaterThan(x, y)); + continue; + } + + case TokenType.GreaterThanOrEqual: + { + var y = ast.Pop(ExpressionType.Int); + var x = ast.Pop(ExpressionType.Int); + ast.Push(Expressions.Expression.GreaterThanOrEqual(x, y)); + 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 71394a1050..78d6af181d 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -146,8 +146,67 @@ namespace OpenRA.Test AssertValue("!-5", 1); } - [TestCase(TestName = "Precedence")] - public void TestPrecedence() + [TestCase(TestName = "Relation operations")] + public void TestRelations() + { + AssertValue("2 < 5", 1); + AssertValue("0 < 5", 1); + AssertValue("5 < 2", 0); + AssertValue("5 < 5", 0); + AssertValue("-5 < 0", 1); + AssertValue("-2 < -5", 0); + AssertValue("-5 < -2", 1); + AssertValue("-5 < -5", 0); + AssertValue("-7 < 5", 1); + AssertValue("0 <= 5", 1); + AssertValue("2 <= 5", 1); + AssertValue("5 <= 2", 0); + AssertValue("5 <= 5", 1); + AssertValue("5 <= 0", 0); + AssertValue("-2 <= -5", 0); + AssertValue("-5 <= -2", 1); + AssertValue("-5 <= -5", 1); + AssertValue("-7 <= 5", 1); + AssertValue("0 <= -5", 0); + AssertValue("-5 <= 0", 1); + AssertValue("5 > 2", 1); + AssertValue("0 > 5", 0); + AssertValue("2 > 5", 0); + AssertValue("5 > 5", 0); + AssertValue("5 > 0", 1); + AssertValue("-2 > -5", 1); + AssertValue("-7 > -5", 0); + AssertValue("-5 > -5", 0); + AssertValue("-4 > -5", 1); + AssertValue("5 >= 0", 1); + AssertValue("0 >= 5", 0); + AssertValue("5 >= 2", 1); + AssertValue("2 >= 5", 0); + AssertValue("5 >= 5", 1); + AssertValue("-5 >= 0", 0); + AssertValue("0 >= -5", 1); + AssertValue("-7 >= 5", 0); + AssertValue("-5 >= -5", 1); + AssertValue("-4 >= -5", 1); + } + + [TestCase(TestName = "Relation Mixed Precedence")] + public void TestRelationMixedPrecedence() + { + AssertValue("5 <= 5 && 2 > 1", 1); + AssertValue("5 > 5 || 2 > 1", 1); + AssertValue("5 > 5 || 1 > 1", 0); + AssertValue("5 <= 5 == 2 > 1", 1); + AssertValue("5 > 5 == 2 > 1", 0); + AssertValue("5 > 5 == 1 > 1", 1); + AssertValue("5 <= 5 != 2 > 1", 0); + AssertValue("5 > 5 != 2 > 1", 1); + AssertValue("5 > 5 != 1 > 1", 0); + AssertValue("5 > 5 != 1 >= 1", 1); + } + + [TestCase(TestName = "AND-OR Precedence")] + public void TestAndOrPrecedence() { AssertTrue("true && false || true"); AssertFalse("false || false && true"); diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 622e799649..cf38e6798c 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -22,7 +22,7 @@ 800: rank-veteran 1600: rank-veteran GrantCondition@RANK-ELITE: - RequiresCondition: rank-veteran == 4 + RequiresCondition: rank-veteran >= 4 Condition: rank-elite DamageMultiplier@RANK-1: RequiresCondition: rank-veteran == 1 diff --git a/mods/d2k/rules/defaults.yaml b/mods/d2k/rules/defaults.yaml index 1febaaa6f7..51ac9d51de 100644 --- a/mods/d2k/rules/defaults.yaml +++ b/mods/d2k/rules/defaults.yaml @@ -22,7 +22,7 @@ 800: rank-veteran 1600: rank-veteran GrantCondition@RANK-ELITE: - RequiresCondition: rank-veteran == 4 + RequiresCondition: rank-veteran >= 4 Condition: rank-elite DamageMultiplier@RANK-1: RequiresCondition: rank-veteran == 1 diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml index 87595c442c..91bf77b37e 100644 --- a/mods/ra/rules/defaults.yaml +++ b/mods/ra/rules/defaults.yaml @@ -21,7 +21,7 @@ 800: rank-veteran 1600: rank-veteran GrantCondition@RANK-ELITE: - RequiresCondition: rank-veteran == 4 + RequiresCondition: rank-veteran >= 4 Condition: rank-elite DamageMultiplier@RANK-1: RequiresCondition: rank-veteran == 1 diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index 5e65fde983..92bdfbaea9 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -24,7 +24,7 @@ RequiresCondition: rank == 1 Condition: rank-veteran GrantCondition@RANK-ELITE: - RequiresCondition: rank == 2 + RequiresCondition: rank >= 2 Condition: rank-elite FirepowerMultiplier@VETERAN: RequiresCondition: rank-veteran From 725cc7c08401892c0488a23b76ca9f26675ecf0c Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 7 Feb 2017 14:00:47 -0600 Subject: [PATCH 14/17] Removed StackedCondition trait --- OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 1 - .../Traits/Conditions/ConditionManager.cs | 34 +------------------ .../Traits/Conditions/StackedCondition.cs | 34 ------------------- 3 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index b9e1fe0516..fc98f2f131 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -778,7 +778,6 @@ - diff --git a/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs b/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs index d626c183c8..2123c8896e 100644 --- a/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs +++ b/OpenRA.Mods.Common/Traits/Conditions/ConditionManager.cs @@ -63,15 +63,9 @@ namespace OpenRA.Mods.Common.Traits /// Each granted condition receives a unique token that is used when revoking. Dictionary tokens = new Dictionary(); - /// Set of conditions that are monitored for stacked bonuses, and the bonus conditions that they grant. - readonly Dictionary stackedConditions = new Dictionary(); - - /// Tokens granted by the stacked condition bonuses defined in stackedConditions. - readonly Dictionary> stackedTokens = new Dictionary>(); - int nextToken = 1; - /// Cache of condition -> enabled state for quick evaluation of boolean conditions. + /// Cache of condition -> enabled state for quick evaluation of token counter conditions. readonly Dictionary conditionCache = new Dictionary(); /// Read-only version of conditionCache that is passed to IConditionConsumers. @@ -111,12 +105,6 @@ namespace OpenRA.Mods.Common.Traits conditionCache[kv.Value] = conditionState.Tokens.Count; } - foreach (var sc in self.Info.TraitInfos()) - { - stackedConditions[sc.Condition] = sc.StackedConditions; - stackedTokens[sc.Condition] = new Stack(); - } - // Update all traits with their initial condition state foreach (var consumer in allConsumers) consumer.ConditionsChanged(self, readOnlyConditionCache); @@ -137,26 +125,6 @@ namespace OpenRA.Mods.Common.Traits foreach (var t in conditionState.Consumers) t.ConditionsChanged(self, readOnlyConditionCache); - - string[] sc; - if (stackedConditions.TryGetValue(condition, out sc)) - { - var target = (conditionState.Tokens.Count - 1).Clamp(0, sc.Length); - var st = stackedTokens[condition]; - for (var i = st.Count; i < target; i++) - { - // Empty strings are used to skip unwanted levels - var t = !string.IsNullOrEmpty(sc[i]) ? GrantCondition(self, sc[i]) : InvalidConditionToken; - st.Push(t); - } - - for (var i = st.Count; i > target; i--) - { - var t = st.Pop(); - if (t != InvalidConditionToken) - RevokeCondition(self, t); - } - } } /// Grants a specified condition. diff --git a/OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs b/OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs deleted file mode 100644 index c2cd3574a8..0000000000 --- a/OpenRA.Mods.Common/Traits/Conditions/StackedCondition.cs +++ /dev/null @@ -1,34 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2017 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. - */ -#endregion - -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.Traits -{ - [Desc("Grant additional conditions when a specified condition has been granted multiple times.")] - public class StackedConditionInfo : TraitInfo - { - [FieldLoader.Require] - [ConsumedConditionReference] - [Desc("Condition to monitor.")] - public readonly string Condition = null; - - [FieldLoader.Require] - [FieldLoader.AllowEmptyEntries] - [GrantedConditionReference] - [Desc("Conditions to grant when the monitored condition is granted multiple times.", - "The first entry is activated at 2x grants, second entry at 3x grants, and so on.", - "Use empty entries to skip levels.")] - public readonly string[] StackedConditions = { }; - } - - public class StackedCondition { } -} From f605756351873cf1390391bde5e47546b23047ee Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 19:41:50 -0600 Subject: [PATCH 15/17] ConditionExpression: Add arithmetic operations --- OpenRA.Game/Support/ConditionExpression.cs | 182 +++++++++++++++--- .../OpenRA.Game/ConditionExpressionTest.cs | 55 ++++++ 2 files changed, 213 insertions(+), 24 deletions(-) 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")] From 67dcbd1b2c39b8a40bcdf89ecf368eb8c1fc3950 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Tue, 14 Feb 2017 20:03:56 -0600 Subject: [PATCH 16/17] ConditionExpression: added bool constants --- OpenRA.Game/Support/ConditionExpression.cs | 42 +++++++++++++++++-- .../OpenRA.Game/ConditionExpressionTest.cs | 26 +++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/OpenRA.Game/Support/ConditionExpression.cs b/OpenRA.Game/Support/ConditionExpression.cs index 8fdb3cd35f..9ffd48ab29 100644 --- a/OpenRA.Game/Support/ConditionExpression.cs +++ b/OpenRA.Game/Support/ConditionExpression.cs @@ -111,6 +111,10 @@ namespace OpenRA.Support enum TokenType { + // fixed values + False, + True, + // varying values Number, Variable, @@ -198,6 +202,12 @@ namespace OpenRA.Support case TokenType.Invalid: yield return new TokenTypeInfo("()", Precedence.Invalid); continue; + case TokenType.False: + yield return new TokenTypeInfo("false", Precedence.Value); + continue; + case TokenType.True: + yield return new TokenTypeInfo("true", Precedence.Value); + continue; case TokenType.Number: yield return new TokenTypeInfo("()", Precedence.Value); continue; @@ -326,6 +336,20 @@ namespace OpenRA.Support return false; } + static TokenType VariableOrKeyword(string expression, int start, int length) + { + var i = start; + if (length == 4 && expression[i++] == 't' && expression[i++] == 'r' && expression[i++] == 'u' + && expression[i] == 'e') + return TokenType.True; + + if (length == 5 && expression[i++] == 'f' && expression[i++] == 'a' && expression[i++] == 'l' + && expression[i++] == 's' && expression[i] == 'e') + return TokenType.False; + + return TokenType.Variable; + } + public static TokenType GetNextType(string expression, ref int i, TokenType lastType = TokenType.Invalid) { var start = i; @@ -438,14 +462,14 @@ namespace OpenRA.Support 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++) + for (i = start; i < expression.Length; i++) { cc = CharClassOf(expression[i]); if (cc == CharClass.Whitespace || cc == CharClass.Operator) - return TokenType.Variable; + return VariableOrKeyword(expression, start, i - start); } - return TokenType.Variable; + return VariableOrKeyword(expression, start, i - start); } public static Token GetNext(string expression, ref int i, TokenType lastType = TokenType.Invalid) @@ -828,6 +852,18 @@ namespace OpenRA.Support continue; } + case TokenType.False: + { + ast.Push(False); + continue; + } + + case TokenType.True: + { + ast.Push(True); + 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 1cb61887cb..66cc5faea7 100644 --- a/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs +++ b/OpenRA.Test/OpenRA.Game/ConditionExpressionTest.cs @@ -23,8 +23,8 @@ namespace OpenRA.Test { IReadOnlyDictionary testValues = new ReadOnlyDictionary(new Dictionary() { - { "true", 1 }, - { "false", 0 } + { "one", 1 }, + { "five", 5 } }); void AssertFalse(string expression) @@ -66,6 +66,26 @@ namespace OpenRA.Test AssertValue("-12", -12); } + [TestCase(TestName = "Variables")] + public void TestVariables() + { + AssertValue("one", 1); + AssertValue("five", 5); + } + + [TestCase(TestName = "Boolean Constants")] + public void TestBoolConsts() + { + AssertValue(" true", 1); + AssertValue(" true ", 1); + AssertValue("true", 1); + AssertValue("false", 0); + AssertValue("tru", 0); + AssertValue("fals", 0); + AssertValue("tr", 0); + AssertValue("fal", 0); + } + [TestCase(TestName = "Booleans")] public void TestBooleans() { @@ -320,6 +340,8 @@ namespace OpenRA.Test public void TestUndefinedSymbols() { AssertFalse("undef1 || undef2"); + AssertValue("undef1", 0); + AssertValue("undef1 + undef2", 0); } } } From 47804e5fab668dca19d2115aae920ae58a638363 Mon Sep 17 00:00:00 2001 From: atlimit8 Date: Sat, 18 Feb 2017 12:41:49 -0600 Subject: [PATCH 17/17] Improve Upgrader for Upgrades => Condition expressions --- .../UtilityCommands/UpgradeRules.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs index 2a53959552..450c009eae 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -149,14 +149,31 @@ namespace OpenRA.Mods.Common.UtilityCommands upgradeMaxAcceptedLevel = FieldLoader.GetValue("", maxAcceptedNode.Value.Value); var processed = false; - if (upgradeTypes.Length == 1 && upgradeMinEnabledLevel == 0 && upgradeMaxEnabledLevel == 0 && upgradeMaxAcceptedLevel == 1) + if (upgradeMinEnabledLevel == 0 && upgradeMaxEnabledLevel == 0 && upgradeMaxAcceptedLevel == 1) { - node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", "!" + upgradeTypes.First())); + node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.Select(u => "!" + u).JoinWith(" && "))); processed = true; } - else if (upgradeTypes.Length == 1 && upgradeMinEnabledLevel == 1 && upgradeMaxEnabledLevel == int.MaxValue && upgradeMaxAcceptedLevel == 1) + else if (upgradeMinEnabledLevel == 1 && upgradeMaxEnabledLevel == int.MaxValue && upgradeMaxAcceptedLevel == 1) { - node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.First())); + node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.JoinWith(" || "))); + processed = true; + } + else if (upgradeMinEnabledLevel == 0 && upgradeMaxEnabledLevel < int.MaxValue) + { + node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.JoinWith(" + ") + " <= " + upgradeMaxEnabledLevel)); + processed = true; + } + else if (upgradeMaxEnabledLevel == int.MaxValue && upgradeMinEnabledLevel > 1) + { + node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", upgradeTypes.JoinWith(" + ") + " >= " + upgradeMinEnabledLevel)); + processed = true; + } + else if (upgradeMaxEnabledLevel < int.MaxValue && upgradeMinEnabledLevel > 0) + { + var lowerBound = upgradeMinEnabledLevel + " <= " + upgradeTypes.JoinWith(" + "); + var upperBound = upgradeTypes.JoinWith(" + ") + " <= " + upgradeMaxEnabledLevel; + node.Value.Nodes.Add(new MiniYamlNode("RequiresCondition", lowerBound + " && " + upperBound)); processed = true; }