Add null as an expression value.
Add variable and infix printing to expressions.
This commit is contained in:
Chris McConnell 2019-04-05 12:27:17 -07:00
Родитель f81b3df21b
Коммит f6ea3e2812
13 изменённых файлов: 903 добавлений и 81 удалений

Просмотреть файл

@ -102,6 +102,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Expre
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Declarative.Tests", "tests\Microsoft.Bot.Builder.Dialogs.Declarative.Tests\Microsoft.Bot.Builder.Dialogs.Declarative.Tests.csproj", "{D5E70443-4BA2-42ED-992A-010268440B08}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.AI.TriggerTrees.Tests", "tests\Microsoft.Bot.Builder.AI.TriggerTrees.Tests\Microsoft.Bot.Builder.AI.TriggerTrees.Tests.csproj", "{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU
@ -461,6 +463,14 @@ Global
{D5E70443-4BA2-42ED-992A-010268440B08}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{D5E70443-4BA2-42ED-992A-010268440B08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D5E70443-4BA2-42ED-992A-010268440B08}.Release|Any CPU.Build.0 = Release|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Documentation|Any CPU.Build.0 = Debug|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -513,6 +523,7 @@ Global
{8DC1257B-7650-40EB-97A2-C1CBA306DA6A} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{FB2EA804-158C-4654-AD60-A2105AC366FF} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{D5E70443-4BA2-42ED-992A-010268440B08} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}

Просмотреть файл

@ -218,8 +218,8 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
if (soFar == RelationshipType.Equal)
{
var shortIgnores = shorterClause.Children.Where(p => p.Type != TriggerTree.Ignore);
var longIgnores = longerClause.Children.Where(p => p.Type != TriggerTree.Ignore);
var shortIgnores = shorterClause.Children.Where(p => p.Type == TriggerTree.Ignore);
var longIgnores = longerClause.Children.Where(p => p.Type == TriggerTree.Ignore);
var swapped = false;
if (longIgnores.Count() < shortIgnores.Count())
{

Просмотреть файл

@ -24,7 +24,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
/// <summary>
/// All of the most specific triggers that contain the <see cref="Clause"/> in this node.
/// </summary>
public IReadOnlyList<Trigger> Triggers => _triggers;
public IReadOnlyList<Trigger> Triggers => _triggers;
/// <summary>
/// All triggers that contain the <see cref="Clause"/> in this node.
@ -40,7 +40,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
/// <summary>
/// Specialized children of this node.
/// </summary>
public IReadOnlyList<Node> Specializations => _specializations;
public IReadOnlyList<Node> Specializations => _specializations;
/// <summary>
/// The logical conjunction this node represents.
@ -71,6 +71,13 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
internal Node(Clause clause, TriggerTree tree, Trigger trigger = null)
{
// In order to debug:
// 1) Enable Count and VerifyTree
// 2) Run your scenario
// 3) You will most likely get a beak on the error.
// 4) Enable TraceTree and set it hear to get the trace before count
// Node._count has the global count for breakpointd
// ShowTrace = _count > 280000;
Clause = clause;
Tree = tree;
if (trigger != null)
@ -87,18 +94,18 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
predicate = child.Children[0];
}
children.Add(child);
children.Add(predicate);
}
if (children.Any())
{
Expression = Expression.MakeExpression(ExpressionType.And, null, children.ToArray());
}
else
{
Expression = Expression.ConstantExpression(true);
}
}
}
if (Expression == null)
{
Expression = Expression.ConstantExpression(true);
}
}
public override string ToString()
@ -108,7 +115,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
return builder.ToString();
}
public void ToString(StringBuilder builder, int indent = 0)
public void ToString(StringBuilder builder, int indent = 0)
=> Clause.ToString(builder, indent);
/// <summary>
@ -197,7 +204,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
_allTriggers.Add(trigger);
var add = true;
for (var i = 0; i < _triggers.Count(); )
for (var i = 0; i < _triggers.Count();)
{
var existing = _triggers[i];
var reln = trigger.Relationship(existing, Tree.Comparers);
@ -246,9 +253,6 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
case RelationshipType.Specializes:
{
triggerNode.AddSpecialization(this);
#if DEBUG
Debug.Assert(triggerNode.CheckInvariants());
#endif
op = Operation.Inserted;
}
break;
@ -309,7 +313,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
{
_specializations.Add(triggerNode);
#if DEBUG
Debug.Assert(triggerNode.CheckInvariants());
Debug.Assert(CheckInvariants());
#endif
op = Operation.Added;
}
@ -332,6 +336,8 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
var reln = Relationship(child);
Debug.Assert(reln == RelationshipType.Generalizes);
}
// Siblings should be incomparable
for (var i = 0; i < _specializations.Count; ++i)
{
var first = _specializations[i];
@ -342,6 +348,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
Debug.Assert(reln == RelationshipType.Incomparable);
}
}
// Triggers should be incomparable
for (var i = 0; i < _triggers.Count(); ++i)
{
@ -355,6 +362,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
}
}
}
// All triggers should all be found in triggers
for (var i = 0; i < _allTriggers.Count(); ++i)
{
@ -437,6 +445,10 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
added = true;
#if TraceTree
if (Node.ShowTrace) Debug.WriteLine("Added as specialization");
#endif
#if DEBUG
Debug.Assert(CheckInvariants());
#endif
}
return added;
@ -510,7 +522,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
if (!found)
{
var (value, error) = Expression.TryEvaluate(state);
if (error != null && value is bool match && match && Triggers.Any())
if (error == null && value is bool match && match && Triggers.Any())
{
matches.Add(this);
found = true;

Просмотреть файл

@ -174,7 +174,10 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
}
}
private Expression MakeExpression(string type, Expression expression) => Expression.MakeExpression(type, null, expression.Children);
private Expression ReplaceExpression(string type, Expression expression) => Expression.MakeExpression(type, TriggerTree.LookupFunction(type), expression.Children);
private Expression MakeExpression(string type, Expression expression) => Expression.MakeExpression(type, TriggerTree.LookupFunction(type), expression);
// Push not down to leaves using De Morgan's rule
private Expression PushDownNot(Expression expression, bool inNot)
@ -213,33 +216,37 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
case ExpressionType.LessThan:
if (inNot)
{
newExpr = MakeExpression(ExpressionType.GreaterThanOrEqual, expression);
newExpr = ReplaceExpression(ExpressionType.GreaterThanOrEqual, expression);
}
break;
case ExpressionType.LessThanOrEqual:
if (inNot)
{
newExpr = MakeExpression(ExpressionType.GreaterThan, expression);
newExpr = ReplaceExpression(ExpressionType.GreaterThan, expression);
}
break;
case ExpressionType.Equal:
if (inNot)
{
newExpr = MakeExpression(ExpressionType.NotEqual, expression);
newExpr = ReplaceExpression(ExpressionType.NotEqual, expression);
}
break;
case ExpressionType.GreaterThanOrEqual:
if (inNot)
{
newExpr = MakeExpression(ExpressionType.LessThan, expression);
newExpr = ReplaceExpression(ExpressionType.LessThan, expression);
}
break;
case ExpressionType.GreaterThan:
if (inNot)
{
newExpr = MakeExpression(ExpressionType.LessThanOrEqual, expression);
newExpr = ReplaceExpression(ExpressionType.LessThanOrEqual, expression);
}
break;
case ExpressionType.Exists:
// Rewrite exists(x) -> x != null
newExpr = Expression.MakeExpression(inNot ? ExpressionType.Equal : ExpressionType.NotEqual, null, expression.Children[0], Expression.ConstantExpression(null));
break;
case TriggerTree.Optional:
case TriggerTree.Ignore:
// Pass through optional/ignore
@ -385,6 +392,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
if (reln == RelationshipType.Equal)
{
_clauses.RemoveAt(j);
--j;
}
else
{
@ -441,8 +449,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
&& cnst.Value is string str
&& str == variable)
{
cnst.Value = binding;
newExpr = cnst;
newExpr = Expression.Accessor(binding);
changed = true;
}
else
@ -566,6 +573,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
}
}
}
clause.Children = predicates.ToArray();
}
}
}

Просмотреть файл

@ -99,7 +99,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
ExpressionEvaluator eval;
if (type == Optional || type == Ignore)
{
eval = new ExpressionEvaluator(null, ReturnType.Object, BuiltInFunctions.ValidateUnaryBoolean);
eval = new ExpressionEvaluator(null, ReturnType.Boolean, BuiltInFunctions.ValidateUnaryBoolean);
}
else
{

Просмотреть файл

@ -133,6 +133,10 @@ namespace Microsoft.Bot.Builder.Expressions.Parser
{
result = Expression.ConstantExpression(true);
}
else if (symbol == "null")
{
result = Expression.ConstantExpression(null);
}
else if (IsShortHandExpression(symbol))
{
result = MakeShortHandExpression(symbol);

Просмотреть файл

@ -268,7 +268,7 @@ namespace Microsoft.Bot.Builder.Expressions
public static string VerifyBoolean(object value, Expression expression)
{
string error = null;
if (!(value is Boolean))
if (!(value is bool))
{
error = $"{expression} is not a boolean.";
}
@ -414,7 +414,7 @@ namespace Microsoft.Bot.Builder.Expressions
/// <returns>Information about expression type.</returns>
public static ExpressionEvaluator Lookup(string type)
{
if (!_functions.TryGetValue(type, out ExpressionEvaluator eval))
if (!_functions.TryGetValue(type, out var eval))
{
throw new ArgumentException($"{type} does not have a built-in expression evaluator.");
}
@ -444,7 +444,7 @@ namespace Microsoft.Bot.Builder.Expressions
{
object value = null;
string error = null;
object instance = state;
var instance = state;
var children = expression.Children;
if (children.Length == 2)
{
@ -477,7 +477,7 @@ namespace Microsoft.Bot.Builder.Expressions
{
if (idxValue is int idx)
{
int count = -1;
var count = -1;
if (inst is Array arr)
{
count = arr.Length;
@ -531,7 +531,7 @@ namespace Microsoft.Bot.Builder.Expressions
(result, error) = child.TryEvaluate(state);
if (error == null)
{
if (!(result is Boolean bresult))
if (!(result is bool bresult))
{
error = $"{child} is not boolean";
break;
@ -559,7 +559,7 @@ namespace Microsoft.Bot.Builder.Expressions
(result, error) = child.TryEvaluate(state);
if (error == null)
{
if (!(result is Boolean bresult))
if (!(result is bool bresult))
{
error = $"{child} is not boolean";
break;
@ -668,7 +668,7 @@ namespace Microsoft.Bot.Builder.Expressions
{ ExpressionType.Divide,
new ExpressionEvaluator(ApplySequence(args => args[0] / args[1],
(value, expression) => {
string error = VerifyNumber(value, expression);
var error = VerifyNumber(value, expression);
if (error == null && Convert.ToDouble(value) == 0.0)
{
error = $"Cannot divide by 0 from {expression}";
@ -692,7 +692,7 @@ namespace Microsoft.Bot.Builder.Expressions
{ ExpressionType.LessThanOrEqual, Comparison(args => args[0] <= args[1]) },
{ ExpressionType.Equal,
new ExpressionEvaluator(Apply(args => args[0] == args[1]), ReturnType.Boolean, ValidateBinary) },
{ ExpressionType.NotEqual,
{ ExpressionType.NotEqual,
new ExpressionEvaluator(Apply(args => args[0] != args[1]), ReturnType.Boolean, ValidateBinary) },
{ ExpressionType.GreaterThan,Comparison(args => args[0] > args[1]) },
{ ExpressionType.GreaterThanOrEqual, Comparison(args => args[0] >= args[1]) },

Просмотреть файл

@ -34,7 +34,7 @@ namespace Microsoft.Bot.Builder.Expressions
Evaluator.ReturnType =
(value is string ? ReturnType.String
: value.IsNumber() ? ReturnType.Number
: value is Boolean ? ReturnType.Boolean
: value is bool ? ReturnType.Boolean
: ReturnType.Object);
_value = value;
}
@ -44,7 +44,11 @@ namespace Microsoft.Bot.Builder.Expressions
public override string ToString()
{
if (Value is string str)
if (Value == null)
{
return "null";
}
else if (Value is string str)
{
return $"'{Value}'";
}

Просмотреть файл

@ -70,15 +70,12 @@ namespace Microsoft.Bot.Builder.Expressions
/// <summary>
/// Expected result of evaluating expression.
/// </summary>
public ReturnType ReturnType { get { return Evaluator.ReturnType; } }
public ReturnType ReturnType => Evaluator.ReturnType;
/// <summary>
/// Validate immediate expression.
/// </summary>
public void Validate()
{
Evaluator.ValidateExpression(this);
}
public void Validate() => Evaluator.ValidateExpression(this);
/// <summary>
/// Recursively validate the expression tree.
@ -103,30 +100,63 @@ namespace Microsoft.Bot.Builder.Expressions
=> Evaluator.TryEvaluate(this, state);
public override string ToString()
{
return ToString(Type);
}
protected string ToString(string name)
{
var builder = new StringBuilder();
builder.Append(Type);
builder.Append('(');
var first = true;
foreach (var child in Children)
// Special support for memory paths
if (Type == ExpressionType.Accessor)
{
if (first)
var prop = (Children[0] as Constant).Value;
if (Children.Count() == 1)
{
first = false;
builder.Append(prop);
}
else
{
builder.Append(", ");
builder.Append(Children[1].ToString());
builder.Append('.');
builder.Append(prop);
}
builder.Append(child.ToString());
}
builder.Append(')');
else if (Type == ExpressionType.Element)
{
builder.Append(Children[0].ToString());
builder.Append('[');
builder.Append(Children[1].ToString());
builder.Append(']');
}
else
{
var infix = Type.Length > 0 && !char.IsLetter(Type[0]) && Children.Count() >= 2;
if (!infix)
{
builder.Append(Type);
}
builder.Append('(');
var first = true;
foreach (var child in Children)
{
if (first)
{
first = false;
}
else
{
if (infix)
{
builder.Append(' ');
builder.Append(Type);
builder.Append(' ');
}
else
{
builder.Append(", ");
}
}
builder.Append(child.ToString());
}
builder.Append(')');
}
return builder.ToString();
}

Просмотреть файл

@ -16,19 +16,17 @@ namespace Microsoft.Bot.Builder.Expressions
/// <param name="value">Value to check.</param>
/// <returns>True if numeric type.</returns>
public static bool IsNumber(this object value)
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
}
=> value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
/// <summary>
/// Test an object to see if it is an integer type.
@ -36,16 +34,14 @@ namespace Microsoft.Bot.Builder.Expressions
/// <param name="value">Value to check.</param>
/// <returns>True if numeric type.</returns>
public static bool IsInteger(this object value)
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong;
}
=> value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong;
/// <summary>
/// Do a deep equality between expressions.
@ -55,7 +51,7 @@ namespace Microsoft.Bot.Builder.Expressions
/// <returns>True if expressions are the same.</returns>
public static bool DeepEquals(this Expression expr, Expression other)
{
bool eq = true;
var eq = true;
if (expr != null && other != null)
{
eq = expr.Type == other.Type;
@ -65,7 +61,7 @@ namespace Microsoft.Bot.Builder.Expressions
{
var val = ((Constant)expr).Value;
var otherVal = ((Constant)other).Value;
eq = val.Equals(otherVal);
eq = val == otherVal || (val != null && val.Equals(otherVal));
}
else
{
@ -186,7 +182,7 @@ namespace Microsoft.Bot.Builder.Expressions
}
else if (instance is JObject jobj)
{
if (jobj.TryGetValue(property, out JToken jtoken))
if (jobj.TryGetValue(property, out var jtoken))
{
if (jtoken is JArray jarray)
{

Просмотреть файл

@ -0,0 +1,567 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Bot.Builder.AI.TriggerTrees;
using Microsoft.Bot.Builder.Expressions;
namespace Microsoft.TriggerTreeTests
{
public class Comparison
{
public string Type;
public object Value;
public Comparison(string type, object value)
{
Type = type;
Value = value;
}
}
public class ExpressionInfo
{
public Expression Expression;
public Dictionary<string, Comparison> Bindings = new Dictionary<string, Comparison>();
public List<Quantifier> Quantifiers = new List<Quantifier>();
public ExpressionInfo(Expression expression)
{
Expression = expression;
}
public ExpressionInfo(Expression expression, string name, object value, string type)
{
Expression = expression;
Bindings.Add(name, new Comparison(type, value));
}
public ExpressionInfo(Expression expression, Dictionary<string, Comparison> bindings, List<Quantifier> quantifiers = null)
{
Expression = expression;
Bindings = bindings;
if (quantifiers != null)
{
Quantifiers = quantifiers;
}
}
public override string ToString() => Expression.ToString();
}
public class TriggerInfo
{
public Expression Trigger;
public Dictionary<string, object> Bindings = new Dictionary<string, object>();
}
public class Generator
{
public Random Rand;
public class SimpleValues
{
public int Int = 1;
public double Double = 2.0;
public string String = "3";
public object Object = null;
public SimpleValues()
{ }
public SimpleValues(int integer)
{
Int = integer;
}
public SimpleValues(double number)
{
Double = number;
}
public SimpleValues(object obj)
{
Object = obj;
}
public bool Test(int? value) => value.HasValue && Int == value;
public bool Test(double? value) => value.HasValue && Double == value;
public bool Test(string value) => value != null && String == value;
public bool Test(SimpleValues value) => Int == value.Int && Double == value.Double && String == value.String && Object.Equals(value.Object);
public static bool Test(SimpleValues obj, int? value) => value.HasValue && obj.Int == value;
public static bool Test(SimpleValues obj, double? value) => value.HasValue && obj.Double == value;
public static bool Test(SimpleValues obj, string value) => value != null && obj.String == value;
public static bool Test(SimpleValues obj, object other) => other != null && obj.Object.Equals(other);
}
public Generator(int seed = 0)
{
Rand = new Random(seed);
}
private static readonly string[] comparisons = new string[] {
ExpressionType.LessThan, ExpressionType.LessThanOrEqual,
ExpressionType.Equal,
// TODO: null values are always not equal ExpressionType.NotEqual,
ExpressionType.GreaterThanOrEqual, ExpressionType.GreaterThan };
/* Predicates */
public Expression GenerateString(int length) => Expression.ConstantExpression(RandomString(length));
public string RandomString(int length)
{
var builder = new StringBuilder();
for (var i = 0; i < length; ++i)
{
builder.Append(((char)('a' + Rand.Next(26))));
}
return builder.ToString();
}
private const double DoubleEpsilon = 0.000001;
private int AdjustValue(int value, string type)
{
var result = value;
const int epsilon = 1;
switch (type)
{
case ExpressionType.LessThan: result += epsilon; break;
case ExpressionType.NotEqual: result += epsilon; break;
case ExpressionType.GreaterThan: result -= epsilon; break;
}
return result;
}
private double AdjustValue(double value, string type)
{
var result = value;
switch (type)
{
case ExpressionType.LessThan: result += DoubleEpsilon; break;
case ExpressionType.NotEqual: result += DoubleEpsilon; break;
case ExpressionType.GreaterThan: result -= DoubleEpsilon; break;
}
return result;
}
public ExpressionInfo GenerateSimpleComparison(string name)
{
Expression expression = null;
object value = null;
var type = RandomChoice<string>(comparisons);
switch (Rand.Next(2))
{
case 0:
{
value = Rand.Next();
expression = Expression.MakeExpression(
type,
null,
Expression.Accessor(name),
Expression.ConstantExpression(AdjustValue((int)value, type)));
}
break;
case 1:
{
value = Rand.NextDouble();
expression = Expression.MakeExpression(
type,
null,
Expression.Accessor(name),
Expression.ConstantExpression(AdjustValue((double)value, type)));
}
break;
}
return new ExpressionInfo(expression, name, value, type);
}
public ExpressionInfo GenerateHasValueComparison(string name)
{
Expression expression = null;
object value = null;
switch (Rand.Next(3))
{
case 0:
expression = Expression.MakeExpression(ExpressionType.Exists, null, Expression.Accessor(name));
value = Rand.Next();
break;
case 1:
expression = Expression.MakeExpression(ExpressionType.Exists, null, Expression.Accessor(name));
value = Rand.NextDouble();
break;
case 2:
expression = Expression.MakeExpression(ExpressionType.NotEqual, null, Expression.Accessor(name), Expression.ConstantExpression(null));
value = RandomString(5);
break;
}
return new ExpressionInfo(expression, name, value, ExpressionType.Not);
}
public List<ExpressionInfo> GeneratePredicates(int n, string nameBase)
{
var expressions = new List<ExpressionInfo>();
for (var i = 0; i < n; ++i)
{
var name = $"{nameBase}{i}";
var selection = RandomWeighted(new double[] { 1.0, 1.0 });
switch (selection)
{
case 0: expressions.Add(GenerateSimpleComparison(name)); break;
case 1: expressions.Add(GenerateHasValueComparison(name)); break;
}
}
return expressions;
}
public List<ExpressionInfo> GenerateConjunctions(List<ExpressionInfo> predicates, int numConjunctions, int minClause, int maxClause)
{
var conjunctions = new List<ExpressionInfo>();
for (var i = 0; i < numConjunctions; ++i)
{
var clauses = minClause + Rand.Next(maxClause - minClause);
var expressions = new List<ExpressionInfo>();
var used = new List<int>();
for (var j = 0; j < clauses; ++j)
{
int choice;
do
{
choice = Rand.Next(predicates.Count);
} while (used.Contains(choice));
expressions.Add(predicates[choice]);
used.Add(choice);
}
var conjunction = Binary(ExpressionType.And, expressions, out var bindings);
conjunctions.Add(new ExpressionInfo(conjunction, bindings));
}
return conjunctions;
}
public List<ExpressionInfo> GenerateDisjunctions(List<ExpressionInfo> predicates, int numDisjunctions, int minClause, int maxClause)
{
var disjunctions = new List<ExpressionInfo>();
for (var i = 0; i < numDisjunctions; ++i)
{
var clauses = minClause + Rand.Next(maxClause - minClause);
var expressions = new List<ExpressionInfo>();
var used = new List<int>();
for (var j = 0; j < clauses; ++j)
{
int choice;
do
{
choice = Rand.Next(predicates.Count);
} while (used.Contains(choice));
expressions.Add(predicates[choice]);
used.Add(choice);
}
var disjunction = Binary(ExpressionType.Or, expressions, out var bindings);
disjunctions.Add(new ExpressionInfo(disjunction, bindings));
}
return disjunctions;
}
public List<ExpressionInfo> GenerateOptionals(List<ExpressionInfo> predicates, int numOptionals, int minClause, int maxClause)
{
var optionals = new List<ExpressionInfo>();
for (var i = 0; i < numOptionals; ++i)
{
var clauses = minClause + Rand.Next(maxClause - minClause);
var expressions = new List<ExpressionInfo>();
var used = new List<int>();
for (var j = 0; j < clauses; ++j)
{
int choice;
do
{
choice = Rand.Next(predicates.Count);
} while (used.Contains(choice));
var predicate = predicates[choice];
if (j == 0)
{
var optional = Expression.MakeExpression(TriggerTree.Optional, null, predicate.Expression);
if (Rand.NextDouble() < 0.25)
{
optional = Expression.NotExpression(optional);
}
expressions.Add(new ExpressionInfo(optional, predicate.Bindings));
}
else
{
expressions.Add(predicate);
}
used.Add(choice);
}
var conjunction = Binary(ExpressionType.And, expressions, out var bindings);
optionals.Add(new ExpressionInfo(conjunction, bindings));
}
return optionals;
}
public Expression Binary(string type, IEnumerable<ExpressionInfo> expressions,
out Dictionary<string, Comparison> bindings)
{
bindings = MergeBindings(expressions);
Expression binaryExpression = null;
foreach (var info in expressions)
{
if (binaryExpression == null)
{
binaryExpression = info.Expression;
}
else
{
binaryExpression = Expression.MakeExpression(type, null, binaryExpression, info.Expression);
}
}
return binaryExpression;
}
public IEnumerable<Expression> Predicates(IEnumerable<ExpressionInfo> expressions)
{
foreach (var info in expressions)
{
yield return info.Expression;
}
}
private int SplitMemory(string mem, out string baseName)
{
var i = 0;
for (; i < mem.Length; ++i)
{
if (char.IsDigit(mem[i]))
{
break;
}
}
baseName = mem.Substring(0, i);
return int.Parse(mem.Substring(i));
}
public List<ExpressionInfo> GenerateQuantfiers(List<ExpressionInfo> predicates, int numExpressions, int maxVariable, int maxExpansion, int maxQuantifiers)
{
var result = new List<ExpressionInfo>();
var allBindings = MergeBindings(predicates);
var allTypes = VariablesByType(allBindings);
for (var exp = 0; exp < numExpressions; ++exp)
{
var expression = RandomChoice(predicates);
var info = new ExpressionInfo(expression.Expression);
var numQuants = 1 + Rand.Next(maxQuantifiers - 1);
var chosen = new HashSet<string>();
var maxBase = Math.Min(expression.Bindings.Count, numQuants);
for (var quant = 0; quant < maxBase; ++quant)
{
KeyValuePair<string, Comparison> baseBinding;
// Can only map each expression variable once in a quantifier
do
{
baseBinding = expression.Bindings.ElementAt(Rand.Next(expression.Bindings.Count));
} while (chosen.Contains(baseBinding.Key));
chosen.Add(baseBinding.Key);
SplitMemory(baseBinding.Key, out var baseName);
var mappings = new List<string>();
var expansion = 1 + Rand.Next(maxExpansion - 1);
for (var i = 0; i < expansion; ++i)
{
if (i == 0)
{
mappings.Add($"{baseBinding.Key}");
}
else
{
var mapping = RandomChoice<string>(allTypes[baseBinding.Value.Value.GetType()]);
if (!mappings.Contains(mapping))
{
mappings.Add(mapping);
}
}
}
var any = Rand.NextDouble() < 0.5;
if (any)
{
var mem = RandomChoice(mappings);
if (!info.Bindings.ContainsKey(mem))
{
info.Bindings.Add(mem, baseBinding.Value);
}
info.Quantifiers.Add(new Quantifier(baseBinding.Key, QuantifierType.Any, mappings));
}
else
{
foreach (var mapping in mappings)
{
if (!info.Bindings.ContainsKey(mapping))
{
info.Bindings.Add(mapping, baseBinding.Value);
}
}
info.Quantifiers.Add(new Quantifier(baseBinding.Key, QuantifierType.All, mappings));
}
}
result.Add(info);
}
return result;
}
private Comparison NotValue(Comparison comparison)
{
var value = comparison.Value;
var type = value.GetType();
var isNot = false;
if (type != typeof(int) && type != typeof(double) && type != typeof(string))
{
throw new Exception($"Unsupported type {type}");
}
switch (comparison.Type)
{
case ExpressionType.LessThanOrEqual:
case ExpressionType.LessThan:
{
if (type == typeof(int)) value = (int)value + 1;
else if (type == typeof(double)) value = (double)value + DoubleEpsilon;
}
break;
case ExpressionType.Equal:
{
if (type == typeof(int)) value = (int)value - 1;
else if (type == typeof(double)) value = (double)value - DoubleEpsilon;
}
break;
case ExpressionType.NotEqual:
{
if (type == typeof(int)) value = (int)value - 1;
else if (type == typeof(double)) value = (double)value - DoubleEpsilon;
}
break;
case ExpressionType.GreaterThanOrEqual:
case ExpressionType.GreaterThan:
{
if (type == typeof(int)) value = (int)value - 1;
else if (type == typeof(double)) value = (double)value - DoubleEpsilon;
}
break;
case ExpressionType.Not:
{
isNot = true;
}
break;
}
return isNot ? null : new Comparison(comparison.Type, value);
}
public IEnumerable<ExpressionInfo> GenerateNots(IList<ExpressionInfo> predicates, int numNots)
{
for (var i = 0; i < numNots; ++i)
{
var expr = RandomChoice(predicates);
var bindings = new Dictionary<string, Comparison>();
foreach (var binding in expr.Bindings)
{
var comparison = NotValue(binding.Value);
if (comparison != null)
{
bindings.Add(binding.Key, comparison);
}
}
yield return new ExpressionInfo(Expression.NotExpression(expr.Expression), bindings, expr.Quantifiers);
}
}
private Dictionary<Type, List<string>> VariablesByType(Dictionary<string, Comparison> bindings)
{
var result = new Dictionary<Type, List<string>>();
foreach (var binding in bindings)
{
var type = binding.Value.Value.GetType();
if (!result.ContainsKey(type))
{
result.Add(type, new List<string>());
}
result[type].Add(binding.Key);
}
return result;
}
public Dictionary<string, Comparison> MergeBindings(IEnumerable<ExpressionInfo> expressions)
{
var bindings = new Dictionary<string, Comparison>();
foreach (var info in expressions)
{
foreach (var binding in info.Bindings)
{
bindings[binding.Key] = binding.Value;
}
}
return bindings;
}
public T RandomChoice<T>(IList<T> choices) => choices[Rand.Next(choices.Count)];
public class WeightedChoice<T>
{
public double Weight = 0.0;
public T Choice = default(T);
}
public T RandomWeighted<T>(IEnumerable<WeightedChoice<T>> choices)
{
var totalWeight = 0.0;
foreach (var choice in choices)
{
totalWeight += choice.Weight;
}
var selection = Rand.NextDouble() * totalWeight;
var soFar = 0.0;
var result = default(T);
foreach (var choice in choices)
{
if (soFar <= selection)
{
soFar += choice.Weight;
result = choice.Choice;
}
else
{
break;
}
}
return result;
}
public int RandomWeighted(IReadOnlyList<double> weights)
{
var totalWeight = 0.0;
foreach (var weight in weights)
{
totalWeight += weight;
}
var selection = Rand.NextDouble() * totalWeight;
var soFar = 0.0;
var result = 0;
for (var i = 0; i < weights.Count; ++i)
{
if (soFar <= selection)
{
soFar += weights[i];
result = i;
}
else
{
break;
}
}
return result;
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0-preview-20180816-01" />
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Builder.AI.TriggerTrees\Microsoft.Bot.Builder.AI.TriggerTrees.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,169 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Bot.Builder.AI.TriggerTrees;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using Microsoft.Bot.Builder.Expressions;
using Microsoft.Bot.Builder.Expressions.Parser;
namespace Microsoft.TriggerTreeTests
{
[TestClass]
public class Tests
{
private Generator _generator;
private Trigger VerifyAddTrigger(TriggerTree tree, Expression expression, object action)
{
var trigger = tree.AddTrigger(expression, action);
VerifyTree(tree);
return trigger;
}
private void VerifyTree(TriggerTree tree)
{
var badNode = tree.VerifyTree();
Assert.AreEqual(null, badNode);
}
public Tests()
{
_generator = new Generator();
}
[TestMethod]
public void TestRoot()
{
var tree = new TriggerTree();
tree.AddTrigger("true", "root");
var matches = tree.Matches(new Dictionary<string, object>());
Assert.AreEqual(1, matches.Count());
Assert.AreEqual("root", matches.First().Triggers.First().Action);
}
[TestMethod]
public void TestIgnore()
{
var tree = new TriggerTree();
tree.AddTrigger("ignore(!exists(foo)) && exists(blah)", 1);
tree.AddTrigger("exists(blah) && ignore(!exists(foo2)) && woof == 3", 2);
tree.AddTrigger("exists(blah) && woof == 3", 3);
tree.AddTrigger("exists(blah) && woof == 3 && ignore(!exists(foo2))", 2);
var frame = new Dictionary<string, object> { { "blah", 1 }, { "woof", 3 } };
var matches = tree.Matches(frame).ToList();
Assert.AreEqual(2, matches.Count);
Assert.AreEqual(1, matches[0].AllTriggers.Count());
Assert.AreEqual(1, matches[1].AllTriggers.Count());
Assert.AreEqual(3, matches[1].AllTriggers.First().Action);
}
[TestMethod]
public void TestTree()
{
var numPredicates = 100;
var numSingletons = 50;
var numConjunctions = 100;
var numDisjunctions = 100;
var numOptionals = 100;
var numQuantifiers = 100;
var numNots = 100;
var minClause = 2;
var maxClause = 4;
var maxExpansion = 3;
var maxQuantifiers = 3;
var singletons = _generator.GeneratePredicates(numPredicates, "mem");
var tree = new TriggerTree();
var predicates = new List<ExpressionInfo>(singletons);
// Add singletons
foreach (var predicate in singletons.Take(numSingletons))
{
tree.AddTrigger(predicate.Expression, predicate.Bindings);
}
Assert.AreEqual(numSingletons, tree.TotalTriggers);
// Add conjunctions and test matches
var conjunctions = _generator.GenerateConjunctions(predicates, numConjunctions, minClause, maxClause);
foreach (var conjunction in conjunctions)
{
var memory = new Dictionary<string, object>();
foreach (var binding in conjunction.Bindings)
{
memory.Add(binding.Key, binding.Value.Value);
}
var trigger = tree.AddTrigger(conjunction.Expression, conjunction.Bindings);
var matches = tree.Matches(memory);
Assert.IsTrue(matches.Count() == 1);
var first = matches.First().Clause;
foreach (var match in matches)
{
Assert.AreEqual(RelationshipType.Equal, first.Relationship(match.Clause, tree.Comparers));
}
}
Assert.AreEqual(numSingletons + numConjunctions, tree.TotalTriggers);
// Add disjunctions
predicates.AddRange(conjunctions);
var disjunctions = _generator.GenerateDisjunctions(predicates, numDisjunctions, minClause, maxClause);
foreach (var disjunction in disjunctions)
{
tree.AddTrigger(disjunction.Expression, disjunction.Bindings);
}
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions, tree.TotalTriggers);
var all = new List<ExpressionInfo>(predicates);
all.AddRange(disjunctions);
// Add optionals
var optionals = _generator.GenerateOptionals(all, numOptionals, minClause, maxClause);
foreach(var optional in optionals)
{
tree.AddTrigger(optional.Expression, optional.Bindings);
}
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions + numOptionals, tree.TotalTriggers);
all.AddRange(optionals);
// Add quantifiers
var quantified = _generator.GenerateQuantfiers(all, numQuantifiers, maxClause, maxExpansion, maxQuantifiers);
foreach(var expr in quantified)
{
tree.AddTrigger(expr.Expression, expr.Bindings, expr.Quantifiers.ToArray());
}
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions + numOptionals + numQuantifiers, tree.TotalTriggers);
all.AddRange(quantified);
var nots = _generator.GenerateNots(all, numNots);
foreach(var expr in nots)
{
tree.AddTrigger(expr.Expression, expr.Bindings, expr.Quantifiers.ToArray());
}
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions + numOptionals + numQuantifiers + numNots, tree.TotalTriggers);
VerifyTree(tree);
// Test matches
foreach (var predicate in predicates)
{
var memory = new Dictionary<string, object>();
foreach (var binding in predicate.Bindings)
{
memory.Add(binding.Key, binding.Value.Value);
}
var matches = tree.Matches(memory).ToList();
for (var i = 0; i < matches.Count; ++i)
{
var first = matches[i];
for (var j = i + 1; j < matches.Count; ++j)
{
var second = matches[j];
var reln = first.Relationship(second);
Assert.AreEqual(RelationshipType.Incomparable, reln);
}
}
}
tree.GenerateGraph("tree.dot");
}
}
}