Add structural equality and content hashing for FlatExpression.ExprTree#550
Add structural equality and content hashing for FlatExpression.ExprTree#550Copilot wants to merge 11 commits into
Conversation
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/8bcf787b-adee-4401-8721-4587fe7cb997 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/8bcf787b-adee-4401-8721-4587fe7cb997 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
|
|
||
| /// <summary>Reconstructs the boxed constant value from the node's inline 32-bit payload.</summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static object ReadInlineConstantValue(Type type, uint data) |
There was a problem hiding this comment.
I do not like boxing here. Instead of returning object let call the GetHash inside the return the GetHash directly.
There was a problem hiding this comment.
Updated in f933b56. Inline constant hashing now goes straight through typed hash paths without reconstructing boxed values.
| return node.Obj?.GetHashCode() ?? 0; | ||
| } | ||
|
|
||
| private static object GetConstantValue(ExprTree tree, ref ExprNode node) |
There was a problem hiding this comment.
Let avoid the boxing. If we need to compare and call equals do the switch for the constant type and name method ConstantEquals instead.
There was a problem hiding this comment.
Updated in f933b56. Constant comparison now uses a dedicated typed helper (AreConstantsEqual) and avoids boxing for inline primitive constants.
| if (!EqObj(xTree, ref x, yTree, ref y)) | ||
| return false; | ||
|
|
||
| return EqChildren(xTree.GetChildren(xIdx), xTree, yTree.GetChildren(yIdx), yTree); |
There was a problem hiding this comment.
Updated in f933b56. The one-use child comparison helper is gone; the traversal is inlined into the loop-based comparer.
| private static int Combine(int h1, int h2) => | ||
| unchecked(h1 ^ (h2 + (int)0x9e3779b9 + (h1 << 6) + (h1 >> 2))); | ||
|
|
||
| private bool EqNode(ExprTree xTree, int xIdx, ExprTree yTree, int yIdx) |
There was a problem hiding this comment.
Why do we copy ExprTree structs here?
There was a problem hiding this comment.
Updated in f933b56. The comparer/hash entry points now pass ExprTree by ref, so the structural traversal no longer copies the tree structs.
| private static int Combine(int h1, int h2) => | ||
| unchecked(h1 ^ (h2 + (int)0x9e3779b9 + (h1 << 6) + (h1 >> 2))); | ||
|
|
||
| private bool EqNode(ExprTree xTree, int xIdx, ExprTree yTree, int yIdx) |
There was a problem hiding this comment.
I do not the recursive implementation. In the linked issue I have specifically said that we need to purse loop based comparison because we have natural links ChildId -> NextId (switch to child if it has one) -> back to parent. Please implement the comparison in a single method. Use helper for multiple same blocks of code only.
There was a problem hiding this comment.
Updated in f933b56. Equality now uses a single loop-based traversal over the flat links with explicit sibling tracking instead of recursive descent.
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/340a2774-3537-47bb-b6cb-038b55aec114 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/340a2774-3537-47bb-b6cb-038b55aec114 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/340a2774-3537-47bb-b6cb-038b55aec114 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
| public int RootIdx; | ||
|
|
||
| /// <summary>Gets or sets the flat node storage.</summary> | ||
| public SmallList<ExprNode, Stack16<ExprNode>, NoArrayPool<ExprNode>> Nodes; |
There was a problem hiding this comment.
This comparison it too complecated - we already have unified ExprNode representation for expression.
So instead of checking its NodeType we can mask ChildIdx and NextIdx and compare the rest bit by bit.
Please check this approach.
Then we can just go to ChildIdx or to NextIdx - that's it.
There was a problem hiding this comment.
Updated in 023bb11. The comparer now uses masked ExprNode shape checks for the common precheck path, so the loop compares unified node layout first and keeps the existing special handling only where structural semantics differ.
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/5b87e13e-991a-4ea4-aab8-266df9dda7fd Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
dadhi
left a comment
There was a problem hiding this comment.
missing public xml comments break ci
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/306086af-4c82-4f97-9286-a3fe568d3f9a Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
Fixed in 14f0c05. Added the missing XML comments for |
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static bool GetInlineOrStoredBoolean(ref ExprTree tree, ref ExprNode node) => |
There was a problem hiding this comment.
@copilot Is it even possible in our FE design that bool can be stored not inline? If not - why
do we have this unboxing code, the same for other types up to 4 bytes
There was a problem hiding this comment.
Fixed in b1874d8. bool and the other <=4-byte primitive constants are now treated as normalized inline values in the FE path, and ExprTree.ConstantInt was updated to inline as well, so those unboxing helpers were removed. The remaining non-inline fallback is only a defensive object-equality path for externally hand-crafted non-normalized nodes.
There was a problem hiding this comment.
@copilot Let just Debug.Assert that such nodes can not be constructed - or do some other outline check. We need to assume that the node is correct and the users know what they doing - and if possible make all the necessary check or assert in the constructions time
There was a problem hiding this comment.
Addressed in 005b4bf. Small primitive and small-backed enum constants now assert at construction time if they bypass inline storage, and the structural constant compare/hash path no longer keeps the defensive non-inline fallback.
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static bool GetInlineBoolean(ref ExprNode node) => node.InlineValue != 0; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static byte GetInlineByte(ref ExprNode node) => (byte)node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static sbyte GetInlineSByte(ref ExprNode node) => (sbyte)(byte)node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static char GetInlineChar(ref ExprNode node) => (char)(ushort)node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static short GetInlineInt16(ref ExprNode node) => (short)(ushort)node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static ushort GetInlineUInt16(ref ExprNode node) => (ushort)node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static int GetInlineInt32(ref ExprNode node) => (int)node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static uint GetInlineUInt32(ref ExprNode node) => node.InlineValue; | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static float GetInlineSingle(ref ExprNode node) => FloatBits.ToFloat(node.InlineValue); |
There was a problem hiding this comment.
@copilot Why do we need this methods just to compare the same x.InlineValue == y.InlineData
There was a problem hiding this comment.
Updated in 0c059a4. The inline constant comparison now uses the normalized InlineValue directly for the small primitive cases, and keeps only the float path special so structural equality still follows Single.Equals semantics.
FlatExpression.ExprTreedid not support structural equality or stable hashing, which made it fall short of Light/System expressions for content-addressed use cases. This change adds tree-level equality/hash semantics that treat equivalent flat trees as the same key even when parameter identities differ.What changed
IEquatable<ExprTree>plusEquals,GetHashCode, and==/!=for structural comparison of flat expression trees.Structural semantics
inline,closure constant, or boxed payload) for both equality and hashing.Hashing
Coverage
ExprTreeas a key