From caec149950722f9190787bfa910eb50b4be035c8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 May 2026 15:24:26 +0000
Subject: [PATCH 1/4] Initial plan
From 1f600150f7d928c8b3affd4443a81895f36a884e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 5 May 2026 15:36:12 +0000
Subject: [PATCH 2/4] Add flat expression order normalization
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/bc8ce881-a5e5-4b7b-ad10-dac6ff73fa64
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 196 ++++++++++++++++++
.../LightExpressionTests.cs | 49 ++++-
2 files changed, 244 insertions(+), 1 deletion(-)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index ad07b3fe..6d8f8d13 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -709,6 +709,96 @@ public SysExpr ToExpression() =>
[RequiresUnreferencedCode(FastExpressionCompiler.LightExpression.Trimming.Message)]
public LightExpression ToLightExpression() => FastExpressionCompiler.LightExpression.FromSysExpressionConverter.ToLightExpression(ToExpression());
+ /// Returns true when all reachable nodes are compacted into a canonical post-order layout.
+ ///
+ /// Canonical post-order means that each child is placed before its parent, the root is the last node,
+ /// and there are no unreachable nodes left in .
+ ///
+ public bool IsInOrder()
+ {
+ if (Nodes.Count == 0)
+ return true;
+ if ((uint)RootIdx >= (uint)Nodes.Count)
+ return false;
+
+ SmallList, NoArrayPool> ordered = default;
+ var visitStates = new byte[Nodes.Count];
+ if (!TryCollectReachablePostOrder(RootIdx, visitStates, ref ordered) || ordered.Count != Nodes.Count)
+ return false;
+
+ for (var i = 0; i < ordered.Count; ++i)
+ if (ordered[i] != i)
+ return false;
+
+ return true;
+ }
+
+ /// Compacts the current tree into the canonical post-order layout and drops unreachable nodes.
+ /// The number of removed unreachable nodes.
+ public int PutInOrder()
+ {
+ if (Nodes.Count == 0)
+ return 0;
+ if ((uint)RootIdx >= (uint)Nodes.Count)
+ throw new InvalidOperationException($"Root idx {RootIdx} is outside of the node range.");
+
+ SmallList, NoArrayPool> ordered = default;
+ var visitStates = new byte[Nodes.Count];
+ if (!TryCollectReachablePostOrder(RootIdx, visitStates, ref ordered))
+ throw new InvalidOperationException("The flat expression contains an invalid or cyclic child link.");
+
+ var removedCount = Nodes.Count - ordered.Count;
+ var alreadyInOrder = removedCount == 0;
+ if (alreadyInOrder)
+ for (var i = 0; i < ordered.Count; ++i)
+ if (ordered[i] != i)
+ {
+ alreadyInOrder = false;
+ break;
+ }
+
+ if (alreadyInOrder)
+ return 0;
+
+ var remap = new int[Nodes.Count];
+ Array.Fill(remap, -1);
+ for (var i = 0; i < ordered.Count; ++i)
+ remap[ordered[i]] = i;
+
+ SmallList, NoArrayPool> reorderedNodes = default;
+ for (var i = 0; i < ordered.Count; ++i)
+ {
+ ref var oldNode = ref Nodes.GetSurePresentRef(ordered[i]);
+ ref var newNode = ref reorderedNodes.AddDefaultAndGetRef();
+ newNode = oldNode;
+ newNode.SetNextIdx(0);
+ }
+
+ for (var i = 0; i < ordered.Count; ++i)
+ {
+ ref var oldNode = ref Nodes.GetSurePresentRef(ordered[i]);
+ if (!HasStructuralChildren(in oldNode))
+ continue;
+
+ var oldChildIdx = oldNode.ChildIdx;
+ ref var newNode = ref reorderedNodes.GetSurePresentRef(i);
+ newNode.SetChildInfo(remap[oldChildIdx], oldNode.ChildCount);
+
+ for (var childNumber = 1; childNumber < oldNode.ChildCount; ++childNumber)
+ {
+ var nextOldChildIdx = Nodes.GetSurePresentRef(oldChildIdx).NextIdx;
+ reorderedNodes.GetSurePresentRef(remap[oldChildIdx]).SetNextIdx(remap[nextOldChildIdx]);
+ oldChildIdx = nextOldChildIdx;
+ }
+ }
+
+ Nodes = reorderedNodes;
+ RootIdx = remap[RootIdx];
+ ClosureConstants = CompactClosureConstants();
+ RebuildMetadata();
+ return removedCount;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int AddFactoryExpressionNode(Type type, object obj, ExpressionType nodeType, int child) =>
AddNode(type, obj, nodeType, ExprNodeKind.Expression, 0, CloneChild(child));
@@ -1408,6 +1498,12 @@ private static Type GetArrayElementType(Type arrayType, int depth)
return elementType ?? typeof(object);
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool HasStructuralChildren(in ExprNode node) =>
+ node.ChildCount != 0 &&
+ !ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) &&
+ node.Kind != ExprNodeKind.UInt16Pair;
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int CloneChild(int idx)
{
@@ -1606,6 +1702,106 @@ private static bool Contains(ref SmallList
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ushort ToStoredUShortIdx(int idx) => checked((ushort)idx);
+ private bool TryCollectReachablePostOrder(
+ int idx,
+ byte[] visitStates,
+ ref SmallList, NoArrayPool> ordered)
+ {
+ if ((uint)idx >= (uint)Nodes.Count)
+ return false;
+
+ var visitState = visitStates[idx];
+ if (visitState == 2)
+ return true;
+ if (visitState == 1)
+ return false;
+
+ visitStates[idx] = 1;
+
+ ref var node = ref Nodes.GetSurePresentRef(idx);
+ if (HasStructuralChildren(in node))
+ {
+ var childIdx = node.ChildIdx;
+ for (var i = 0; i < node.ChildCount; ++i)
+ {
+ var currentChildIdx = childIdx;
+ if (!TryCollectReachablePostOrder(currentChildIdx, visitStates, ref ordered))
+ return false;
+ childIdx = Nodes.GetSurePresentRef(currentChildIdx).NextIdx;
+ }
+ }
+
+ visitStates[idx] = 2;
+ ordered.Add(idx);
+ return true;
+ }
+
+ private SmallList