Basic expression tree and added triggerTree.
This commit is contained in:
Родитель
d55bc239e0
Коммит
11c6e70306
|
@ -76,8 +76,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialo
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Expressions", "libraries\Microsoft.CommonExpressions\Microsoft.Expressions.csproj", "{68899A42-6375-43BC-95BB-2E4798D2C0EB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Expressions.Tests", "tests\Microsoft.Expressions.Tests\Microsoft.Expressions.Tests.csproj", "{A7637631-40EF-4E8D-A90A-422A5FD593D1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Declarative", "libraries\Microsoft.Bot.Builder.Dialogs.Declarative\Microsoft.Bot.Builder.Dialogs.Declarative.csproj", "{1BC05915-044E-4776-8956-B44BBEFF2F84}"
|
||||
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", "{CD473E17-DA3C-4E64-9AC6-31B4FB6D69E7}"
|
||||
|
@ -98,6 +96,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.TestB
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Builder.TestBot.WebApi", "samples\Microsoft.Bot.Builder.TestBot.WebApi\Microsoft.Bot.Builder.TestBot.WebApi.csproj", "{A920C660-CAAC-421B-AE0C-B79BE31DA472}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.AI.TriggerTrees", "libraries\Microsoft.Bot.Builder.AI.TriggerTrees\Microsoft.Bot.Builder.AI.TriggerTrees.csproj", "{A8BC2784-763B-4256-89ED-24DE029F9B38}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Expressions.Tests", "tests\Microsoft.Expressions.Tests\Microsoft.Expressions.Tests.csproj", "{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU
|
||||
|
@ -355,14 +357,6 @@ Global
|
|||
{68899A42-6375-43BC-95BB-2E4798D2C0EB}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{68899A42-6375-43BC-95BB-2E4798D2C0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{68899A42-6375-43BC-95BB-2E4798D2C0EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1BC05915-044E-4776-8956-B44BBEFF2F84}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug - NuGet Packages|Any CPU
|
||||
{1BC05915-044E-4776-8956-B44BBEFF2F84}.Debug - NuGet Packages|Any CPU.Build.0 = Debug - NuGet Packages|Any CPU
|
||||
{1BC05915-044E-4776-8956-B44BBEFF2F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
|
@ -435,6 +429,20 @@ Global
|
|||
{A920C660-CAAC-421B-AE0C-B79BE31DA472}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A920C660-CAAC-421B-AE0C-B79BE31DA472}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A920C660-CAAC-421B-AE0C-B79BE31DA472}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -475,7 +483,6 @@ Global
|
|||
{2255FF5B-8010-4C8D-897E-BDDEBB629967} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
|
||||
{78D16CCC-6184-4678-87D8-E8C4299317CE} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
|
||||
{68899A42-6375-43BC-95BB-2E4798D2C0EB} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
|
||||
{A7637631-40EF-4E8D-A90A-422A5FD593D1} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
|
||||
{1BC05915-044E-4776-8956-B44BBEFF2F84} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
|
||||
{CD473E17-DA3C-4E64-9AC6-31B4FB6D69E7} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
|
||||
{150BC344-4534-41E1-9700-9D231CF327D2} = {763168FA-A590-482C-84D8-2922F7ADB1A2}
|
||||
|
@ -485,6 +492,8 @@ Global
|
|||
{9940D79A-B7D7-42ED-A353-0A889B458842} = {1DEDDC46-F2F4-448F-A0B7-FD48682EEC80}
|
||||
{758AA8A8-D476-473A-9761-84D2317D8268} = {1DEDDC46-F2F4-448F-A0B7-FD48682EEC80}
|
||||
{A920C660-CAAC-421B-AE0C-B79BE31DA472} = {1DEDDC46-F2F4-448F-A0B7-FD48682EEC80}
|
||||
{A8BC2784-763B-4256-89ED-24DE029F9B38} = {763168FA-A590-482C-84D8-2922F7ADB1A2}
|
||||
{AE3FC7F6-B212-4438-B1D7-C121BB4C15ED} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
public class Clause
|
||||
{
|
||||
public List<Expression> Predicates = new List<Expression>();
|
||||
public Dictionary<string, string> AnyBindings = new Dictionary<string, string>();
|
||||
internal bool Subsumed = false;
|
||||
|
||||
internal Clause()
|
||||
{ }
|
||||
|
||||
internal Clause(Clause fromClause)
|
||||
{
|
||||
foreach (var pair in fromClause.AnyBindings)
|
||||
{
|
||||
AnyBindings.Add(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal Clause(Expression expression)
|
||||
{
|
||||
Predicates.Add(expression);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
ToString(builder);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public void ToString(StringBuilder builder, int indent = 0)
|
||||
{
|
||||
builder.Append(' ', indent);
|
||||
if (Subsumed)
|
||||
{
|
||||
builder.Append('*');
|
||||
}
|
||||
builder.Append('(');
|
||||
var first = true;
|
||||
foreach (var predicate in Predicates)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(" && ");
|
||||
}
|
||||
builder.Append(predicate.ToString());
|
||||
}
|
||||
builder.Append(')');
|
||||
foreach (var binding in AnyBindings)
|
||||
{
|
||||
builder.Append($" {binding.Key}->{binding.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
public RelationshipType Relationship(Clause other, Dictionary<string, IPredicateComparer> comparers)
|
||||
{
|
||||
var soFar = RelationshipType.Incomparable;
|
||||
var shorter = this;
|
||||
var shorterCount = shorter.PredicateCount();
|
||||
var longer = other;
|
||||
var longerCount = longer.PredicateCount();
|
||||
var swapped = false;
|
||||
if (longerCount < shorterCount)
|
||||
{
|
||||
longer = this;
|
||||
shorter = other;
|
||||
var tmp = longerCount;
|
||||
longerCount = shorterCount;
|
||||
shorterCount = tmp;
|
||||
swapped = true;
|
||||
}
|
||||
if (shorterCount == 0)
|
||||
{
|
||||
if (longerCount == 0)
|
||||
{
|
||||
soFar = RelationshipType.Equal;
|
||||
}
|
||||
else
|
||||
{
|
||||
soFar = RelationshipType.Generalizes;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If every one of shorter predicates is equal or superset of one in longer, then shoter is a superset of longer
|
||||
foreach (var shortPredicate in shorter.Predicates)
|
||||
{
|
||||
var shorterRel = RelationshipType.Incomparable;
|
||||
if (!IsIgnore(shortPredicate))
|
||||
{
|
||||
foreach (var longPredicate in longer.Predicates)
|
||||
{
|
||||
shorterRel = Relationship(shortPredicate, longPredicate, comparers);
|
||||
if (shorterRel != RelationshipType.Incomparable)
|
||||
{
|
||||
// Found related predicates
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shorterRel == RelationshipType.Incomparable)
|
||||
{
|
||||
// Predicate in shorter is incomparable so done
|
||||
soFar = RelationshipType.Incomparable;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (soFar == RelationshipType.Incomparable)
|
||||
{
|
||||
soFar = shorterRel;
|
||||
}
|
||||
if (soFar == RelationshipType.Equal)
|
||||
{
|
||||
if (shorterRel == RelationshipType.Generalizes
|
||||
|| (shorterRel == RelationshipType.Specializes && shorterCount == longerCount)
|
||||
|| shorterRel == RelationshipType.Equal)
|
||||
{
|
||||
soFar = shorterRel;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (soFar != shorterRel)
|
||||
{
|
||||
// Not continued with sub/super so incomparable
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shorterCount != longerCount)
|
||||
{
|
||||
switch (soFar)
|
||||
{
|
||||
case RelationshipType.Equal:
|
||||
case RelationshipType.Generalizes: soFar = RelationshipType.Generalizes; break;
|
||||
default: soFar = RelationshipType.Incomparable; break;
|
||||
}
|
||||
}
|
||||
soFar = BindingsRelationship(soFar, shorter, longer);
|
||||
}
|
||||
soFar = IgnoreRelationship(soFar, shorter, longer);
|
||||
return swapped ? soFar.Swap() : soFar;
|
||||
}
|
||||
|
||||
private RelationshipType BindingsRelationship(RelationshipType soFar, Clause shorterClause, Clause longerClause)
|
||||
{
|
||||
if (soFar == RelationshipType.Equal)
|
||||
{
|
||||
var swapped = false;
|
||||
var shorter = shorterClause.AnyBindings;
|
||||
var longer = longerClause.AnyBindings;
|
||||
if (shorterClause.AnyBindings.Count > longerClause.AnyBindings.Count)
|
||||
{
|
||||
shorter = longerClause.AnyBindings;
|
||||
longer = shorterClause.AnyBindings;
|
||||
swapped = true;
|
||||
}
|
||||
foreach (var shortBinding in shorter)
|
||||
{
|
||||
var found = false;
|
||||
foreach (var longBinding in longer)
|
||||
{
|
||||
if (shortBinding.Key == longBinding.Key && shortBinding.Value == longBinding.Value)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
soFar = RelationshipType.Incomparable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (soFar == RelationshipType.Equal && shorter.Count < longer.Count)
|
||||
{
|
||||
soFar = RelationshipType.Specializes;
|
||||
}
|
||||
soFar = Swap(soFar, swapped);
|
||||
}
|
||||
return soFar;
|
||||
}
|
||||
|
||||
private RelationshipType Swap(RelationshipType soFar, bool swapped)
|
||||
{
|
||||
if (swapped)
|
||||
{
|
||||
switch (soFar)
|
||||
{
|
||||
case RelationshipType.Specializes: soFar = RelationshipType.Generalizes; break;
|
||||
case RelationshipType.Generalizes: soFar = RelationshipType.Specializes; break;
|
||||
}
|
||||
}
|
||||
return soFar;
|
||||
}
|
||||
|
||||
private RelationshipType IgnoreRelationship(RelationshipType soFar, Clause shorterClause, Clause longerClause)
|
||||
{
|
||||
if (soFar == RelationshipType.Equal)
|
||||
{
|
||||
var shortIgnores = shorterClause.Predicates.Where(p => IsIgnore(p));
|
||||
var longIgnores = longerClause.Predicates.Where(p => IsIgnore(p));
|
||||
var swapped = false;
|
||||
if (longIgnores.Count() < shortIgnores.Count())
|
||||
{
|
||||
var old = longIgnores;
|
||||
longIgnores = shortIgnores;
|
||||
shortIgnores = old;
|
||||
swapped = true;
|
||||
}
|
||||
|
||||
foreach (var shortPredicate in shortIgnores)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (var longPredicate in longIgnores)
|
||||
{
|
||||
if (shortPredicate.DeepEquals(longPredicate))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
soFar = RelationshipType.Incomparable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (soFar == RelationshipType.Equal)
|
||||
{
|
||||
if (shorterClause.Predicates.Count() == 0 && longerClause.Predicates.Count() > 0)
|
||||
{
|
||||
soFar = RelationshipType.Generalizes;
|
||||
}
|
||||
else if (shortIgnores.Count() < longIgnores.Count())
|
||||
{
|
||||
soFar = RelationshipType.Incomparable;
|
||||
}
|
||||
}
|
||||
soFar = Swap(soFar, swapped);
|
||||
}
|
||||
return soFar;
|
||||
}
|
||||
|
||||
private RelationshipType Relationship(Expression expr, Expression other, Dictionary<string, IPredicateComparer> comparers)
|
||||
{
|
||||
RelationshipType relationship = RelationshipType.Incomparable;
|
||||
var root = expr;
|
||||
var rootOther = other;
|
||||
if (expr.NodeType == ExpressionType.Not && other.NodeType == ExpressionType.Not)
|
||||
{
|
||||
root = ((UnaryExpression)expr).Operand;
|
||||
rootOther = ((UnaryExpression)other).Operand;
|
||||
|
||||
}
|
||||
IPredicateComparer comparer = null;
|
||||
if (root.NodeType == ExpressionType.Call && rootOther.NodeType == ExpressionType.Call)
|
||||
{
|
||||
var name = ((MethodCallExpression)root).Method.Name;
|
||||
var nameOther = ((MethodCallExpression)rootOther).Method.Name;
|
||||
if (name == nameOther)
|
||||
{
|
||||
comparers.TryGetValue(name, out comparer);
|
||||
}
|
||||
}
|
||||
if (comparer != null)
|
||||
{
|
||||
relationship = comparer.Relationship(root, rootOther);
|
||||
}
|
||||
else
|
||||
{
|
||||
relationship = expr.DeepEquals(other) ? RelationshipType.Equal : RelationshipType.Incomparable;
|
||||
}
|
||||
return relationship;
|
||||
}
|
||||
|
||||
private static readonly MethodInfo IGNORE = typeof(TriggerTree).GetMethod("Ignore");
|
||||
private int PredicateCount()
|
||||
{
|
||||
return Predicates.Count(e => !IsIgnore(e));
|
||||
}
|
||||
|
||||
private bool IsIgnore(Expression expression)
|
||||
{
|
||||
return expression is MethodCallExpression call && call.Method == IGNORE;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
public static partial class Extensions
|
||||
{
|
||||
public static bool DeepEquals(this Expression expr, Expression other)
|
||||
{
|
||||
bool eq = true;
|
||||
if (expr != null && other != null)
|
||||
{
|
||||
eq = expr.NodeType == other.NodeType && expr.Type == other.Type;
|
||||
if (eq)
|
||||
{
|
||||
switch (expr.NodeType)
|
||||
{
|
||||
case ExpressionType.Call:
|
||||
{
|
||||
var call = (MethodCallExpression)expr;
|
||||
var callOther = (MethodCallExpression)other;
|
||||
eq = call.Object.DeepEquals(callOther.Object)
|
||||
&& call.Method == callOther.Method
|
||||
&& call.Arguments.Count == callOther.Arguments.Count;
|
||||
for (var i = 0; eq && i < call.Arguments.Count; ++i)
|
||||
{
|
||||
eq = call.Arguments[i].DeepEquals(callOther.Arguments[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Conditional:
|
||||
{
|
||||
var cond = (ConditionalExpression)expr;
|
||||
var condOther = (ConditionalExpression)other;
|
||||
eq = cond.Test.DeepEquals(condOther.Test) &&
|
||||
cond.IfFalse.DeepEquals(condOther.IfFalse) &&
|
||||
cond.IfTrue.DeepEquals(condOther.IfTrue);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Constant:
|
||||
{
|
||||
var constant = (ConstantExpression)expr;
|
||||
var constantOther = (ConstantExpression)other;
|
||||
if (constant.Value == null)
|
||||
{
|
||||
eq = constantOther.Value == null;
|
||||
}
|
||||
else
|
||||
{
|
||||
eq = constant.Value.Equals(constantOther.Value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.MemberAccess:
|
||||
{
|
||||
var member = (MemberExpression)expr;
|
||||
var memberOther = (MemberExpression)other;
|
||||
eq = member.Member.Equals(memberOther.Member)
|
||||
&& member.Expression.DeepEquals(memberOther.Expression);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.MemberInit:
|
||||
{
|
||||
var member = (MemberInitExpression)expr;
|
||||
var memberOther = (MemberInitExpression)other;
|
||||
eq = member.DeepEquals(memberOther);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.New:
|
||||
{
|
||||
var newExpr = (NewExpression)expr;
|
||||
var newOther = (NewExpression)other;
|
||||
eq = newExpr.Constructor.Equals(newOther.Constructor)
|
||||
&& newExpr.Arguments.Count == newOther.Arguments.Count;
|
||||
for (var i = 0; eq && i < newExpr.Arguments.Count; ++i)
|
||||
{
|
||||
eq = newExpr.Arguments[i].DeepEquals(newOther.Arguments[i]);
|
||||
}
|
||||
eq = eq && newExpr.Members?.Count == newOther.Members?.Count;
|
||||
if (newExpr.Members != null)
|
||||
{
|
||||
for (var i = 0; eq && i < newExpr.Members.Count; ++i)
|
||||
{
|
||||
eq = newExpr.Members[i].Equals(newOther.Members[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.NewArrayInit:
|
||||
{
|
||||
var init = (NewArrayExpression)expr;
|
||||
var initOther = (NewArrayExpression)other;
|
||||
eq = init.Expressions.Count == initOther.Expressions.Count;
|
||||
for (var i = 0; eq && i < init.Expressions.Count; ++i)
|
||||
{
|
||||
eq = init.Expressions[i].DeepEquals(initOther.Expressions[i]);
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Parameter:
|
||||
{
|
||||
var parameter = (ParameterExpression)expr;
|
||||
var parameterOther = (ParameterExpression)other;
|
||||
// NOTE: This assumes that all parameters of the same type are the same.
|
||||
// This is so you can use different names for the parameter across expressions.
|
||||
eq = parameter.Type == parameterOther.Type;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (expr is UnaryExpression unary && other is UnaryExpression unaryOther)
|
||||
{
|
||||
eq = unary.Operand.DeepEquals(unaryOther.Operand);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expr is BinaryExpression binary && other is BinaryExpression binaryOther)
|
||||
{
|
||||
eq = binary.Left.DeepEquals(binaryOther.Left)
|
||||
&& binary.Right.DeepEquals(binaryOther.Right);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"{expr.NodeType} is not handled");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return eq;
|
||||
}
|
||||
|
||||
public static bool DeepEquals(this MemberInitExpression init, MemberInitExpression other)
|
||||
{
|
||||
var eq =
|
||||
init.Bindings.Count == other.Bindings.Count
|
||||
&& init.NewExpression.DeepEquals(other.NewExpression);
|
||||
for (var i = 0; eq && i < init.Bindings.Count; ++i)
|
||||
{
|
||||
eq = init.Bindings[i].DeepEquals(other.Bindings[i]);
|
||||
}
|
||||
return eq;
|
||||
}
|
||||
|
||||
public static bool DeepEquals(this MemberBinding binding, MemberBinding bindingOther)
|
||||
{
|
||||
var eq = binding.Member.Equals(bindingOther.Member)
|
||||
&& binding.BindingType.Equals(bindingOther.BindingType);
|
||||
if (eq)
|
||||
{
|
||||
switch (binding.BindingType)
|
||||
{
|
||||
case MemberBindingType.Assignment:
|
||||
{
|
||||
var assignment = (MemberAssignment)binding;
|
||||
var assignmentOther = (MemberAssignment)bindingOther;
|
||||
eq = assignment.Expression.DeepEquals(assignmentOther.Expression);
|
||||
}
|
||||
break;
|
||||
case MemberBindingType.ListBinding:
|
||||
{
|
||||
var list = (MemberListBinding)binding;
|
||||
var listOther = (MemberListBinding)bindingOther;
|
||||
eq = list.Initializers.Count == listOther.Initializers.Count;
|
||||
for (var j = 0; eq && j < list.Initializers.Count; ++j)
|
||||
{
|
||||
var initializer = list.Initializers[j];
|
||||
var initializerOther = listOther.Initializers[j];
|
||||
eq = initializer.Arguments.Count == initializerOther.Arguments.Count;
|
||||
for (var k = 0; eq && k < initializer.Arguments.Count; ++k)
|
||||
{
|
||||
eq = initializer.Arguments[k].DeepEquals(initializerOther.Arguments[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MemberBindingType.MemberBinding:
|
||||
{
|
||||
eq = binding.DeepEquals(bindingOther);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return eq;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
public static partial class Extensions
|
||||
{
|
||||
public static RelationshipType Swap(this RelationshipType original)
|
||||
{
|
||||
var relationship = original;
|
||||
switch (original)
|
||||
{
|
||||
case RelationshipType.Specializes:
|
||||
relationship = RelationshipType.Generalizes;
|
||||
break;
|
||||
case RelationshipType.Generalizes:
|
||||
relationship = RelationshipType.Specializes;
|
||||
break;
|
||||
}
|
||||
return relationship;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate to test memory to see if a trigger expression is true.
|
||||
/// </summary>
|
||||
/// <param name="memory"></param>
|
||||
/// <returns></returns>
|
||||
public delegate bool Evaluator(IDictionary<string, object> memory);
|
||||
|
||||
/// <summary>
|
||||
/// Optimize a clause by rewriting it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If returned clause is null, then the expression will always be false.
|
||||
/// This is to allow things like combining simple comparisons into a range predicate.
|
||||
/// </remarks>
|
||||
public interface IOptimizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Optionally rewrite a clause.
|
||||
/// </summary>
|
||||
/// <param name="clause">Original clause.</param>
|
||||
/// <returns></returns>
|
||||
Clause Optimize(Clause clause);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Relationship between trigger expressions.
|
||||
/// </summary>
|
||||
public enum RelationshipType {
|
||||
/// <summary>
|
||||
/// First argument specializes the second, i.e. applies to a subset of the states the second argument covers.
|
||||
/// </summary>
|
||||
Specializes,
|
||||
|
||||
/// <summary>
|
||||
/// First and second argument are the same expression.
|
||||
/// </summary>
|
||||
Equal,
|
||||
|
||||
/// <summary>
|
||||
/// First argument generalizes the second, i.e. applies to a superset of the states the second argument covers.
|
||||
/// </summary>
|
||||
Generalizes,
|
||||
|
||||
/// <summary>
|
||||
/// Cannot tell how the first and second arguments relate.
|
||||
/// </summary>
|
||||
Incomparable };
|
||||
|
||||
/// <summary>
|
||||
/// Compare two predicates to identify the relationship between them.
|
||||
/// </summary>
|
||||
public interface IPredicateComparer
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of predicate.
|
||||
/// </summary>
|
||||
string Predicate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Identify the relationship between two predicates.
|
||||
/// </summary>
|
||||
/// <param name="predicate">First predicate.</param>
|
||||
/// <param name="other">Second predicate.</param>
|
||||
/// <returns>Relationship beteween predicates.</returns>
|
||||
/// <remarks>
|
||||
/// This is useful for doing things like identifying that Range("size", 1, 5) is more specialized than Range("size", 1, 10).
|
||||
/// </remarks>
|
||||
RelationshipType Relationship(Expression predicate, Expression other);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<RootNamespace>Microsoft.Bot.Builder.AI.TriggerTrees</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.CommonExpressions\Microsoft.Expressions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,525 @@
|
|||
// This will trace the whole process, but will generate a lot of output
|
||||
// #define TraceTree
|
||||
|
||||
// This adds a counter to each comparison when building the tree so that you can find it in the trace.
|
||||
// There is a node static count and boolean ShowTrace that can be turned on/off if needed.
|
||||
// #define Count
|
||||
|
||||
// This will verify the tree as it is built by checking invariants
|
||||
// #define VerifyTree
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
/// <summary>
|
||||
/// Node in a trigger tree.
|
||||
/// </summary>
|
||||
public class Node
|
||||
{
|
||||
/// <summary>
|
||||
/// All of the most specific triggers that contain the <see cref="Clause"/> in this node.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Trigger> Triggers { get { return _triggers; } }
|
||||
|
||||
/// <summary>
|
||||
/// All triggers that contain the <see cref="Clause"/> in this node.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Triggers only contain the most specific trigger, so if this node
|
||||
/// as Pred(A) and there was a rule R1: Pred(A) -> A1 and R2: Pred(A) v Pred(B) -> A2
|
||||
/// then the second trigger would be in AllTriggers, but not Triggers because it
|
||||
/// is more general.
|
||||
/// </remarks>
|
||||
public IReadOnlyList<Trigger> AllTriggers { get { return _allTriggers; } }
|
||||
|
||||
/// <summary>
|
||||
/// Specialized children of this node.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Node> Specializations { get { return _specializations; } }
|
||||
|
||||
/// <summary>
|
||||
/// The logical conjunction this node represents.
|
||||
/// </summary>
|
||||
public Clause Clause { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The tree this node is found in.
|
||||
/// </summary>
|
||||
public TriggerTree Tree { get; }
|
||||
|
||||
private List<Trigger> _allTriggers = new List<Trigger>();
|
||||
private List<Trigger> _triggers = new List<Trigger>();
|
||||
private List<Node> _specializations = new List<Node>();
|
||||
private readonly Evaluator _evaluator;
|
||||
|
||||
#if Count
|
||||
private static int _count = 0;
|
||||
#endif
|
||||
|
||||
#if TraceTree
|
||||
public static bool ShowTrace = true;
|
||||
#endif
|
||||
|
||||
internal Node(Clause clause, TriggerTree tree, Trigger trigger = null)
|
||||
{
|
||||
Clause = clause;
|
||||
Tree = tree;
|
||||
if (trigger != null)
|
||||
{
|
||||
_allTriggers.Add(trigger);
|
||||
_triggers.Add(trigger);
|
||||
if (Clause != null)
|
||||
{
|
||||
var firstPredicate = true;
|
||||
Expression body = null;
|
||||
foreach (var clausePredicate in Clause.Predicates)
|
||||
{
|
||||
var predicate = clausePredicate;
|
||||
var ignore = TriggerTree.GetIgnore(predicate);
|
||||
if (ignore != null)
|
||||
{
|
||||
predicate = ignore.Arguments[0];
|
||||
}
|
||||
if (firstPredicate)
|
||||
{
|
||||
firstPredicate = false;
|
||||
body = predicate;
|
||||
}
|
||||
else
|
||||
{
|
||||
body = Expression.AndAlso(body, predicate);
|
||||
}
|
||||
}
|
||||
if (body != null)
|
||||
{
|
||||
_evaluator = Expression.Lambda<Evaluator>(body, trigger.OriginalExpression.Parameters).Compile();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
ToString(builder);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public void ToString(StringBuilder builder, int indent = 0)
|
||||
{
|
||||
Clause.ToString(builder, indent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the most specific matches below this node.
|
||||
/// </summary>
|
||||
/// <param name="frame">Frame to evaluate against.</param>
|
||||
/// <returns>List of the most specific matches found.</returns>
|
||||
internal IReadOnlyList<Node> Matches(IDictionary<string, object> frame)
|
||||
{
|
||||
var matches = new List<Node>();
|
||||
Matches(frame, matches, new Dictionary<Node, bool>());
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identify the relationship between two nodes.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <returns>Relationship between this node and the other.</returns>
|
||||
public RelationshipType Relationship(Node other)
|
||||
{
|
||||
return Clause.Relationship(other.Clause, Tree.Comparers);
|
||||
}
|
||||
|
||||
private enum Operation { None, Found, Added, Removed, Inserted };
|
||||
|
||||
internal bool AddNode(Node triggerNode)
|
||||
{
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace)
|
||||
{
|
||||
Debug.WriteLine("");
|
||||
Debug.WriteLine($"***** Add Trigger {triggerNode.Triggers.First().OriginalExpression} *****");
|
||||
Debug.IndentSize = 2;
|
||||
}
|
||||
#endif
|
||||
return AddNode(triggerNode, new Dictionary<Node, Operation>()) == Operation.Added;
|
||||
}
|
||||
|
||||
internal bool RemoveTrigger(Trigger trigger)
|
||||
{
|
||||
var removed = new Dictionary<Node, Operation>();
|
||||
return RemoveTrigger(trigger, null, removed) == Operation.Removed;
|
||||
}
|
||||
|
||||
// In order to add a trigger we have to walk over the whole tree
|
||||
// If I am adding B and encounter A, A could have a specialization of A & B without B being present.
|
||||
private Operation AddNode(Node triggerNode, Dictionary<Node, Operation> ops)
|
||||
{
|
||||
var op = Operation.None;
|
||||
if (!ops.TryGetValue(this, out op))
|
||||
{
|
||||
var trigger = triggerNode.Triggers.First();
|
||||
var relationship = Relationship(triggerNode);
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace)
|
||||
{
|
||||
Debug.WriteLine("");
|
||||
#if Count
|
||||
Debug.Write($"{_count}:");
|
||||
#endif
|
||||
Debug.WriteLine(this);
|
||||
Debug.WriteLine($"{relationship}");
|
||||
Debug.WriteLine(triggerNode);
|
||||
}
|
||||
#endif
|
||||
#if Count
|
||||
++_count;
|
||||
#endif
|
||||
switch (relationship)
|
||||
{
|
||||
case RelationshipType.Equal:
|
||||
{
|
||||
// Ensure action is not already there
|
||||
bool found = false;
|
||||
foreach (var existing in _allTriggers)
|
||||
{
|
||||
if (trigger.Action != null && trigger.Action.Equals(existing.Action))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
op = Operation.Found;
|
||||
if (!found)
|
||||
{
|
||||
_allTriggers.Add(trigger);
|
||||
var add = true;
|
||||
for (var i = 0; i < _triggers.Count(); )
|
||||
{
|
||||
var existing = _triggers[i];
|
||||
var reln = trigger.Relationship(existing, Tree.Comparers);
|
||||
if (reln == RelationshipType.Generalizes)
|
||||
{
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine($"Trigger specialized by {existing}");
|
||||
#endif
|
||||
add = false;
|
||||
break;
|
||||
}
|
||||
else if (reln == RelationshipType.Specializes)
|
||||
{
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine($"Trigger replaces {existing}");
|
||||
#endif
|
||||
_triggers.RemoveAt(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
if (add)
|
||||
{
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine("Add trigger");
|
||||
#endif
|
||||
_triggers.Add(trigger);
|
||||
}
|
||||
#if DEBUG
|
||||
Debug.Assert(CheckInvariants());
|
||||
#endif
|
||||
op = Operation.Added;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RelationshipType.Incomparable:
|
||||
{
|
||||
foreach (var child in _specializations)
|
||||
{
|
||||
child.AddNode(triggerNode, ops);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RelationshipType.Specializes:
|
||||
{
|
||||
triggerNode.AddSpecialization(this);
|
||||
#if DEBUG
|
||||
Debug.Assert(triggerNode.CheckInvariants());
|
||||
#endif
|
||||
op = Operation.Inserted;
|
||||
}
|
||||
break;
|
||||
case RelationshipType.Generalizes:
|
||||
{
|
||||
bool foundOne = false;
|
||||
List<Node> removals = null;
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) ++Debug.IndentLevel;
|
||||
#endif
|
||||
foreach (var child in _specializations)
|
||||
{
|
||||
var childOp = child.AddNode(triggerNode, ops);
|
||||
if (childOp != Operation.None)
|
||||
{
|
||||
foundOne = true;
|
||||
if (childOp == Operation.Inserted)
|
||||
{
|
||||
if (removals == null)
|
||||
{
|
||||
removals = new List<Node>();
|
||||
}
|
||||
removals.Add(child);
|
||||
op = Operation.Added;
|
||||
}
|
||||
else
|
||||
{
|
||||
op = childOp;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) --Debug.IndentLevel;
|
||||
#endif
|
||||
if (removals != null)
|
||||
{
|
||||
foreach (var child in removals)
|
||||
{
|
||||
_specializations.Remove(child);
|
||||
}
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace)
|
||||
{
|
||||
Debug.WriteLine("Generalized");
|
||||
foreach (var removal in removals)
|
||||
{
|
||||
Debug.WriteLine(removal);
|
||||
}
|
||||
Debug.WriteLine($"in {this}");
|
||||
}
|
||||
#endif
|
||||
_specializations.Add(triggerNode);
|
||||
#if DEBUG
|
||||
Debug.Assert(CheckInvariants());
|
||||
#endif
|
||||
}
|
||||
if (!foundOne)
|
||||
{
|
||||
_specializations.Add(triggerNode);
|
||||
#if DEBUG
|
||||
Debug.Assert(triggerNode.CheckInvariants());
|
||||
#endif
|
||||
op = Operation.Added;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Prevent visiting this node again
|
||||
ops[this] = op;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private bool CheckInvariants()
|
||||
{
|
||||
#if VerifyTree
|
||||
foreach (var child in _specializations)
|
||||
{
|
||||
var reln = Relationship(child);
|
||||
Debug.Assert(reln == RelationshipType.Generalizes);
|
||||
}
|
||||
for (var i = 0; i < _specializations.Count; ++i)
|
||||
{
|
||||
var first = _specializations[i];
|
||||
for (var j = i + 1; j < _specializations.Count; ++j)
|
||||
{
|
||||
var second = _specializations[j];
|
||||
var reln = first.Relationship(second);
|
||||
Debug.Assert(reln == RelationshipType.Incomparable);
|
||||
}
|
||||
}
|
||||
// Triggers should be incomparable
|
||||
for (var i = 0; i < _triggers.Count(); ++i)
|
||||
{
|
||||
for (var j = i + 1; j < _triggers.Count(); ++j)
|
||||
{
|
||||
var reln = _triggers[i].Relationship(_triggers[j], Tree.Comparers);
|
||||
if (reln == RelationshipType.Specializes || reln == RelationshipType.Generalizes)
|
||||
{
|
||||
Debug.Assert(false, $"{this} triggers overlap");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// All triggers should all be found in triggers
|
||||
for (var i = 0; i < _allTriggers.Count(); ++i)
|
||||
{
|
||||
var allTrigger = _allTriggers[i];
|
||||
var found = false;
|
||||
for (var j = 0; j < _triggers.Count(); ++j)
|
||||
{
|
||||
var trigger = _triggers[j];
|
||||
var reln = allTrigger.Relationship(trigger, Tree.Comparers);
|
||||
if (allTrigger == trigger || reln == RelationshipType.Generalizes)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
Debug.Assert(false, $"{this} missing all trigger {allTrigger}");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
private bool AddSpecialization(Node specialization)
|
||||
{
|
||||
var added = false;
|
||||
List<Node> removals = null;
|
||||
bool skip = false;
|
||||
foreach (var child in _specializations)
|
||||
{
|
||||
var reln = specialization.Relationship(child);
|
||||
if (reln == RelationshipType.Equal)
|
||||
{
|
||||
skip = true;
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine($"Already has child");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
if (reln == RelationshipType.Generalizes)
|
||||
{
|
||||
if (removals == null) removals = new List<Node>();
|
||||
removals.Add(child);
|
||||
}
|
||||
else if (reln == RelationshipType.Specializes)
|
||||
{
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine($"Specialized by {child}");
|
||||
#endif
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!skip)
|
||||
{
|
||||
if (removals != null)
|
||||
{
|
||||
foreach (var removal in removals)
|
||||
{
|
||||
// Don't need to add back because specialization already has them
|
||||
|
||||
_specializations.Remove(removal);
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace)
|
||||
{
|
||||
Debug.WriteLine($"Replaced {removal}");
|
||||
++Debug.IndentLevel;
|
||||
}
|
||||
#endif
|
||||
specialization.AddSpecialization(removal);
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) --Debug.IndentLevel;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
_specializations.Add(specialization);
|
||||
added = true;
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine("Added as specialization");
|
||||
#endif
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
// TODO: Implement remove and drop parent
|
||||
// Need to make sure we update triggers from allTriggers as well.
|
||||
private Operation RemoveTrigger(Trigger trigger, Node parent, Dictionary<Node, Operation> removed)
|
||||
{
|
||||
var op = Operation.None;
|
||||
/*
|
||||
if (!removed.TryGetValue(this, out op))
|
||||
{
|
||||
var relationship = trigger.Relationship(_triggers.First());
|
||||
#if TraceTree
|
||||
Debug.WriteLine(trigger.ToString());
|
||||
Debug.WriteLine($"Remove {relationship}");
|
||||
Debug.WriteLine(ToString());
|
||||
Debug.WriteLine("");
|
||||
#endif
|
||||
switch (relationship)
|
||||
{
|
||||
case RelationshipType.Equal:
|
||||
{
|
||||
if (_triggers.Remove(trigger))
|
||||
{
|
||||
op = Operation.Removed;
|
||||
}
|
||||
else
|
||||
{
|
||||
op = Operation.Found;
|
||||
}
|
||||
if (parent != null && _triggers.Count == 0)
|
||||
{
|
||||
parent._specializations.AddRange(_specializations);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RelationshipType.Specializes:
|
||||
{
|
||||
foreach (var child in new List<Node>(_specializations))
|
||||
{
|
||||
op = child.RemoveTrigger(trigger, this, removed);
|
||||
if (op != Operation.None)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
removed[this] = op;
|
||||
}
|
||||
*/
|
||||
return op;
|
||||
}
|
||||
|
||||
private bool Matches(IDictionary<string, object> memory, List<Node> matches, Dictionary<Node, bool> matched)
|
||||
{
|
||||
if (!matched.TryGetValue(this, out bool found))
|
||||
{
|
||||
found = false;
|
||||
foreach (var child in _specializations)
|
||||
{
|
||||
if (child.Matches(memory, matches, matched))
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
// No child matched so we might
|
||||
if (!found)
|
||||
{
|
||||
if (_evaluator == null ? Triggers.Any() : _evaluator(memory))
|
||||
{
|
||||
matches.Add(this);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
matched.Add(this, found);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of quantifier for expanding trigger expressions.
|
||||
/// </summary>
|
||||
public enum QuantifierType {
|
||||
/// <summary>
|
||||
/// Within a clause, duplicate any predicate with variable for each possible binding.
|
||||
/// </summary>
|
||||
All,
|
||||
|
||||
/// <summary>
|
||||
/// Create a new clause for each possible binding of variable.
|
||||
/// </summary>
|
||||
Any };
|
||||
|
||||
/// <summary>
|
||||
/// Quantifier for allowing runtime expansion of expressions.
|
||||
/// </summary>
|
||||
public class Quantifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of variable that will be replaced.
|
||||
/// </summary>
|
||||
public string Variable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of quantifier.
|
||||
/// </summary>
|
||||
public QuantifierType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Possible bindings for quantifier.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Bindings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a quantifier.
|
||||
/// </summary>
|
||||
/// <param name="variable">Name of variable to replace.</param>
|
||||
/// <param name="type">Type of quantifier.</param>
|
||||
/// <param name="bindings">Possible bindings for variable.</param>
|
||||
public Quantifier(string variable, QuantifierType type, IEnumerable<string> bindings)
|
||||
{
|
||||
Variable = variable;
|
||||
Type = type;
|
||||
Bindings = bindings;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} {Variable} {Bindings.Count()}";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,626 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Expressions;
|
||||
using Antlr4.Runtime.Tree;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
/// <summary>
|
||||
/// A trigger is a combination of a trigger expression and the corresponding action.
|
||||
/// </summary>
|
||||
public class Trigger
|
||||
{
|
||||
/// <summary>
|
||||
/// Original trigger expression.
|
||||
/// </summary>
|
||||
public string OriginalExpression;
|
||||
|
||||
private IParseTree _parse;
|
||||
private TriggerTree _tree;
|
||||
private IEnumerable<Quantifier> _quantifiers;
|
||||
private List<Clause> _clauses;
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when trigger is true.
|
||||
/// </summary>
|
||||
public object Action { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Expressions are converted into Disjunctive Normal Form where ! is pushed to the leaves and there is an implicit || between clauses and && within a clause.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Clause> Clauses { get { return _clauses; } }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a trigger expression.
|
||||
/// </summary>
|
||||
/// <param name="tree">Trigger tree that contains this trigger.</param>
|
||||
/// <param name="expression">Expression for when the trigger action is possible.</param>
|
||||
/// <param name="action">Action to take when a trigger matches.</param>
|
||||
/// <param name="quantifiers">Quantifiers to dynamically expand the expression.</param>
|
||||
internal Trigger(TriggerTree tree, string expression, object action, params Quantifier[] quantifiers)
|
||||
{
|
||||
_parse = ExpressionEngine.Parse(expression);
|
||||
_tree = tree;
|
||||
Action = action;
|
||||
OriginalExpression = expression;
|
||||
_quantifiers = quantifiers;
|
||||
if (expression != null)
|
||||
{
|
||||
var notNormalized = PushDownNot(_parse, false);
|
||||
_clauses = GenerateClauses(notNormalized).ToList();
|
||||
RemoveDuplicatedPredicates();
|
||||
OptimizeClauses();
|
||||
ExpandQuantifiers();
|
||||
RemoveDuplicates();
|
||||
MarkSubsumedClauses();
|
||||
}
|
||||
else
|
||||
{
|
||||
_clauses = new List<Clause>();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
ToString(builder);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public RelationshipType Relationship(Trigger other, Dictionary<string, IPredicateComparer> comparers)
|
||||
{
|
||||
RelationshipType result;
|
||||
var first = Relationship(this, other, comparers);
|
||||
var second = Relationship(other, this, comparers);
|
||||
if (first == RelationshipType.Equal)
|
||||
{
|
||||
if (second == RelationshipType.Equal)
|
||||
{
|
||||
// All first clauses == second clauses
|
||||
result = RelationshipType.Equal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// All first clauses found in second
|
||||
result = RelationshipType.Specializes;
|
||||
}
|
||||
}
|
||||
else if (first == RelationshipType.Specializes)
|
||||
{
|
||||
// All first clauses specialize or equal a second clause
|
||||
result = RelationshipType.Specializes;
|
||||
}
|
||||
else if (second == RelationshipType.Equal || second == RelationshipType.Specializes)
|
||||
{
|
||||
// All second clauses are equal or specialize a first clause
|
||||
result = RelationshipType.Generalizes;
|
||||
}
|
||||
else
|
||||
{
|
||||
// All other cases are incomparable
|
||||
result = RelationshipType.Incomparable;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private RelationshipType Relationship(Trigger trigger, Trigger other, Dictionary<string, IPredicateComparer> comparers)
|
||||
{
|
||||
var soFar = RelationshipType.Incomparable;
|
||||
foreach (var clause in trigger.Clauses)
|
||||
{
|
||||
if (!clause.Subsumed)
|
||||
{
|
||||
// Check other for = or clause that is specialized
|
||||
var clauseSoFar = RelationshipType.Incomparable;
|
||||
foreach (var second in other.Clauses)
|
||||
{
|
||||
if (!second.Subsumed)
|
||||
{
|
||||
var reln = clause.Relationship(second, comparers);
|
||||
if (reln == RelationshipType.Equal || reln == RelationshipType.Specializes)
|
||||
{
|
||||
clauseSoFar = reln;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clauseSoFar == RelationshipType.Incomparable || clauseSoFar == RelationshipType.Generalizes)
|
||||
{
|
||||
// Some clause is not comparable
|
||||
soFar = RelationshipType.Incomparable;
|
||||
break;
|
||||
}
|
||||
else if (clauseSoFar == RelationshipType.Equal)
|
||||
{
|
||||
if (soFar == RelationshipType.Incomparable)
|
||||
{
|
||||
// Start on equal clause
|
||||
soFar = clauseSoFar;
|
||||
}
|
||||
}
|
||||
else if (clauseSoFar == RelationshipType.Specializes)
|
||||
{
|
||||
// Either going from incomparable or equal to specializes
|
||||
soFar = clauseSoFar;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Either incomparable, equal or specializes
|
||||
return soFar;
|
||||
}
|
||||
|
||||
protected void ToString(StringBuilder builder, int indent = 0)
|
||||
{
|
||||
builder.Append(' ', indent);
|
||||
if (_clauses.Any())
|
||||
{
|
||||
bool first = true;
|
||||
foreach (var clause in _clauses)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(' ', indent);
|
||||
builder.Append("|| ");
|
||||
}
|
||||
builder.Append(clause.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append("<Empty>");
|
||||
}
|
||||
}
|
||||
|
||||
// Push not down to leaves using De Morgan's rule
|
||||
private Expression PushDownNot(IParseTree expression, bool inNot)
|
||||
{
|
||||
var e = new Parse
|
||||
Expression newExpr = expression;
|
||||
var unary = expression as UnaryExpression;
|
||||
var binary = expression as BinaryExpression;
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.AndAlso:
|
||||
{
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = System.Linq.Expressions.Expression.OrElse(PushDownNot(binary.Left, true), PushDownNot(binary.Right, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
newExpr = System.Linq.Expressions.Expression.AndAlso(PushDownNot(binary.Left, false), PushDownNot(binary.Right, false));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.OrElse:
|
||||
{
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = System.Linq.Expressions.Expression.AndAlso(PushDownNot(binary.Left, true), PushDownNot(binary.Right, true));
|
||||
}
|
||||
else
|
||||
{
|
||||
newExpr = System.Linq.Expressions.Expression.OrElse(PushDownNot(binary.Left, false), PushDownNot(binary.Right, false));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Not:
|
||||
newExpr = PushDownNot(((UnaryExpression)expression).Operand, !inNot);
|
||||
break;
|
||||
// Rewrite comparison operators
|
||||
case ExpressionType.LessThan:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.GreaterThanOrEqual(binary.Left, binary.Right);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.LessThanOrEqual:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.GreaterThan(binary.Left, binary.Right);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Equal:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.NotEqual(binary.Left, binary.Right);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.GreaterThanOrEqual:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.LessThan(binary.Left, binary.Right);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.GreaterThan:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.LessThanOrEqual(binary.Left, binary.Right);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Call:
|
||||
{
|
||||
var special = TriggerTree.GetOptional(expression) ?? TriggerTree.GetIgnore(expression);
|
||||
if (special != null)
|
||||
{
|
||||
// Pass not through optional/ignore
|
||||
newExpr = Expression.Call(special.Method, PushDownNot(special.Arguments[0], inNot));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.Not(expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = Expression.Not(expression);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return newExpr;
|
||||
}
|
||||
|
||||
private IEnumerable<Expression> OrLeaves(Expression expression)
|
||||
{
|
||||
if (expression.NodeType == ExpressionType.OrElse)
|
||||
{
|
||||
var or = (BinaryExpression)expression;
|
||||
foreach (var leaf in OrLeaves(or.Left))
|
||||
{
|
||||
yield return leaf;
|
||||
}
|
||||
foreach (var leaf in OrLeaves(or.Right))
|
||||
{
|
||||
yield return leaf;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return expression;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Clause> GenerateClauses(Expression expression)
|
||||
{
|
||||
var binary = expression as BinaryExpression;
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.AndAlso:
|
||||
{
|
||||
var rightClauses = GenerateClauses(binary.Right);
|
||||
foreach (var left in GenerateClauses(binary.Left))
|
||||
{
|
||||
foreach (var right in rightClauses)
|
||||
{
|
||||
var clause = new Clause();
|
||||
clause.Predicates.AddRange(left.Predicates);
|
||||
clause.Predicates.AddRange(right.Predicates);
|
||||
yield return clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.OrElse:
|
||||
{
|
||||
foreach (var left in GenerateClauses(binary.Left))
|
||||
{
|
||||
yield return left;
|
||||
}
|
||||
foreach (var right in GenerateClauses(binary.Right))
|
||||
{
|
||||
yield return right;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Call:
|
||||
{
|
||||
var optional = TriggerTree.GetOptional(expression);
|
||||
if (optional != null)
|
||||
{
|
||||
yield return new Clause();
|
||||
foreach (var clause in GenerateClauses(optional.Arguments[0]))
|
||||
{
|
||||
yield return clause;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new Clause(expression);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
yield return new Clause(expression);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any duplicate predicates within a clause
|
||||
// NOTE: This is annoying but expression hash codes of DeepEquals expressions are different
|
||||
private void RemoveDuplicatedPredicates()
|
||||
{
|
||||
// Rewrite clauses to remove duplicated tests
|
||||
for (var i = 0; i < _clauses.Count(); ++i)
|
||||
{
|
||||
var clause = _clauses[i];
|
||||
var newClause = new Clause();
|
||||
for (var p = 0; p < clause.Predicates.Count(); ++p)
|
||||
{
|
||||
var pred = clause.Predicates[p];
|
||||
var found = false;
|
||||
for (var q = p + 1; q < clause.Predicates.Count(); ++q)
|
||||
{
|
||||
if (pred.DeepEquals(clause.Predicates[q]))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
newClause.Predicates.Add(pred);
|
||||
}
|
||||
}
|
||||
_clauses[i] = newClause;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark clauses that are more specific than another clause as subsumed and also remove any = clauses.
|
||||
private void MarkSubsumedClauses()
|
||||
{
|
||||
for (var i = 0; i < _clauses.Count(); ++i)
|
||||
{
|
||||
var clause = _clauses[i];
|
||||
if (!clause.Subsumed)
|
||||
{
|
||||
for (var j = i + 1; j < _clauses.Count(); ++j)
|
||||
{
|
||||
var other = _clauses[j];
|
||||
if (!other.Subsumed)
|
||||
{
|
||||
var reln = clause.Relationship(other, _tree.Comparers);
|
||||
if (reln == RelationshipType.Equal)
|
||||
{
|
||||
_clauses.RemoveAt(j);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (reln == RelationshipType.Specializes)
|
||||
{
|
||||
clause.Subsumed = true;
|
||||
break;
|
||||
}
|
||||
else if (reln == RelationshipType.Generalizes)
|
||||
{
|
||||
other.Subsumed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OptimizeClauses()
|
||||
{
|
||||
foreach (var clause in _clauses)
|
||||
{
|
||||
foreach (var optimizer in _tree.Optimizers)
|
||||
{
|
||||
optimizer.Optimize(clause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExpandQuantifiers()
|
||||
{
|
||||
if (_quantifiers != null && _quantifiers.Any())
|
||||
{
|
||||
foreach (var quantifier in _quantifiers)
|
||||
{
|
||||
var newClauses = new List<Clause>();
|
||||
foreach (var clause in _clauses)
|
||||
{
|
||||
newClauses.AddRange(ExpandQuantifier(quantifier, clause));
|
||||
}
|
||||
_clauses = newClauses;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Expression SubstituteVariable(string variable, string binding, Expression expression, ref bool changed)
|
||||
{
|
||||
var newExpr = expression;
|
||||
var unary = expression as UnaryExpression;
|
||||
var binary = expression as BinaryExpression;
|
||||
switch (expression.NodeType)
|
||||
{
|
||||
case ExpressionType.Call:
|
||||
{
|
||||
var call = (MethodCallExpression)expression;
|
||||
var args = new List<Expression>();
|
||||
foreach (var arg in call.Arguments)
|
||||
{
|
||||
args.Add(SubstituteVariable(variable, binding, arg, ref changed));
|
||||
}
|
||||
newExpr = System.Linq.Expressions.Expression.Call(call.Object, call.Method, args);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Not:
|
||||
newExpr = System.Linq.Expressions.Expression.Not(SubstituteVariable(variable, binding, unary.Operand, ref changed));
|
||||
break;
|
||||
case ExpressionType.LessThan:
|
||||
newExpr = System.Linq.Expressions.Expression.LessThan(
|
||||
SubstituteVariable(variable, binding, binary.Left, ref changed),
|
||||
SubstituteVariable(variable, binding, binary.Right, ref changed));
|
||||
break;
|
||||
case ExpressionType.LessThanOrEqual:
|
||||
newExpr = System.Linq.Expressions.Expression.LessThanOrEqual(
|
||||
SubstituteVariable(variable, binding, binary.Left, ref changed),
|
||||
SubstituteVariable(variable, binding, binary.Right, ref changed));
|
||||
break;
|
||||
case ExpressionType.Equal:
|
||||
newExpr = System.Linq.Expressions.Expression.Equal(
|
||||
SubstituteVariable(variable, binding, binary.Left, ref changed),
|
||||
SubstituteVariable(variable, binding, binary.Right, ref changed));
|
||||
break;
|
||||
case ExpressionType.NotEqual:
|
||||
newExpr = System.Linq.Expressions.Expression.NotEqual(
|
||||
SubstituteVariable(variable, binding, binary.Left, ref changed),
|
||||
SubstituteVariable(variable, binding, binary.Right, ref changed));
|
||||
break;
|
||||
case ExpressionType.GreaterThan:
|
||||
newExpr = System.Linq.Expressions.Expression.GreaterThan(
|
||||
SubstituteVariable(variable, binding, binary.Left, ref changed),
|
||||
SubstituteVariable(variable, binding, binary.Right, ref changed));
|
||||
break;
|
||||
case ExpressionType.GreaterThanOrEqual:
|
||||
newExpr = System.Linq.Expressions.Expression.GreaterThanOrEqual(
|
||||
SubstituteVariable(variable, binding, binary.Left, ref changed),
|
||||
SubstituteVariable(variable, binding, binary.Right, ref changed));
|
||||
break;
|
||||
case ExpressionType.Constant:
|
||||
{
|
||||
var constant = (ConstantExpression)expression;
|
||||
if (constant.Type == typeof(string) && (string)constant.Value == variable)
|
||||
{
|
||||
newExpr = System.Linq.Expressions.Expression.Constant(binding);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Convert:
|
||||
newExpr = System.Linq.Expressions.Expression.Convert(SubstituteVariable(variable, binding, unary.Operand, ref changed), unary.Type);
|
||||
break;
|
||||
case ExpressionType.ConvertChecked:
|
||||
newExpr = System.Linq.Expressions.Expression.ConvertChecked(SubstituteVariable(variable, binding, unary.Operand, ref changed), unary.Type);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return newExpr;
|
||||
}
|
||||
|
||||
private IEnumerable<Clause> ExpandQuantifier(Quantifier quantifier, Clause clause)
|
||||
{
|
||||
if (quantifier.Type == QuantifierType.All)
|
||||
{
|
||||
var newClause = new Clause(clause);
|
||||
if (quantifier.Bindings.Any())
|
||||
{
|
||||
foreach (var predicate in clause.Predicates)
|
||||
{
|
||||
foreach (var binding in quantifier.Bindings)
|
||||
{
|
||||
var changed = false;
|
||||
var newPredicate = SubstituteVariable(quantifier.Variable, binding, predicate, ref changed);
|
||||
newClause.Predicates.Add(newPredicate);
|
||||
if (!changed)
|
||||
{
|
||||
// No change to first predicate, so can stop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Empty quantifier is trivially true so remove any predicate that refers to quantifier
|
||||
foreach (var predicate in clause.Predicates)
|
||||
{
|
||||
var changed = false;
|
||||
SubstituteVariable(quantifier.Variable, string.Empty, predicate, ref changed);
|
||||
if (!changed)
|
||||
{
|
||||
newClause.Predicates.Add(predicate);
|
||||
}
|
||||
}
|
||||
}
|
||||
yield return newClause;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (quantifier.Bindings.Any())
|
||||
{
|
||||
var changed = false;
|
||||
foreach (var binding in quantifier.Bindings)
|
||||
{
|
||||
var newClause = new Clause(clause);
|
||||
foreach (var predicate in clause.Predicates)
|
||||
{
|
||||
var newPredicate = SubstituteVariable(quantifier.Variable, binding, predicate, ref changed);
|
||||
newClause.Predicates.Add(newPredicate);
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
newClause.AnyBindings.Add(quantifier.Variable, binding);
|
||||
}
|
||||
yield return newClause;
|
||||
if (!changed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep clause if does not contain any binding
|
||||
var changed = false;
|
||||
foreach (var predicate in clause.Predicates)
|
||||
{
|
||||
SubstituteVariable(quantifier.Variable, string.Empty, predicate, ref changed);
|
||||
if (changed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!changed)
|
||||
{
|
||||
yield return clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDuplicates()
|
||||
{
|
||||
foreach (var clause in _clauses)
|
||||
{
|
||||
// NOTE: This is quadratic in clause length but GetHashCode is not equal for expressions and we expect the number of clauses to be small.
|
||||
var predicates = clause.Predicates;
|
||||
for (var i = 0; i < predicates.Count; ++i)
|
||||
{
|
||||
var first = predicates[i];
|
||||
for (var j = i + 1; j < predicates.Count;)
|
||||
{
|
||||
var second = predicates[j];
|
||||
if (first.DeepEquals(second))
|
||||
{
|
||||
predicates.RemoveAt(j);
|
||||
}
|
||||
else
|
||||
{
|
||||
++j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
||||
{
|
||||
// Each trigger is normalized to disjunctive normal form and then expanded with quantifiers.
|
||||
// Each of those clauses is then put into a DAG where the most restrictive clauses are at the bottom.
|
||||
// When matching the most specific clauses block out any more general clauses.
|
||||
//
|
||||
// Disjunctions and quantification do not change the tree construction, but they are used in determing
|
||||
// what triggers are returned. For example, from a strictly logical sense A&B v C&D is more general then A&B or C. If we had these rules:
|
||||
// R1(A)
|
||||
// R2(A&B)
|
||||
// R3(A&BvC&D)
|
||||
// R4(C)
|
||||
// Then from a strictly logic viewpoint the tree should be:
|
||||
// Root
|
||||
// | | |
|
||||
// R3(A&B v C&D) R1(A) R4(C)
|
||||
// | /
|
||||
// R2(A&B)
|
||||
// The problem is that from the developer standpoint R3 is more of a shortcut for two rules, i.e.A&B and another rule for C&D.
|
||||
// In the tree above if you had C&D you would get both R3 and R4—which does not seem like what you really want.
|
||||
// Even though R3 is a disjunction, C&D is more specific than just C.
|
||||
// The fix is build the tree just based on the conjunctions and then filter triggers on a specific clause so that more specific triggers remove more general ones, i.e. disjunctions.
|
||||
// This is what the correspoinding tree looks like:
|
||||
// Root
|
||||
// | |
|
||||
// A: R1(A) C: R4(C)
|
||||
// | |
|
||||
// A&B: R2(A&B), R3(A&BvC&D) C&D: R3(A&BvC&D)
|
||||
// If you had A&B you can look at the triggers and return R2 instead of R3—that seems appropriate.
|
||||
// But, if you also had C&D at the same time you would still get R3 triggering because of C&D, I think this is the right thing.
|
||||
// Even though R3 was filtered out of the A&B branch, it is still the most specific answer in the C&D branch.
|
||||
// If we remove R3 all together then we would end up returning R4 instead which doesn’t seem correct from the standpoint of disjunctions being a shortcut for multiple rules.
|
||||
|
||||
/// <summary>
|
||||
/// A trigger tree organizes evaluators according to generalization/specialization in order to make it easier to use rules.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A trigger expression generates true if the expression evaluated on a frame is true.
|
||||
/// The expression itself consists of arbitrary boolean functions ("predicates") combined with && || !.
|
||||
/// Most predicates are expressed over the frame passed in, but they can be anything--there are even ways of optimizing or comparing them.
|
||||
/// By organizing evaluators into a tree (techinically a DAG) it becomes easier to use rules by reducing the coupling between rules.
|
||||
/// For example if a rule applies if some predicate A is true, then another rule that applies if A && B are true is
|
||||
/// more specialized. If the second expression is true, then because we know of the relationship we can ignore the first
|
||||
/// rule--even though its expression is true. Without this kind of capability in order to add the second rule, you would
|
||||
/// have to change the first to become A && !B.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("{ToString()}"), DebuggerTypeProxy(typeof(Debugger))]
|
||||
public class TriggerTree
|
||||
{
|
||||
public List<IOptimizer> Optimizers = new List<IOptimizer>();
|
||||
public Dictionary<string, IPredicateComparer> Comparers = new Dictionary<string, IPredicateComparer>();
|
||||
public Node Root;
|
||||
public int TotalTriggers = 0;
|
||||
|
||||
private class Debugger
|
||||
{
|
||||
public string TreeString;
|
||||
public List<IOptimizer> _optimizers;
|
||||
public Dictionary<string, IPredicateComparer> _comparers;
|
||||
public Debugger(TriggerTree triggers)
|
||||
{
|
||||
TreeString = triggers.TreeToString();
|
||||
_optimizers = triggers.Optimizers;
|
||||
_comparers = triggers.Comparers;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to mark a trigger expression as optional.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When an expression is being processed, optional creates a disjunction where the expression is both included and not
|
||||
/// included with the rest of the expression. This is a simple way to express this common relationship. By generating
|
||||
/// both clauses then matching the expression can be more specific when the optional expression is true.
|
||||
/// </remarks>
|
||||
/// <param name="flag">Optional expression.</param>
|
||||
/// <returns>True</returns>
|
||||
public static bool Optional(bool flag)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly MethodInfo OPTIONAL = typeof(TriggerTree).GetMethod("Optional");
|
||||
internal static bool IsOptional(Expression expression) => expression is MethodCallExpression call && call.Method == OPTIONAL;
|
||||
internal static MethodCallExpression GetOptional(Expression expression)
|
||||
{
|
||||
return expression is MethodCallExpression call && call.Method == OPTIONAL ? call : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Any predicate expression wrapped in this will be ignored for specialization.
|
||||
/// </summary>
|
||||
/// <param name="flag">Boolean expression.</param>
|
||||
/// <returns>Flag value.</returns>
|
||||
/// <remarks>
|
||||
/// This is useful for when you need to add expression to the trigger that are part of rule mechanics rather than of intent.
|
||||
/// For example, if you have a counter for how often a particular message is displayed, then that is part of the triggering condition,
|
||||
/// but all such rules would be incomparable because they counter is per-rule.
|
||||
/// </remarks>
|
||||
public static bool Ignore(bool flag)
|
||||
{
|
||||
return flag;
|
||||
}
|
||||
|
||||
private static readonly MethodInfo IGNORE = typeof(TriggerTree).GetMethod("Ignore");
|
||||
internal static bool IsIgnore(Expression expression) => expression is MethodCallExpression call && call.Method == IGNORE;
|
||||
internal static MethodCallExpression GetIgnore(Expression expression)
|
||||
{
|
||||
return expression is MethodCallExpression call && call.Method == IGNORE ? call : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct an empty trigger tree.
|
||||
/// </summary>
|
||||
public TriggerTree()
|
||||
{
|
||||
Root = new Node(new Clause(), this);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"TriggerTree with {TotalTriggers} triggers";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a trigger expression to the tree.
|
||||
/// </summary>
|
||||
/// <param name="expression">Trigger to add</param>
|
||||
/// <param name="action">Action when triggered.</param>
|
||||
/// <param name="quantifiers">Quantifiers to use when expanding expressions.</param>
|
||||
/// <returns>New trigger.</returns>
|
||||
public Trigger AddTrigger(string expression, object action, params Quantifier[] quantifiers)
|
||||
{
|
||||
var trigger = new Trigger(this, expression, action, quantifiers);
|
||||
var added = false;
|
||||
if (trigger.Clauses.Any())
|
||||
{
|
||||
foreach (var clause in trigger.Clauses)
|
||||
{
|
||||
var newNode = new Node(clause, this, trigger);
|
||||
if (Root.AddNode(newNode))
|
||||
{
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
added = Root.AddNode(new Node(new Clause(), this, trigger));
|
||||
}
|
||||
if (added)
|
||||
{
|
||||
++TotalTriggers;
|
||||
}
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove trigger from tree.
|
||||
/// </summary>
|
||||
/// <param name="trigger">Trigger to remove.</param>
|
||||
/// <returns>True if removed trigger.</returns>
|
||||
public bool RemoveTrigger(Trigger trigger)
|
||||
{
|
||||
var result = Root.RemoveTrigger(trigger);
|
||||
if (result)
|
||||
{
|
||||
--TotalTriggers;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a string describing the tree.
|
||||
/// </summary>
|
||||
/// <param name="indent">Current indent level.</param>
|
||||
/// <returns>string describing the tree.</returns>
|
||||
public string TreeToString(int indent = 0)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
TreeToString(builder, Root, indent);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void TreeToString(StringBuilder builder, Node node, int indent)
|
||||
{
|
||||
node.ToString(builder, indent);
|
||||
builder.Append($" [{node.Triggers.Count}]");
|
||||
builder.AppendLine();
|
||||
foreach (var child in node.Specializations)
|
||||
{
|
||||
TreeToString(builder, child, indent + 2);
|
||||
}
|
||||
}
|
||||
|
||||
public void GenerateGraph(string outPath)
|
||||
{
|
||||
using (var output = new StreamWriter(outPath))
|
||||
{
|
||||
output.WriteLine("digraph TriggerTree {");
|
||||
GenerateGraph(output, Root, 0);
|
||||
output.WriteLine("}");
|
||||
}
|
||||
}
|
||||
|
||||
private string NameNode(Node node)
|
||||
{
|
||||
return '"' + node.ToString().Replace("\"", "\\\"") + '"';
|
||||
}
|
||||
|
||||
private void GenerateGraph(StreamWriter output, Node node, int indent)
|
||||
{
|
||||
output.Write($"{"".PadLeft(indent)}{NameNode(node)}");
|
||||
var first = true;
|
||||
foreach (var child in node.Specializations)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
output.Write(" -> {");
|
||||
first = false;
|
||||
}
|
||||
output.Write($" {NameNode(child)}");
|
||||
}
|
||||
if (!first) output.WriteLine($"{"".PadLeft(indent)}}}");
|
||||
else output.WriteLine();
|
||||
foreach (var child in node.Specializations)
|
||||
{
|
||||
GenerateGraph(output, child, indent + 2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void IWrite(StreamWriter writer, params string[] strings)
|
||||
{
|
||||
writer.Write(" ");
|
||||
foreach (var str in strings)
|
||||
{
|
||||
writer.Write(str);
|
||||
}
|
||||
writer.WriteLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the possible matches given the current frame.
|
||||
/// </summary>
|
||||
/// <param name="frame">Frame to evaluate against.</param>
|
||||
/// <returns>Enumeration of possible matches.</returns>
|
||||
/// <remarks>
|
||||
/// Most predicates work on the current frame, but predicates are free to access other frames or use other sources of knowledge.
|
||||
/// </remarks>
|
||||
public IEnumerable<Node> Matches(IDictionary<string, object> frame)
|
||||
{
|
||||
return Root.Matches(frame);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify the tree meets speicalization/generalization invariants.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Node VerifyTree()
|
||||
{
|
||||
return VerifyTree(Root, new HashSet<Node>());
|
||||
}
|
||||
|
||||
private Node VerifyTree(Node node, HashSet<Node> visited)
|
||||
{
|
||||
Node badNode = null;
|
||||
if (!visited.Contains(node))
|
||||
{
|
||||
visited.Add(node);
|
||||
for (var i = 0; badNode == null && i < node.Specializations.Count; ++i)
|
||||
{
|
||||
var first = node.Specializations[i];
|
||||
if (node.Relationship(first) != RelationshipType.Generalizes)
|
||||
{
|
||||
badNode = node;
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyTree(node.Specializations[i], visited);
|
||||
for (var j = i + 1; j < node.Specializations.Count; ++j)
|
||||
{
|
||||
var second = node.Specializations[j];
|
||||
if (first.Relationship(second) != RelationshipType.Incomparable)
|
||||
{
|
||||
badNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return badNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge together evaluator expressions.
|
||||
/// </summary>
|
||||
/// <param name="combination">How to combine expressions.</param>
|
||||
/// <param name="allExpressions">Lambda expressions to combine.</param>
|
||||
/// <returns>New evaluator lambda expression.</returns>
|
||||
static public Expression<Evaluator> MergeEvaluators(ExpressionType combination, params Expression<Evaluator>[] allExpressions)
|
||||
{
|
||||
Expression<Evaluator> result = null;
|
||||
var expressions = allExpressions.ToList();
|
||||
expressions.RemoveAll(e => e == null);
|
||||
if (expressions.Any())
|
||||
{
|
||||
if (expressions.Count == 1)
|
||||
{
|
||||
result = expressions.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
var parameter = expressions.First().Parameters.First();
|
||||
var replacer = new ReplaceParameter();
|
||||
Expression body = null;
|
||||
foreach (var expr in expressions)
|
||||
{
|
||||
var newBody = replacer.Modify(expr.Body, parameter);
|
||||
if (body == null)
|
||||
{
|
||||
body = newBody;
|
||||
}
|
||||
else
|
||||
{
|
||||
body = Expression.MakeBinary(combination, body, newBody);
|
||||
}
|
||||
}
|
||||
result = Expression.Lambda<Evaluator>(body, parameter);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ReplaceParameter : ExpressionVisitor
|
||||
{
|
||||
private ParameterExpression _parameter;
|
||||
|
||||
public Expression Modify(Expression expression, ParameterExpression parameter)
|
||||
{
|
||||
_parameter = parameter;
|
||||
return Visit(expression);
|
||||
}
|
||||
|
||||
protected override Expression VisitParameter(ParameterExpression node)
|
||||
{
|
||||
return _parameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class Binary : Expression
|
||||
{
|
||||
public Binary(string type, Expression left, Expression right)
|
||||
: base(type)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
}
|
||||
|
||||
public Expression Left { get; }
|
||||
|
||||
public Expression Right { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type}({Left}, {Right})";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class Call : NAry
|
||||
{
|
||||
public Call(string function, IEnumerable<Expression> args)
|
||||
: base(ExpressionType.Call, args)
|
||||
{
|
||||
Function = function;
|
||||
}
|
||||
|
||||
public string Function { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString(Function);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class Constant: Expression
|
||||
{
|
||||
public Constant(object value)
|
||||
: base(ExpressionType.Constant)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public object Value { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public abstract class Expression
|
||||
{
|
||||
public Expression(string type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public string Type { get; }
|
||||
|
||||
// TODO: Do I need these?
|
||||
public static Binary Add(Expression left, Expression right) => new Binary(ExpressionType.Add, left, right);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class ExpressionType
|
||||
{
|
||||
// Math
|
||||
public const string Add = "+";
|
||||
public const string Subtract = "-";
|
||||
public const string Multiply = "*";
|
||||
|
||||
// Comparisons
|
||||
public const string LessThan = "<";
|
||||
public const string LessThanOrEqual = "<=";
|
||||
public new const string Equals = "==";
|
||||
public const string NotEquals = "!=";
|
||||
public const string GreaterThan = ">";
|
||||
public const string GreaterThanOrEqual = ">=";
|
||||
|
||||
// Logic
|
||||
public const string And = "&&";
|
||||
public const string Or = "||";
|
||||
public const string Not = "!";
|
||||
|
||||
// Misc
|
||||
public const string Call = "Call";
|
||||
public const string Constant = "Constant";
|
||||
public const string Variable = "Variable";
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
|||
|
||||
Task<object> Evaluate(string expression, IDictionary<string, object> vars);
|
||||
|
||||
IParseTree Parse();
|
||||
Expression Parse();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class NAry : Expression
|
||||
{
|
||||
public NAry(string type, IEnumerable<Expression> children)
|
||||
: base(type)
|
||||
{
|
||||
Children = children.ToArray();
|
||||
}
|
||||
|
||||
public Expression[] Children { get; }
|
||||
|
||||
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)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
|
||||
builder.Append(child.ToString());
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class Unary : Expression
|
||||
{
|
||||
public Unary(string type, Expression child)
|
||||
: base(type)
|
||||
{
|
||||
Child = child;
|
||||
}
|
||||
|
||||
public Expression Child { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type}({Child})";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Bot.Builder.Dialogs.Expressions
|
||||
{
|
||||
public class Variable : Expression
|
||||
{
|
||||
public Variable(string path, string name = null)
|
||||
: base(ExpressionType.Variable)
|
||||
{
|
||||
Path = path;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name == null ? $"{{{Path}}}" : $"{Name}={{{Path}}}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.Expressions.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class ExpressionTree
|
||||
{
|
||||
[TestMethod]
|
||||
public void ExprTree()
|
||||
{
|
||||
var t1 = ExpressionEngine.Parse("{a} > 3");
|
||||
Console.WriteLine(t1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Moq" Version="4.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
<Version>15.9.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk">
|
||||
<Version>15.9.0</Version>
|
||||
<Version>15.8.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq">
|
||||
<Version>4.10.0</Version>
|
||||
|
|
Загрузка…
Ссылка в новой задаче