diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index ad07b3fe..35c8b643 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -61,6 +61,7 @@ public struct ExprNode private const uint MetaKeepWithoutNext = 0xFFFF0000u; // _data layout: bits [31:16]=ChildCount | [15:0]=ChildIdx (or full uint for inline constants) private const int DataCountShift = 16; + private const uint DataKeepWithoutChildIdx = 0xFFFF0000u; private const uint DataIdxMask = 0xFFFFu; private const int FlagsShift = 4; private const uint KindMask = 0x0Fu; @@ -106,6 +107,7 @@ public struct ExprNode internal ExprNode(Type type, object obj, ExpressionType nodeType, ExprNodeKind kind, byte flags = 0, int childIdx = 0, int childCount = 0, int nextIdx = 0) { + Debug.Assert(!RequiresInlineConstantStorage(type, obj, nodeType)); Type = type; Obj = obj; var tag = (byte)((flags << FlagsShift) | (byte)kind); @@ -136,9 +138,39 @@ internal void SetChildInfo(int childIdx, int childCount) => [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool IsExpression() => Kind == ExprNodeKind.Expression; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool RequiresInlineConstantStorage(Type type, object obj, ExpressionType nodeType) + { + if (nodeType != ExpressionType.Constant || obj == null || ReferenceEquals(obj, InlineValueMarker)) + return false; + + return type.IsEnum + ? IsSmallPrimitive(Type.GetTypeCode(Enum.GetUnderlyingType(type))) + : type.IsPrimitive && IsSmallPrimitive(Type.GetTypeCode(type)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSmallPrimitive(TypeCode tc) => + tc == TypeCode.Boolean || tc == TypeCode.Byte || tc == TypeCode.SByte || + tc == TypeCode.Char || tc == TypeCode.Int16 || tc == TypeCode.UInt16 || + tc == TypeCode.Int32 || tc == TypeCode.UInt32 || tc == TypeCode.Single; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool HasFlag(byte flag) => (Flags & flag) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool HasSameHeaderExceptNext(ref ExprNode other) => + Type == other.Type && (_meta & MetaKeepWithoutNext) == (other._meta & MetaKeepWithoutNext); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool HasSameShapeExceptLinks(ref ExprNode other) => + HasSameHeaderExceptNext(ref other) && + (_data & DataKeepWithoutChildIdx) == (other._data & DataKeepWithoutChildIdx); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool HasSameShapeExceptNext(ref ExprNode other) => + HasSameHeaderExceptNext(ref other) && _data == other._data; + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool ShouldCloneWhenLinked() => ReferenceEquals(Obj, InlineValueMarker) || @@ -173,7 +205,7 @@ public LambdaClosureParameterUsage(ushort lambdaIdx, ushort parameterIdx, ushort } /// Stores an expression tree as flat nodes plus separate closure constants. -public struct ExprTree +public struct ExprTree : IEquatable { private static readonly object ClosureConstantMarker = new(); private const byte ParameterByRefFlag = 1; @@ -274,7 +306,7 @@ public int Constant(object value, Type type) /// Adds an constant node. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int ConstantInt(int value) => AddRawExpressionNode(typeof(int), value, ExpressionType.Constant); + public int ConstantInt(int value) => AddInlineConstantNode(typeof(int), unchecked((uint)value)); /// Adds a typed constant node. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -709,6 +741,29 @@ public SysExpr ToExpression() => [RequiresUnreferencedCode(FastExpressionCompiler.LightExpression.Trimming.Message)] public LightExpression ToLightExpression() => FastExpressionCompiler.LightExpression.FromSysExpressionConverter.ToLightExpression(ToExpression()); + /// Structurally compares two flat expression trees. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(ExprTree other) => + new StructuralComparer().Eq(ref this, ref other); + + /// Structurally compares this tree with another object. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object obj) => + obj is ExprTree other && Equals(other); + + /// Computes a content-addressable hash for the flat expression tree. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() => + new StructuralComparer().Hash(ref this); + + /// Determines whether two flat expression trees are structurally equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ExprTree left, ExprTree right) => left.Equals(right); + + /// Determines whether two flat expression trees are not structurally equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(ExprTree left, ExprTree right) => !left.Equals(right); + [MethodImpl(MethodImplOptions.AggressiveInlining)] private int AddFactoryExpressionNode(Type type, object obj, ExpressionType nodeType, int child) => AddNode(type, obj, nodeType, ExprNodeKind.Expression, 0, CloneChild(child)); @@ -1606,6 +1661,427 @@ private static bool Contains(ref SmallList [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ushort ToStoredUShortIdx(int idx) => checked((ushort)idx); + private struct StructuralComparer + { + private SmallList, NoArrayPool> _xParameterIds, _yParameterIds; + private SmallList, NoArrayPool> _xLabelIds, _yLabelIds; + private SmallList, NoArrayPool> _eqFrames; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Eq(ref ExprTree xTree, ref ExprTree yTree) + { + if (xTree.Nodes.Count == 0 || yTree.Nodes.Count == 0) + return xTree.Nodes.Count == yTree.Nodes.Count; + + var xIdx = xTree.RootIdx; + var yIdx = yTree.RootIdx; + var remainingSiblings = 0; + while (true) + { + ref var x = ref xTree.Nodes.GetSurePresentRef(xIdx); + ref var y = ref yTree.Nodes.GetSurePresentRef(yIdx); + if (x.Kind == ExprNodeKind.UInt16Pair) + { + if (!x.HasSameShapeExceptNext(ref y)) + return false; + } + else if (x.NodeType == ExpressionType.Constant) + { + if (!x.HasSameHeaderExceptNext(ref y)) + return false; + } + else if (!x.HasSameShapeExceptLinks(ref y)) + return false; + + var descendX = 0; + var descendY = 0; + var descendChildCount = 0; + var restoreXParameterCount = -1; + var restoreYParameterCount = -1; + + if (x.Kind != ExprNodeKind.UInt16Pair) + { + if (x.Kind == ExprNodeKind.LabelTarget) + { + if (!EqLabelTarget(ref x, ref y)) + return false; + } + else if (x.Kind == ExprNodeKind.CatchBlock) + { + restoreXParameterCount = _xParameterIds.Count; + restoreYParameterCount = _yParameterIds.Count; + descendX = x.ChildIdx; + descendY = y.ChildIdx; + descendChildCount = x.ChildCount - (x.HasFlag(CatchHasVariableFlag) ? 1 : 0); + if (x.HasFlag(CatchHasVariableFlag)) + { + ref var xv = ref xTree.Nodes.GetSurePresentRef(descendX); + ref var yv = ref yTree.Nodes.GetSurePresentRef(descendY); + if (!AreEquivalentParameterDeclarations(ref xv, ref yv)) + return false; + _xParameterIds.Add(ToStoredUShortIdx(xv.ChildIdx)); + _yParameterIds.Add(ToStoredUShortIdx(yv.ChildIdx)); + descendX = xv.NextIdx; + descendY = yv.NextIdx; + } + } + else + { + switch (x.NodeType) + { + case ExpressionType.Parameter: + if (!EqParameter(ref x, ref y)) + return false; + break; + + case ExpressionType.Constant: + if (!AreConstantsEqual(ref xTree, ref x, ref yTree, ref y)) + return false; + break; + + case ExpressionType.Lambda: + if (x.ChildCount == 0) + return false; + + restoreXParameterCount = _xParameterIds.Count; + restoreYParameterCount = _yParameterIds.Count; + descendX = x.ChildIdx; + descendY = y.ChildIdx; + descendChildCount = 1; + var xParameterIdx = xTree.Nodes.GetSurePresentRef(descendX).NextIdx; + var yParameterIdx = yTree.Nodes.GetSurePresentRef(descendY).NextIdx; + for (var i = 1; i < x.ChildCount; ++i) + { + ref var xp = ref xTree.Nodes.GetSurePresentRef(xParameterIdx); + ref var yp = ref yTree.Nodes.GetSurePresentRef(yParameterIdx); + if (!AreEquivalentParameterDeclarations(ref xp, ref yp)) + return false; + _xParameterIds.Add(ToStoredUShortIdx(xp.ChildIdx)); + _yParameterIds.Add(ToStoredUShortIdx(yp.ChildIdx)); + xParameterIdx = xp.NextIdx; + yParameterIdx = yp.NextIdx; + } + break; + + case ExpressionType.Block: + if (x.ChildCount == 0) + return false; + + restoreXParameterCount = _xParameterIds.Count; + restoreYParameterCount = _yParameterIds.Count; + descendX = x.ChildIdx; + descendY = y.ChildIdx; + descendChildCount = 1; + if (x.ChildCount == 2) + { + ref var xVariables = ref xTree.Nodes.GetSurePresentRef(descendX); + ref var yVariables = ref yTree.Nodes.GetSurePresentRef(descendY); + if (xVariables.Kind != ExprNodeKind.ChildList || yVariables.Kind != ExprNodeKind.ChildList || + xVariables.ChildCount != yVariables.ChildCount) + return false; + + var xVariableIdx = xVariables.ChildIdx; + var yVariableIdx = yVariables.ChildIdx; + for (var i = 0; i < xVariables.ChildCount; ++i) + { + ref var xv = ref xTree.Nodes.GetSurePresentRef(xVariableIdx); + ref var yv = ref yTree.Nodes.GetSurePresentRef(yVariableIdx); + if (!AreEquivalentParameterDeclarations(ref xv, ref yv)) + return false; + _xParameterIds.Add(ToStoredUShortIdx(xv.ChildIdx)); + _yParameterIds.Add(ToStoredUShortIdx(yv.ChildIdx)); + xVariableIdx = xv.NextIdx; + yVariableIdx = yv.NextIdx; + } + + descendX = xVariables.NextIdx; + descendY = yVariables.NextIdx; + } + break; + + default: + if (!EqObj(ref x, ref y)) + return false; + if (x.ChildCount != 0) + { + descendX = x.ChildIdx; + descendY = y.ChildIdx; + descendChildCount = x.ChildCount; + } + break; + } + } + } + + if (descendChildCount != 0) + { + _eqFrames.Add(new TraversalFrame(x.NextIdx, y.NextIdx, remainingSiblings, restoreXParameterCount, restoreYParameterCount)); + xIdx = descendX; + yIdx = descendY; + remainingSiblings = descendChildCount - 1; + continue; + } + + var advanced = false; + while (true) + { + if (remainingSiblings != 0) + { + xIdx = x.NextIdx; + yIdx = y.NextIdx; + remainingSiblings--; + advanced = true; + break; + } + + if (_eqFrames.Count == 0) + return true; + + var frame = _eqFrames[_eqFrames.Count - 1]; + _eqFrames.Count--; + if (frame.XParameterCount >= 0) + _xParameterIds.Count = frame.XParameterCount; + if (frame.YParameterCount >= 0) + _yParameterIds.Count = frame.YParameterCount; + if (frame.RemainingSiblingsAfterNode != 0) + { + xIdx = frame.XNextIdx; + yIdx = frame.YNextIdx; + remainingSiblings = frame.RemainingSiblingsAfterNode - 1; + advanced = true; + break; + } + } + if (advanced) + continue; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Hash(ref ExprTree tree) => + tree.Nodes.Count == 0 ? 0 : HashNode(ref tree, tree.RootIdx); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Combine(int h1, int h2) => + unchecked(h1 ^ (h2 + (int)0x9e3779b9 + (h1 << 6) + (h1 >> 2))); + + private bool EqParameter(ref ExprNode x, ref ExprNode y) + { + var xId = ToStoredUShortIdx(x.ChildIdx); + for (var i = 0; i < _xParameterIds.Count; ++i) + if (_xParameterIds[i] == xId) + return _yParameterIds[i] == ToStoredUShortIdx(y.ChildIdx); + + return x.HasFlag(ParameterByRefFlag) == y.HasFlag(ParameterByRefFlag) && + Equals(x.Obj, y.Obj); + } + + private bool EqLabelTarget(ref ExprNode x, ref ExprNode y) + { + var xId = ToStoredUShortIdx(x.ChildIdx); + for (var i = 0; i < _xLabelIds.Count; ++i) + if (_xLabelIds[i] == xId) + return _yLabelIds[i] == ToStoredUShortIdx(y.ChildIdx); + + _xLabelIds.Add(xId); + _yLabelIds.Add(ToStoredUShortIdx(y.ChildIdx)); + return Equals(x.Obj, y.Obj); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool AreEquivalentParameterDeclarations(ref ExprNode x, ref ExprNode y) => + x.NodeType == ExpressionType.Parameter && + y.NodeType == ExpressionType.Parameter && + x.HasSameShapeExceptLinks(ref y); + + private static bool EqObj(ref ExprNode x, ref ExprNode y) => + ReferenceEquals(x.Obj, y.Obj) || Equals(x.Obj, y.Obj); + + private int HashNode(ref ExprTree tree, int idx) + { + ref var node = ref tree.Nodes.GetSurePresentRef(idx); + if (node.Kind == ExprNodeKind.LabelTarget) + return Combine(Combine((int)node.Kind, node.Type?.GetHashCode() ?? 0), node.Obj?.GetHashCode() ?? 0); + + if (node.Kind == ExprNodeKind.CatchBlock) + return HashCatchBlock(ref tree, idx, ref node); + + if (node.Kind == ExprNodeKind.UInt16Pair) + return Combine(Combine((int)node.Kind, node.ChildIdx), node.ChildCount); + + var h = Combine(Combine((int)node.Kind, (int)node.NodeType), node.Type?.GetHashCode() ?? 0); + h = Combine(h, node.Flags); + + switch (node.NodeType) + { + case ExpressionType.Parameter: + { + var id = ToStoredUShortIdx(node.ChildIdx); + for (var i = 0; i < _xParameterIds.Count; ++i) + if (_xParameterIds[i] == id) + return Combine(h, i); + return Combine(h, node.Obj?.GetHashCode() ?? 0); + } + + case ExpressionType.Constant: + return Combine(h, GetConstantHashCode(ref tree, ref node)); + + case ExpressionType.Lambda: + return HashLambda(ref tree, idx, h); + + case ExpressionType.Block: + return HashBlock(ref tree, idx, h); + } + + h = Combine(h, node.Obj?.GetHashCode() ?? 0); + var childIdx = node.ChildIdx; + for (var i = 0; i < node.ChildCount; ++i) + { + h = Combine(h, HashNode(ref tree, childIdx)); + childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; + } + return h; + } + + private int HashLambda(ref ExprTree tree, int idx, int h) + { + var scopeCount = _xParameterIds.Count; + ref var node = ref tree.Nodes.GetSurePresentRef(idx); + var bodyIdx = node.ChildIdx; + var parameterIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + for (var i = 1; i < node.ChildCount; ++i) + { + ref var parameter = ref tree.Nodes.GetSurePresentRef(parameterIdx); + _xParameterIds.Add(ToStoredUShortIdx(parameter.ChildIdx)); + h = Combine(h, Combine(parameter.Type?.GetHashCode() ?? 0, parameter.HasFlag(ParameterByRefFlag) ? 1 : 0)); + parameterIdx = parameter.NextIdx; + } + + h = Combine(h, HashNode(ref tree, bodyIdx)); + _xParameterIds.Count = scopeCount; + return h; + } + + private int HashBlock(ref ExprTree tree, int idx, int h) + { + var scopeCount = _xParameterIds.Count; + ref var node = ref tree.Nodes.GetSurePresentRef(idx); + var bodyListIdx = node.ChildIdx; + if (node.ChildCount == 2) + { + ref var variables = ref tree.Nodes.GetSurePresentRef(bodyListIdx); + var variableIdx = variables.ChildIdx; + for (var i = 0; i < variables.ChildCount; ++i) + { + ref var variable = ref tree.Nodes.GetSurePresentRef(variableIdx); + _xParameterIds.Add(ToStoredUShortIdx(variable.ChildIdx)); + h = Combine(h, Combine(variable.Type?.GetHashCode() ?? 0, variable.HasFlag(ParameterByRefFlag) ? 1 : 0)); + variableIdx = variable.NextIdx; + } + bodyListIdx = variables.NextIdx; + } + + h = Combine(h, HashNode(ref tree, bodyListIdx)); + _xParameterIds.Count = scopeCount; + return h; + } + + private int HashCatchBlock(ref ExprTree tree, int idx, ref ExprNode node) + { + var h = Combine(Combine((int)node.Kind, node.Type?.GetHashCode() ?? 0), node.Flags); + var scopeCount = _xParameterIds.Count; + var childIdx = 0; + var catchChildIdx = node.ChildIdx; + if (node.HasFlag(CatchHasVariableFlag)) + { + ref var variable = ref tree.Nodes.GetSurePresentRef(catchChildIdx); + _xParameterIds.Add(ToStoredUShortIdx(variable.ChildIdx)); + h = Combine(h, Combine(variable.Type?.GetHashCode() ?? 0, variable.HasFlag(ParameterByRefFlag) ? 1 : 0)); + catchChildIdx = variable.NextIdx; + childIdx++; + } + + h = Combine(h, HashNode(ref tree, catchChildIdx)); + catchChildIdx = tree.Nodes.GetSurePresentRef(catchChildIdx).NextIdx; + childIdx++; + if (node.HasFlag(CatchHasFilterFlag)) + h = Combine(h, HashNode(ref tree, catchChildIdx)); + + _xParameterIds.Count = scopeCount; + return h; + } + + private static int GetConstantHashCode(ref ExprTree tree, ref ExprNode node) + { + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker)) + return GetInlineConstantHashCode(node.Type, node.InlineValue); + + Debug.Assert(!ExprNode.RequiresInlineConstantStorage(node.Type, node.Obj, node.NodeType)); + return GetStoredConstantValue(ref tree, ref node)?.GetHashCode() ?? 0; + } + + private static bool AreConstantsEqual(ref ExprTree xTree, ref ExprNode x, ref ExprTree yTree, ref ExprNode y) + { + var xInline = ReferenceEquals(x.Obj, ExprNode.InlineValueMarker); + var yInline = ReferenceEquals(y.Obj, ExprNode.InlineValueMarker); + Debug.Assert(xInline == yInline); + if (xInline != yInline) + return false; + + if (!xInline) + { + Debug.Assert(!ExprNode.RequiresInlineConstantStorage(x.Type, x.Obj, x.NodeType)); + var xObj = GetStoredConstantValue(ref xTree, ref x); + var yObj = GetStoredConstantValue(ref yTree, ref y); + return xObj?.Equals(yObj) ?? yObj == null; + } + + if (x.Type.IsEnum) + return x.InlineValue == y.InlineValue; + + var typeCode = Type.GetTypeCode(x.Type); + Debug.Assert(IsSmallPrimitive(typeCode)); + return typeCode != TypeCode.Single + ? x.InlineValue == y.InlineValue + : FloatBits.ToFloat(x.InlineValue).Equals(FloatBits.ToFloat(y.InlineValue)); + } + + private static object GetStoredConstantValue(ref ExprTree tree, ref ExprNode node) => + ReferenceEquals(node.Obj, ClosureConstantMarker) ? tree.ClosureConstants[node.ChildIdx] : node.Obj; + + private static int GetInlineConstantHashCode(Type type, uint data) + { + if (!type.IsEnum) + { + var typeCode = Type.GetTypeCode(type); + Debug.Assert(IsSmallPrimitive(typeCode)); + if (typeCode == TypeCode.Single) + return FloatBits.ToFloat(data).GetHashCode(); + } + + return data.GetHashCode(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct TraversalFrame + { + public readonly int RemainingSiblingsAfterNode; + public readonly int XParameterCount; + public readonly int YParameterCount; + public readonly ushort XNextIdx; + public readonly ushort YNextIdx; + + public TraversalFrame(int xNextIdx, int yNextIdx, int remainingSiblingsAfterNode, int xParameterCount, int yParameterCount) + { + RemainingSiblingsAfterNode = remainingSiblingsAfterNode; + XParameterCount = xParameterCount; + YParameterCount = yParameterCount; + XNextIdx = checked((ushort)xNextIdx); + YNextIdx = checked((ushort)yNextIdx); + } + } + } + /// Reconstructs System.Linq nodes from the flat representation while reusing parameter and label identities. private struct Reader { diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 3e3ff694..98084bd8 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -55,7 +55,11 @@ public int Run() Flat_blocks_with_variables_tracked_from_expression_conversion(); Flat_goto_and_label_nodes_tracked_from_expression_conversion(); Flat_try_catch_nodes_tracked_from_expression_conversion(); - return 38; + Flat_equal_lambdas_with_different_parameter_names_are_structurally_equal_and_hash_equal(); + Flat_equal_nested_lambdas_with_captures_are_structurally_equal_and_hash_equal(); + Flat_standalone_parameters_use_name_in_structural_equality(); + Flat_structural_hash_supports_dictionary_lookup(); + return 42; } @@ -1023,5 +1027,56 @@ public void Flat_try_catch_nodes_tracked_from_expression_conversion() Asserts.AreEqual(1, fe.TryCatchNodes.Count); } + + public void Flat_equal_lambdas_with_different_parameter_names_are_structurally_equal_and_hash_equal() + { + var x = Parameter(typeof(int), "x"); + var left = Lambda>(Add(x, Constant(1)), x).ToFlatExpression(); + + var y = Parameter(typeof(int), "y"); + var right = Lambda>(Add(y, Constant(1)), y).ToFlatExpression(); + + Asserts.IsTrue(left.Equals(right)); + Asserts.IsTrue(left == right); + Asserts.AreEqual(left.GetHashCode(), right.GetHashCode()); + } + + public void Flat_equal_nested_lambdas_with_captures_are_structurally_equal_and_hash_equal() + { + var x = Parameter(typeof(int), "x"); + var left = Lambda>>( + Lambda>(Add(x, Constant(1))), + x).ToFlatExpression(); + + var y = Parameter(typeof(int), "value"); + var right = Lambda>>( + Lambda>(Add(y, Constant(1))), + y).ToFlatExpression(); + + Asserts.IsTrue(left.Equals(right)); + Asserts.AreEqual(left.GetHashCode(), right.GetHashCode()); + } + + public void Flat_standalone_parameters_use_name_in_structural_equality() + { + var left = Parameter(typeof(int), "x").ToFlatExpression(); + var right = Parameter(typeof(int), "y").ToFlatExpression(); + + Asserts.IsFalse(left.Equals(right)); + } + + public void Flat_structural_hash_supports_dictionary_lookup() + { + var x = Parameter(typeof(int), "x"); + var key = Lambda>(Add(x, Constant(1)), x).ToFlatExpression(); + var dict = new Dictionary { [key] = "found" }; + + var lookup = default(ExprTree); + var y = lookup.ParameterOf("arg"); + lookup.RootIdx = lookup.Lambda>(lookup.Add(y, lookup.ConstantInt(1)), y); + + Asserts.IsTrue(dict.TryGetValue(lookup, out var value)); + Asserts.AreEqual("found", value); + } } }