From d9c85d982a6d141d63e02f39ed1ee5542d09183e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:01:02 +0000 Subject: [PATCH 1/6] Initial plan From 826f86bc8c29c90a6a8aaba9832207d3baee7ac0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:16:38 +0000 Subject: [PATCH 2/6] Add FlatExpression POC: data-oriented flat expression tree with tests Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/ac75b2c5-61a5-4e7b-b468-5bfff97971ac Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler/FlatExpression.cs | 738 ++++++++++++++++++ .../Program.cs | 1 + .../FlatExpressionTests.cs | 391 ++++++++++ 3 files changed, 1130 insertions(+) create mode 100644 src/FastExpressionCompiler/FlatExpression.cs create mode 100644 test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs new file mode 100644 index 00000000..258c41bf --- /dev/null +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -0,0 +1,738 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016-2026 Maksim Volkau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// FlatExpression.cs — POC for a data-oriented, flat (SOA-flavoured) expression tree. +// +// KEY IDEAS (from issue #512 / comments): +// • Intrusive linked-list tree: every node has ChildIdx (first child) + NextIdx (next sibling) +// encoded as 1-based indices into a single flat Nodes array. +// • 0 (default) == nil, so an uninitialised Idx.It means "absent". +// • ExpressionNode is a "fat" struct: NodeType, Type, Info, plus two child index slots. +// • ExpressionTree keeps all nodes + closure constants in SmallList<> wrappers. +// The SmallList, NoArrayPool> variant keeps the first 16 nodes +// directly inside the struct (on the stack when the tree is a local variable). +// • Factory methods mutate `this` and return the 1-based Idx of the new node. +// +// WINS: +// ✓ Small expressions fully on stack — zero heap allocation for ≤16 nodes. +// ✓ Trivially serializable: arrays of plain structs with integer references. +// ✓ O(1) node access by Idx — no pointer chasing. +// ✓ Structural equality via a single pass over the two arrays. +// ✓ Closure constants collected automatically during construction. +// ✓ Dead-code / liveness bits can be packed into the Idx.It upper bits later. +// +// GAPS / CONS / OBSTACLES: +// ✗ Not API-compatible with System.Linq.Expressions — requires a conversion adapter. +// ✗ Mutable struct semantics: accidental copy of ExpressionTree silently forks state. +// ✗ A node can only belong to one tree (tree, not DAG); re-use across trees requires +// cloning (but that is a minor cost given how rarely it occurs). +// ✗ ExpressionNode fat-struct (≈ 40 bytes) × 16 on-stack ≈ 640 bytes per tree on +// the call-stack — suitable for leaf methods, not for deeply recursive builders. +// ✗ Parameter identity across nested lambdas must be tracked by the caller through the +// returned Idx (same index = same parameter). +// ✗ Info field boxes MethodBase / string — one allocation per Call/New/Parameter node. +// A future optimisation could use a dedicated MethodBase[] side array. + +#nullable disable + +namespace FastExpressionCompiler.FlatExpression; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using FastExpressionCompiler.ImTools; + +using SysExpr = System.Linq.Expressions.Expression; +using SysParam = System.Linq.Expressions.ParameterExpression; + +/// +/// 1-based index into . +/// default / It == 0 is the nil sentinel. +/// +[StructLayout(LayoutKind.Sequential)] +public struct Idx : IEquatable +{ + /// 1-based position in the Nodes array. 0 = nil. + public int It; + + /// True when this index represents "no node". + public bool IsNil => It == 0; + + /// The nil sentinel. + public static Idx Nil => default; + + /// Creates an Idx from a 1-based position. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Idx Of(int oneBasedIndex) => new Idx { It = oneBasedIndex }; + + /// + public bool Equals(Idx other) => It == other.It; + + /// + public override bool Equals(object obj) => obj is Idx other && Equals(other); + + /// + public override int GetHashCode() => It; + + /// + public override string ToString() => IsNil ? "nil" : It.ToString(); +} + +/// +/// A fat node inside . Uses an intrusive linked-list to +/// represent trees without any nested allocation. +/// +/// Layout conventions by : +/// +/// ConstantType, Info = boxed value (or null when ConstantIndex ≥ 0) +/// Parameter/VariableType, Info = name (string or null) +/// DefaultType, ChildIdx/ExtraIdx = Nil +/// UnaryNodeType, Type, Info = MethodInfo (nullable), ChildIdx = operand +/// BinaryNodeType, Type, Info = MethodInfo (nullable), ChildIdx = left, ExtraIdx = right +/// NewType, Info = ConstructorInfo, ChildIdx = first arg (args chained via NextIdx) +/// CallType, Info = MethodInfo, ChildIdx = instance (or first arg for static), ExtraIdx = first arg for instance calls +/// LambdaType = delegate type, Info = Idx[] of parameter indices, ChildIdx = body, ExtraIdx = Nil +/// BlockType, ChildIdx = first expr (chained via NextIdx), ExtraIdx = first variable (chained via NextIdx) +/// ConditionalType, ChildIdx = test, ExtraIdx = ifTrue; ifFalse is chained via ExtraIdx.NextIdx +/// +/// +[StructLayout(LayoutKind.Sequential)] +public struct ExpressionNode +{ + /// The kind of this expression node (mirrors ). + public ExpressionType NodeType; + + /// The CLR type this expression evaluates to. + public Type Type; + + /// + /// Node-kind-specific metadata: + /// + /// Constant → the boxed value (null when using ConstantIndex). + /// Parameter → the parameter name (string, may be null). + /// Call / Invoke → the . + /// New → the . + /// Unary / Binary with custom method → the . + /// Lambda → [] of parameter node indices. + /// + /// Design note: Lambda does NOT chain params via NextIdx because the parameter nodes + /// may already have their NextIdx used as argument chains in New/Call. + /// Storing params as an Idx[] in Info avoids that conflict at the cost of one small + /// heap allocation per lambda node. A future optimisation could pack them into a + /// dedicated ParamsIdx array side-table with a (start, count) slice reference. + /// + /// + /// + /// + public object Info; + + /// + /// For nodes: 0-based index into + /// if ≥ 0, otherwise the value lives in + /// directly and is treated as a compile-time literal (no closure slot). + /// + public int ConstantIndex; + + /// Next sibling in a linked list (next argument, parameter, or statement). + public Idx NextIdx; + + /// First child node (first arg, operand, body, first statement…). + public Idx ChildIdx; + + /// + /// Second child slot: + /// + /// Binary → right operand. + /// Lambda → Nil (parameters are stored as Idx[] in Info instead of NextIdx chain). + /// Call (instance) → first argument (ChildIdx is the target). + /// Block → first variable declaration. + /// Conditional → ifTrue branch (ifFalse is ifTrue.NextIdx). + /// + /// + public Idx ExtraIdx; +} + +/// +/// Flat expression tree. All nodes live in (a ) +/// and closure constants in . +/// +/// Nodes are 1-indexed: Idx.It == 1 corresponds to Nodes.Items[0]. +/// Idx.It == 0 () means "absent". +/// +/// Factory methods mutate this struct and return the of the new node. +/// Because this is a mutable struct you should hold it as a local variable (or on the heap via +/// a wrapper) and not pass it by value to helpers — use ref parameters instead. +/// +public struct ExpressionTree +{ + // ------------------------------------------------------------------------- + // Storage + // ------------------------------------------------------------------------- + + /// + /// All expression nodes. First 16 slots are held inside the struct itself via + /// ; overflow spills to the heap array. + /// + public SmallList, NoArrayPool> Nodes; + + /// + /// Closure constants collected during tree construction. + /// Reference-type values and structs larger than a pointer go here; primitives may + /// stay in directly (ConstantIndex == -1). + /// First 4 slots are held inside the struct itself. + /// + public SmallList, NoArrayPool> ClosureConstants; + + /// The root node (usually the outermost Lambda). Set by . + public Idx RootIdx; + + // ------------------------------------------------------------------------- + // Primitive helpers + // ------------------------------------------------------------------------- + + /// Gets a reference to the node at (1-based, not nil). + [UnscopedRef] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref ExpressionNode NodeAt(Idx idx) + { + Debug.Assert(!idx.IsNil, "Cannot dereference a nil Idx"); + return ref Nodes.GetSurePresentRef(idx.It - 1); + } + + /// Total number of nodes added so far. + public int NodeCount => Nodes.Count; + + // ------------------------------------------------------------------------- + // Internal: add a node and return its 1-based Idx + // ------------------------------------------------------------------------- + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Idx AddNode( + ExpressionType nodeType, + Type type, + object info = null, + int constantIndex = -1, + Idx childIdx = default, + Idx extraIdx = default) + { + ref var n = ref Nodes.AddDefaultAndGetRef(); + n.NodeType = nodeType; + n.Type = type; + n.Info = info; + n.ConstantIndex = constantIndex; + n.ChildIdx = childIdx; + n.ExtraIdx = extraIdx; + n.NextIdx = Idx.Nil; + return Idx.Of(Nodes.Count); // Count is already incremented by AddDefaultAndGetRef + } + + // ------------------------------------------------------------------------- + // Factory — Constant + // ------------------------------------------------------------------------- + + /// + /// Adds a constant node. Reference types and large structs are added to the closure + /// constants array so they can be mutated after compilation; plain primitives (int, bool, + /// string) are stored inline in . + /// + public Idx Constant(object value, bool putIntoClosure = false) + { + if (value == null) + return AddNode(ExpressionType.Constant, typeof(object), null); + + var type = value.GetType(); + + if (!putIntoClosure && IsInlineable(type)) + return AddNode(ExpressionType.Constant, type, value, constantIndex: -1); + + // Add to closure constants + var ci = ClosureConstants.Count; + ClosureConstants.Add(value); + return AddNode(ExpressionType.Constant, type, null, constantIndex: ci); + } + + /// Typed helper — avoids boxing for common value types. + public Idx Constant(T value, bool putIntoClosure = false) => + Constant((object)value, putIntoClosure); + + // Primitive types that are cheap to keep inline (no closure slot needed by default). + private static bool IsInlineable(Type t) => + t == typeof(int) || t == typeof(long) || t == typeof(double) || t == typeof(float) || + t == typeof(bool) || t == typeof(string) || t == typeof(char) || + t == typeof(byte) || t == typeof(short) || t == typeof(decimal) || + t == typeof(DateTime) || t == typeof(Guid); + + // ------------------------------------------------------------------------- + // Factory — Parameter / Variable + // ------------------------------------------------------------------------- + + /// Adds a parameter node. Use the returned to refer to the same parameter. + public Idx Parameter(Type type, string name = null) => + AddNode(ExpressionType.Parameter, type, info: name); + + /// Alias for (variables are parameters in lambda body). + public Idx Variable(Type type, string name = null) => + AddNode(ExpressionType.Parameter, type, info: name); + + // ------------------------------------------------------------------------- + // Factory — Default + // ------------------------------------------------------------------------- + + /// Adds a default(T) node. + public Idx Default(Type type) => + AddNode(ExpressionType.Default, type); + + // ------------------------------------------------------------------------- + // Factory — Unary + // ------------------------------------------------------------------------- + + /// Adds a unary expression node. + public Idx Unary(ExpressionType nodeType, Idx operand, Type type, MethodInfo method = null) => + AddNode(nodeType, type, info: method, childIdx: operand); + + /// Typed convert. + public Idx Convert(Idx operand, Type toType) => + Unary(ExpressionType.Convert, operand, toType); + + /// Logical not. + public Idx Not(Idx operand) => + Unary(ExpressionType.Not, operand, typeof(bool)); + + /// Negate. + public Idx Negate(Idx operand, Type type) => + Unary(ExpressionType.Negate, operand, type); + + // ------------------------------------------------------------------------- + // Factory — Binary + // ------------------------------------------------------------------------- + + /// Adds a binary expression node. + public Idx Binary(ExpressionType nodeType, Idx left, Idx right, Type type, MethodInfo method = null) => + AddNode(nodeType, type, info: method, childIdx: left, extraIdx: right); + + /// Addition. + public Idx Add(Idx left, Idx right, Type type) => + Binary(ExpressionType.Add, left, right, type); + + /// Subtraction. + public Idx Subtract(Idx left, Idx right, Type type) => + Binary(ExpressionType.Subtract, left, right, type); + + /// Multiply. + public Idx Multiply(Idx left, Idx right, Type type) => + Binary(ExpressionType.Multiply, left, right, type); + + /// Equal. + public Idx Equal(Idx left, Idx right) => + Binary(ExpressionType.Equal, left, right, typeof(bool)); + + /// Assign. + public Idx Assign(Idx target, Idx value, Type type) => + Binary(ExpressionType.Assign, target, value, type); + + // ------------------------------------------------------------------------- + // Factory — New + // ------------------------------------------------------------------------- + + /// + /// Adds a new T(args…) node. + /// Arguments are linked via in the order supplied. + /// + public Idx New(ConstructorInfo ctor, params Idx[] args) + { + var firstArgIdx = LinkList(args); + return AddNode(ExpressionType.New, ctor.DeclaringType, info: ctor, childIdx: firstArgIdx); + } + + // ------------------------------------------------------------------------- + // Factory — Call + // ------------------------------------------------------------------------- + + /// Adds a static or instance method call node. + public Idx Call(MethodInfo method, Idx instance, params Idx[] args) + { + var returnType = method.ReturnType == typeof(void) ? typeof(void) : method.ReturnType; + if (instance.IsNil) + { + // Static call: all args hang from ChildIdx + var firstArgIdx = LinkList(args); + return AddNode(ExpressionType.Call, returnType, info: method, childIdx: firstArgIdx); + } + else + { + // Instance call: target → ChildIdx, args → ExtraIdx + var firstArgIdx = LinkList(args); + return AddNode(ExpressionType.Call, returnType, info: method, + childIdx: instance, extraIdx: firstArgIdx); + } + } + + // ------------------------------------------------------------------------- + // Factory — Lambda + // ------------------------------------------------------------------------- + + /// + /// Adds a lambda node. is the body expression; + /// are the parameter nodes (must already exist in + /// — reuse the values returned by + /// ). + /// + /// Parameters are stored in as an array + /// rather than linked via , because a parameter node's + /// NextIdx may already be in use as part of an argument chain (e.g. when the same parameter + /// is passed to a New or Call node). This is a key design trade-off: + /// one small allocation per lambda vs. avoiding silent list corruption. + /// + /// Sets when is true (default). + /// + public Idx Lambda(Type delegateType, Idx body, Idx[] parameters = null, bool isRoot = true) + { + // Store params as Idx[] in Info — do NOT call LinkList to avoid corrupting NextIdx + // chains that the same param nodes may already participate in (e.g. as New/Call args). + var lambdaIdx = AddNode(ExpressionType.Lambda, delegateType, + info: parameters, // Idx[] or null + childIdx: body); // ExtraIdx left as Nil (unused for Lambda) + if (isRoot) + RootIdx = lambdaIdx; + return lambdaIdx; + } + + // ------------------------------------------------------------------------- + // Factory — Conditional + // ------------------------------------------------------------------------- + + /// Adds a conditional (ternary) expression. + public Idx Conditional(Idx test, Idx ifTrue, Idx ifFalse, Type type) + { + // ifTrue and ifFalse are siblings: link ifFalse as ifTrue.NextIdx + ref var ifTrueNode = ref NodeAt(ifTrue); + ifTrueNode.NextIdx = ifFalse; + return AddNode(ExpressionType.Conditional, type, childIdx: test, extraIdx: ifTrue); + } + + // ------------------------------------------------------------------------- + // Factory — Block + // ------------------------------------------------------------------------- + + /// Adds a block expression. + public Idx Block(Type type, Idx[] exprs, Idx[] variables = null) + { + var firstExprIdx = LinkList(exprs); + var firstVarIdx = variables == null || variables.Length == 0 + ? Idx.Nil + : LinkList(variables); + return AddNode(ExpressionType.Block, type, childIdx: firstExprIdx, extraIdx: firstVarIdx); + } + + // ------------------------------------------------------------------------- + // Intrusive-list helpers + // ------------------------------------------------------------------------- + + /// + /// Chains the nodes at the given indices into a singly-linked list via + /// and returns the head. + /// The last node's NextIdx is left as . + /// + public Idx LinkList(Idx[] indices) + { + if (indices == null || indices.Length == 0) + return Idx.Nil; + + for (var i = 0; i < indices.Length - 1; i++) + NodeAt(indices[i]).NextIdx = indices[i + 1]; + + // Ensure last node's NextIdx is nil (in case it was previously linked elsewhere) + NodeAt(indices[indices.Length - 1]).NextIdx = Idx.Nil; + + return indices[0]; + } + + /// Iterates the sibling chain starting at . + public IEnumerable Siblings(Idx head) + { + var cur = head; + while (!cur.IsNil) + { + yield return cur; + cur = NodeAt(cur).NextIdx; + } + } + + // ------------------------------------------------------------------------- + // Conversion to System.Linq.Expressions + // ------------------------------------------------------------------------- + + /// + /// Converts the flat tree back into a hierarchy so it can be + /// compiled with the standard compiler or FEC. + /// + /// Gaps / obstacles visible here: + /// • We need to materialise a for each Parameter node and + /// cache it by Idx so that shared parameters in nested lambdas resolve correctly. + /// • Closure constants are wrapped in a ConstantExpression; a real FEC + /// integration would inject them into an ArrayClosure instead. + /// + public SysExpr ToSystemExpression() => ToSystemExpression(RootIdx, new Dictionary()); + + private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramMap) + { + if (nodeIdx.IsNil) + throw new InvalidOperationException("Cannot convert nil Idx to System.Linq.Expressions"); + + ref var node = ref NodeAt(nodeIdx); + + switch (node.NodeType) + { + case ExpressionType.Constant: + { + var value = node.ConstantIndex >= 0 + ? ClosureConstants.GetSurePresentRef(node.ConstantIndex) + : node.Info; + return SysExpr.Constant(value, node.Type); + } + + case ExpressionType.Parameter: + { + if (!paramMap.TryGetValue(nodeIdx.It, out var p)) + { + p = SysExpr.Parameter(node.Type, node.Info as string); + paramMap[nodeIdx.It] = p; + } + return p; + } + + case ExpressionType.Default: + return SysExpr.Default(node.Type); + + case ExpressionType.Lambda: + { + // params are stored as Idx[] in Info (not linked via NextIdx — see Lambda factory) + var paramIdxs = node.Info as Idx[]; + var paramExprs = new List(); + if (paramIdxs != null) + foreach (var pIdx in paramIdxs) + { + // ToSystemExpression for parameters populates paramMap + var p = (SysParam)ToSystemExpression(pIdx, paramMap); + paramExprs.Add(p); + } + // Build body AFTER registering parameters so they are found in paramMap + var body = ToSystemExpression(node.ChildIdx, paramMap); + return SysExpr.Lambda(node.Type, body, paramExprs); + } + + case ExpressionType.New: + { + var ctor = (ConstructorInfo)node.Info; + var args = SiblingList(node.ChildIdx, paramMap); + return SysExpr.New(ctor, args); + } + + case ExpressionType.Call: + { + var method = (MethodInfo)node.Info; + if (method.IsStatic) + { + var args = SiblingList(node.ChildIdx, paramMap); + return SysExpr.Call(method, args); + } + else + { + var target = ToSystemExpression(node.ChildIdx, paramMap); + var args = SiblingList(node.ExtraIdx, paramMap); + return SysExpr.Call(target, method, args); + } + } + + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: + case ExpressionType.Modulo: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.ExclusiveOr: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Assign: + case ExpressionType.LeftShift: + case ExpressionType.RightShift: + case ExpressionType.Power: + case ExpressionType.Coalesce: + { + var left = ToSystemExpression(node.ChildIdx, paramMap); + var right = ToSystemExpression(node.ExtraIdx, paramMap); + return SysExpr.MakeBinary(node.NodeType, left, right, false, node.Info as MethodInfo); + } + + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.ArrayLength: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + case ExpressionType.Throw: + case ExpressionType.Unbox: + case ExpressionType.Increment: + case ExpressionType.Decrement: + case ExpressionType.PreIncrementAssign: + case ExpressionType.PostIncrementAssign: + case ExpressionType.PreDecrementAssign: + case ExpressionType.PostDecrementAssign: + { + var operand = ToSystemExpression(node.ChildIdx, paramMap); + return SysExpr.MakeUnary(node.NodeType, operand, node.Type, node.Info as MethodInfo); + } + + case ExpressionType.Conditional: + { + var test = ToSystemExpression(node.ChildIdx, paramMap); + var ifTrue = ToSystemExpression(node.ExtraIdx, paramMap); + var ifFalse = ToSystemExpression(NodeAt(node.ExtraIdx).NextIdx, paramMap); + return SysExpr.Condition(test, ifTrue, ifFalse, node.Type); + } + + case ExpressionType.Block: + { + var exprs = SiblingList(node.ChildIdx, paramMap); + if (node.ExtraIdx.IsNil) + return SysExpr.Block(node.Type, exprs); + + var vars = new List(); + foreach (var vIdx in Siblings(node.ExtraIdx)) + vars.Add((SysParam)ToSystemExpression(vIdx, paramMap)); + return SysExpr.Block(node.Type, vars, exprs); + } + + default: + throw new NotSupportedException( + $"FlatExpression → System.Linq.Expressions: NodeType {node.NodeType} is not yet mapped."); + } + } + + private List SiblingList(Idx head, Dictionary paramMap) + { + var list = new List(); + foreach (var idx in Siblings(head)) + list.Add(ToSystemExpression(idx, paramMap)); + return list; + } + + // ------------------------------------------------------------------------- + // Structural equality + // ------------------------------------------------------------------------- + + /// + /// Checks structural equality of two trees in O(n) time by comparing + /// every node field and every closure constant. + /// Win: no tree traversal required — a single loop over the flat arrays suffices. + /// + public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b) + { + if (a.NodeCount != b.NodeCount) return false; + if (a.ClosureConstants.Count != b.ClosureConstants.Count) return false; + + for (var i = 0; i < a.NodeCount; i++) + { + ref var na = ref a.Nodes.GetSurePresentRef(i); + ref var nb = ref b.Nodes.GetSurePresentRef(i); + if (na.NodeType != nb.NodeType) return false; + if (na.Type != nb.Type) return false; + if (!InfoEqual(na.Info, nb.Info)) return false; + if (na.ConstantIndex != nb.ConstantIndex) return false; + if (na.NextIdx.It != nb.NextIdx.It) return false; + if (na.ChildIdx.It != nb.ChildIdx.It) return false; + if (na.ExtraIdx.It != nb.ExtraIdx.It) return false; + } + + for (var i = 0; i < a.ClosureConstants.Count; i++) + if (!Equals(a.ClosureConstants.GetSurePresentRef(i), + b.ClosureConstants.GetSurePresentRef(i))) + return false; + + return true; + } + + private static bool InfoEqual(object infoA, object infoB) + { + if (infoA is Idx[] ia && infoB is Idx[] ib) + { + if (ia.Length != ib.Length) return false; + for (var k = 0; k < ia.Length; k++) + if (ia[k].It != ib[k].It) return false; + return true; + } + return Equals(infoA, infoB); + } + + // ------------------------------------------------------------------------- + // Debug / diagnostic + // ------------------------------------------------------------------------- + + /// Returns a human-readable dump of all nodes (useful for diagnostics). + public string Dump() + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine($"ExpressionTree NodeCount={NodeCount} ClosureConstants={ClosureConstants.Count} RootIdx={RootIdx}"); + for (var i = 0; i < NodeCount; i++) + { + ref var n = ref Nodes.GetSurePresentRef(i); + sb.AppendLine( + $" [{i + 1}] {n.NodeType,-22} type={n.Type?.Name,-14} " + + $"info={InfoStr(n.Info),-30} " + + $"ci={n.ConstantIndex,2} " + + $"child={n.ChildIdx} extra={n.ExtraIdx} next={n.NextIdx}"); + } + if (ClosureConstants.Count > 0) + { + sb.AppendLine(" Closure constants:"); + for (var i = 0; i < ClosureConstants.Count; i++) + sb.AppendLine($" [{i}] = {ClosureConstants.GetSurePresentRef(i)}"); + } + return sb.ToString(); + } + + private static string InfoStr(object info) => + info == null ? "—" : + info is MethodBase mb ? mb.Name : + info is Idx[] idxArr ? $"params[{string.Join(",", System.Linq.Enumerable.Select(idxArr, x => x.It))}]" : + info.ToString(); +} diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index b2b11cfa..504a3202 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -164,6 +164,7 @@ void Run(Func run, string name = null) Run(new LightExpression.UnitTests.LightExpressionTests().Run); Run(new ToCSharpStringTests().Run); Run(new LightExpression.UnitTests.ToCSharpStringTests().Run); + Run(new FlatExpressionTests().Run); Console.WriteLine($"{Environment.NewLine}UnitTests are passing in {sw.ElapsedMilliseconds} ms."); diff --git a/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs new file mode 100644 index 00000000..77b0c295 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs @@ -0,0 +1,391 @@ +// FlatExpression is only in the FastExpressionCompiler assembly (not the LightExpression variant), +// so these tests are excluded from the LIGHT_EXPRESSION build. +#if !LIGHT_EXPRESSION +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +// FlatExpression lives in a separate namespace — no conditional #if needed here. +using FastExpressionCompiler.FlatExpression; + +namespace FastExpressionCompiler.UnitTests; + +/// +/// Demonstration / exploration tests for the POC. +/// +/// These tests double as living documentation of the design space — wins, gaps, and +/// open questions are highlighted inline. +/// +public class FlatExpressionTests : ITest +{ + public int Run() + { + Idx_default_is_nil(); + Idx_of_is_one_based(); + + Build_constant_node_inline(); + Build_constant_node_in_closure(); + Build_parameter_node(); + + Build_add_two_constants(); + Build_lambda_int_identity(); + Build_lambda_add_two_params(); + Build_new_expression(); + Build_call_static_method(); + Build_conditional(); + Build_block_with_variable(); + + Structural_equality_same_trees(); + Structural_equality_different_trees(); + + Convert_to_system_expression_constant_lambda(); + Convert_to_system_expression_add_lambda(); + Convert_to_system_expression_new_lambda(); + + Dump_does_not_throw(); + + // Round-trip: build flat → System.Linq.Expressions → compile → invoke + Roundtrip_lambda_identity_compile_and_invoke(); + Roundtrip_lambda_add_compile_and_invoke(); + + // Gap showcase: mutating a closure constant after building the flat tree. + Closure_constant_is_mutable_after_build(); + + return 22; + } + + // ------------------------------------------------------------------------- + // Idx basics + // ------------------------------------------------------------------------- + + public void Idx_default_is_nil() + { + var idx = default(Idx); + Asserts.IsTrue(idx.IsNil); + Asserts.AreEqual(0, idx.It); + Asserts.AreEqual(Idx.Nil, idx); + } + + public void Idx_of_is_one_based() + { + var idx = Idx.Of(3); + Asserts.IsFalse(idx.IsNil); + Asserts.AreEqual(3, idx.It); + } + + // ------------------------------------------------------------------------- + // Node construction + // ------------------------------------------------------------------------- + + public void Build_constant_node_inline() + { + var tree = default(ExpressionTree); + var ci = tree.Constant(42); + + Asserts.AreEqual(1, tree.NodeCount); + Asserts.IsFalse(ci.IsNil); + + ref var node = ref tree.NodeAt(ci); + Asserts.AreEqual(ExpressionType.Constant, node.NodeType); + Asserts.AreEqual(typeof(int), node.Type); + Asserts.AreEqual(42, (int)node.Info); + Asserts.AreEqual(-1, node.ConstantIndex); // inline, not in closure + } + + public void Build_constant_node_in_closure() + { + var tree = default(ExpressionTree); + var ci = tree.Constant("hello", putIntoClosure: true); + + ref var node = ref tree.NodeAt(ci); + Asserts.AreEqual(0, node.ConstantIndex); // first closure slot + Asserts.AreEqual(1, tree.ClosureConstants.Count); + Asserts.AreEqual("hello", (string)tree.ClosureConstants.GetSurePresentRef(0)); + } + + public void Build_parameter_node() + { + var tree = default(ExpressionTree); + var pi = tree.Parameter(typeof(int), "x"); + + ref var node = ref tree.NodeAt(pi); + Asserts.AreEqual(ExpressionType.Parameter, node.NodeType); + Asserts.AreEqual(typeof(int), node.Type); + Asserts.AreEqual("x", (string)node.Info); + } + + public void Build_add_two_constants() + { + var tree = default(ExpressionTree); + var a = tree.Constant(10); + var b = tree.Constant(20); + var add = tree.Add(a, b, typeof(int)); + + Asserts.AreEqual(3, tree.NodeCount); + ref var node = ref tree.NodeAt(add); + Asserts.AreEqual(ExpressionType.Add, node.NodeType); + Asserts.AreEqual(a, node.ChildIdx); + Asserts.AreEqual(b, node.ExtraIdx); + } + + public void Build_lambda_int_identity() + { + // (x) => x (identity lambda: Func) + var tree = default(ExpressionTree); + var p = tree.Parameter(typeof(int), "x"); + var lambdaIdx = tree.Lambda(typeof(Func), body: p, parameters: [p]); + + Asserts.AreEqual(2, tree.NodeCount); // param + lambda + Asserts.AreEqual(lambdaIdx, tree.RootIdx); + + ref var lambda = ref tree.NodeAt(lambdaIdx); + Asserts.AreEqual(ExpressionType.Lambda, lambda.NodeType); + Asserts.AreEqual(p, lambda.ChildIdx); // body + + // params are stored as Idx[] in Info (not via NextIdx/ExtraIdx) + var parms = (Idx[])lambda.Info; + Asserts.AreEqual(1, parms.Length); + Asserts.AreEqual(p, parms[0]); + } + + public void Build_lambda_add_two_params() + { + // (x, y) => x + y + var tree = default(ExpressionTree); + var px = tree.Parameter(typeof(int), "x"); + var py = tree.Parameter(typeof(int), "y"); + var add = tree.Add(px, py, typeof(int)); + var lambda = tree.Lambda(typeof(Func), body: add, parameters: [px, py]); + + Asserts.AreEqual(4, tree.NodeCount); // px, py, add, lambda + + ref var lambdaNode = ref tree.NodeAt(lambda); + Asserts.AreEqual(add, lambdaNode.ChildIdx); // body + + // params stored as Idx[] in Info, NextIdx is NOT modified + var parms = (Idx[])lambdaNode.Info; + Asserts.AreEqual(2, parms.Length); + Asserts.AreEqual(px, parms[0]); + Asserts.AreEqual(py, parms[1]); + + // px and py are NOT linked via NextIdx (design decision: avoids list conflicts) + ref var pxNode = ref tree.NodeAt(px); + Asserts.IsTrue(pxNode.NextIdx.IsNil); + } + + public void Build_new_expression() + { + var ctor = typeof(Tuple).GetConstructor([typeof(int), typeof(string)]); + var tree = default(ExpressionTree); + var arg1 = tree.Constant(1); + var arg2 = tree.Constant("hi"); + var newIdx = tree.New(ctor, arg1, arg2); + + ref var newNode = ref tree.NodeAt(newIdx); + Asserts.AreEqual(ExpressionType.New, newNode.NodeType); + Asserts.AreEqual(typeof(Tuple), newNode.Type); + Asserts.AreEqual(ctor, (ConstructorInfo)newNode.Info); + + // args are chained + var siblings = tree.Siblings(newNode.ChildIdx).ToArray(); + Asserts.AreEqual(2, siblings.Length); + Asserts.AreEqual(arg1, siblings[0]); + Asserts.AreEqual(arg2, siblings[1]); + } + + public void Build_call_static_method() + { + var method = typeof(Math).GetMethod(nameof(Math.Abs), [typeof(int)]); + var tree = default(ExpressionTree); + var arg = tree.Parameter(typeof(int), "n"); + var callIdx = tree.Call(method, Idx.Nil, arg); + + ref var callNode = ref tree.NodeAt(callIdx); + Asserts.AreEqual(ExpressionType.Call, callNode.NodeType); + Asserts.AreEqual(method, (MethodInfo)callNode.Info); + } + + public void Build_conditional() + { + // x > 0 ? x : -x + var tree = default(ExpressionTree); + var x = tree.Parameter(typeof(int), "x"); + var zero = tree.Constant(0); + var test = tree.Binary(ExpressionType.GreaterThan, x, zero, typeof(bool)); + var neg = tree.Negate(x, typeof(int)); + // Note: Conditional modifies ifTrue.NextIdx — so ifTrue should be a fresh node. + // Here we re-use x as ifTrue which is fine since NextIdx on parameter node + // was not set yet (it was linked only in ExtraIdx of lambda in other tests). + // For clarity build a fresh constant: + var xCopy = tree.Parameter(typeof(int), "x_copy"); + var cond = tree.Conditional(test, xCopy, neg, typeof(int)); + + ref var condNode = ref tree.NodeAt(cond); + Asserts.AreEqual(ExpressionType.Conditional, condNode.NodeType); + Asserts.AreEqual(test, condNode.ChildIdx); + Asserts.AreEqual(xCopy, condNode.ExtraIdx); // ifTrue + // ifFalse is linked as ifTrue.NextIdx + ref var ifTrueNode = ref tree.NodeAt(xCopy); + Asserts.AreEqual(neg, ifTrueNode.NextIdx); + } + + public void Build_block_with_variable() + { + // { int v = 0; return v; } + var tree = default(ExpressionTree); + var v = tree.Variable(typeof(int), "v"); + var zero = tree.Constant(0); + var assign = tree.Assign(v, zero, typeof(int)); + var blockIdx = tree.Block(typeof(int), exprs: [assign, v], variables: [v]); + + ref var blockNode = ref tree.NodeAt(blockIdx); + Asserts.AreEqual(ExpressionType.Block, blockNode.NodeType); + Asserts.IsFalse(blockNode.ChildIdx.IsNil); // exprs + Asserts.IsFalse(blockNode.ExtraIdx.IsNil); // vars + } + + // ------------------------------------------------------------------------- + // Structural equality + // ------------------------------------------------------------------------- + + public void Structural_equality_same_trees() + { + var t1 = BuildAddTree(); + var t2 = BuildAddTree(); + Asserts.IsTrue(ExpressionTree.StructurallyEqual(ref t1, ref t2)); + } + + public void Structural_equality_different_trees() + { + var t1 = BuildAddTree(); + + var t2 = default(ExpressionTree); + var a = t2.Constant(10); + var b = t2.Constant(99); // different constant value: StructurallyEqual compares Info via Equals + t2.Add(a, b, typeof(int)); + + Asserts.IsFalse(ExpressionTree.StructurallyEqual(ref t1, ref t2)); + } + + // ------------------------------------------------------------------------- + // ToSystemExpression + // ------------------------------------------------------------------------- + + public void Convert_to_system_expression_constant_lambda() + { + // () => 42 + var tree = default(ExpressionTree); + var c = tree.Constant(42); + tree.Lambda(typeof(Func), body: c); + + var sysExpr = tree.ToSystemExpression(); + Asserts.IsNotNull(sysExpr); + Asserts.AreEqual(ExpressionType.Lambda, sysExpr.NodeType); + } + + public void Convert_to_system_expression_add_lambda() + { + // (x, y) => x + y + var tree = default(ExpressionTree); + var px = tree.Parameter(typeof(int), "x"); + var py = tree.Parameter(typeof(int), "y"); + var add = tree.Add(px, py, typeof(int)); + tree.Lambda(typeof(Func), body: add, parameters: [px, py]); + + var sysExpr = (LambdaExpression)tree.ToSystemExpression(); + Asserts.AreEqual(2, sysExpr.Parameters.Count); + Asserts.AreEqual(ExpressionType.Add, sysExpr.Body.NodeType); + } + + public void Convert_to_system_expression_new_lambda() + { + // (n) => new Tuple(n, "x") + var ctor = typeof(Tuple).GetConstructor([typeof(int), typeof(string)]); + var tree = default(ExpressionTree); + var n = tree.Parameter(typeof(int), "n"); + var s = tree.Constant("x"); + var newIdx = tree.New(ctor, n, s); + tree.Lambda(typeof(Func>), body: newIdx, parameters: [n]); + + var sysExpr = (LambdaExpression)tree.ToSystemExpression(); + Asserts.AreEqual(ExpressionType.New, sysExpr.Body.NodeType); + } + + // ------------------------------------------------------------------------- + // Round-trip: flat → System.Linq.Expressions → compile → invoke + // ------------------------------------------------------------------------- + + public void Roundtrip_lambda_identity_compile_and_invoke() + { + // (x) => x + var tree = default(ExpressionTree); + var p = tree.Parameter(typeof(int), "x"); + tree.Lambda(typeof(Func), body: p, parameters: [p]); + + var sysExpr = (Expression>)tree.ToSystemExpression(); + var fn = sysExpr.Compile(); + Asserts.AreEqual(7, fn(7)); + } + + public void Roundtrip_lambda_add_compile_and_invoke() + { + // (x, y) => x + y + var tree = default(ExpressionTree); + var px = tree.Parameter(typeof(int), "x"); + var py = tree.Parameter(typeof(int), "y"); + var add = tree.Add(px, py, typeof(int)); + tree.Lambda(typeof(Func), body: add, parameters: [px, py]); + + var sysExpr = (Expression>)tree.ToSystemExpression(); + var fn = sysExpr.Compile(); + Asserts.AreEqual(11, fn(4, 7)); + } + + // ------------------------------------------------------------------------- + // Gap showcase: mutable closure constant + // ------------------------------------------------------------------------- + + public void Closure_constant_is_mutable_after_build() + { + // Build a lambda that captures a constant via the closure array. + // After building we can swap the constant value without rebuilding the tree + // (same Idx still points to the same closure slot). + var tree = default(ExpressionTree); + var c = tree.Constant("initial", putIntoClosure: true); + tree.Lambda(typeof(Func), body: c); + + // Change the constant at slot 0 + tree.ClosureConstants.GetSurePresentRef(0) = "updated"; + + var sysExpr = (Expression>)tree.ToSystemExpression(); + var fn = sysExpr.Compile(); + Asserts.AreEqual("updated", fn()); + } + + // ------------------------------------------------------------------------- + // Diagnostic + // ------------------------------------------------------------------------- + + public void Dump_does_not_throw() + { + var tree = BuildAddTree(); + var dump = tree.Dump(); + Asserts.IsNotNull(dump); + Asserts.IsTrue(dump.Contains("ExpressionTree")); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static ExpressionTree BuildAddTree() + { + var tree = default(ExpressionTree); + var a = tree.Constant(10); + var b = tree.Constant(20); + tree.Add(a, b, typeof(int)); + return tree; + } +} +#endif From d565578dd0ea665d2c5cc78836ca49bce1cc964c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:45:32 +0000 Subject: [PATCH 3/6] Trim trivial comments; keep only why/how notes Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/cb3f142a-4345-45e1-af61-2f4161da4ed9 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler/FlatExpression.cs | 399 +++--------------- .../FlatExpressionTests.cs | 100 +---- 2 files changed, 77 insertions(+), 422 deletions(-) diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs index 258c41bf..ae37db94 100644 --- a/src/FastExpressionCompiler/FlatExpression.cs +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -22,37 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -// FlatExpression.cs — POC for a data-oriented, flat (SOA-flavoured) expression tree. -// -// KEY IDEAS (from issue #512 / comments): -// • Intrusive linked-list tree: every node has ChildIdx (first child) + NextIdx (next sibling) -// encoded as 1-based indices into a single flat Nodes array. -// • 0 (default) == nil, so an uninitialised Idx.It means "absent". -// • ExpressionNode is a "fat" struct: NodeType, Type, Info, plus two child index slots. -// • ExpressionTree keeps all nodes + closure constants in SmallList<> wrappers. -// The SmallList, NoArrayPool> variant keeps the first 16 nodes -// directly inside the struct (on the stack when the tree is a local variable). -// • Factory methods mutate `this` and return the 1-based Idx of the new node. -// -// WINS: -// ✓ Small expressions fully on stack — zero heap allocation for ≤16 nodes. -// ✓ Trivially serializable: arrays of plain structs with integer references. -// ✓ O(1) node access by Idx — no pointer chasing. -// ✓ Structural equality via a single pass over the two arrays. -// ✓ Closure constants collected automatically during construction. -// ✓ Dead-code / liveness bits can be packed into the Idx.It upper bits later. -// -// GAPS / CONS / OBSTACLES: -// ✗ Not API-compatible with System.Linq.Expressions — requires a conversion adapter. -// ✗ Mutable struct semantics: accidental copy of ExpressionTree silently forks state. -// ✗ A node can only belong to one tree (tree, not DAG); re-use across trees requires -// cloning (but that is a minor cost given how rarely it occurs). -// ✗ ExpressionNode fat-struct (≈ 40 bytes) × 16 on-stack ≈ 640 bytes per tree on -// the call-stack — suitable for leaf methods, not for deeply recursive builders. -// ✗ Parameter identity across nested lambdas must be tracked by the caller through the -// returned Idx (same index = same parameter). -// ✗ Info field boxes MethodBase / string — one allocation per Call/New/Parameter node. -// A future optimisation could use a dedicated MethodBase[] side array. +// POC for issue #512: data-oriented flat expression tree. +// Intrusive linked-list tree: ChildIdx (first child) + NextIdx (next sibling), 1-based into Nodes. +// 0/default == nil. ExpressionTree keeps ≤16 nodes on the stack via Stack16. #nullable disable @@ -72,152 +44,65 @@ namespace FastExpressionCompiler.FlatExpression; using SysExpr = System.Linq.Expressions.Expression; using SysParam = System.Linq.Expressions.ParameterExpression; -/// -/// 1-based index into . -/// default / It == 0 is the nil sentinel. -/// +/// 1-based index into . default == nil. [StructLayout(LayoutKind.Sequential)] public struct Idx : IEquatable { - /// 1-based position in the Nodes array. 0 = nil. public int It; - /// True when this index represents "no node". public bool IsNil => It == 0; - - /// The nil sentinel. public static Idx Nil => default; - /// Creates an Idx from a 1-based position. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Idx Of(int oneBasedIndex) => new Idx { It = oneBasedIndex }; - /// public bool Equals(Idx other) => It == other.It; - - /// public override bool Equals(object obj) => obj is Idx other && Equals(other); - - /// public override int GetHashCode() => It; - - /// public override string ToString() => IsNil ? "nil" : It.ToString(); } /// -/// A fat node inside . Uses an intrusive linked-list to -/// represent trees without any nested allocation. -/// -/// Layout conventions by : +/// Fat node in . Intrusive linked-list tree encoding: /// -/// ConstantType, Info = boxed value (or null when ConstantIndex ≥ 0) -/// Parameter/VariableType, Info = name (string or null) -/// DefaultType, ChildIdx/ExtraIdx = Nil -/// UnaryNodeType, Type, Info = MethodInfo (nullable), ChildIdx = operand -/// BinaryNodeType, Type, Info = MethodInfo (nullable), ChildIdx = left, ExtraIdx = right -/// NewType, Info = ConstructorInfo, ChildIdx = first arg (args chained via NextIdx) -/// CallType, Info = MethodInfo, ChildIdx = instance (or first arg for static), ExtraIdx = first arg for instance calls -/// LambdaType = delegate type, Info = Idx[] of parameter indices, ChildIdx = body, ExtraIdx = Nil -/// BlockType, ChildIdx = first expr (chained via NextIdx), ExtraIdx = first variable (chained via NextIdx) -/// ConditionalType, ChildIdx = test, ExtraIdx = ifTrue; ifFalse is chained via ExtraIdx.NextIdx +/// Constant Info = boxed value; ConstantIndex ≥ 0 → value lives in ClosureConstants instead. +/// Parameter Info = name (string or null). +/// Unary Info = MethodInfo (nullable), ChildIdx = operand. +/// Binary Info = MethodInfo (nullable), ChildIdx = left, ExtraIdx = right. +/// New Info = ConstructorInfo, ChildIdx = first arg (chained via NextIdx). +/// Call Info = MethodInfo, ChildIdx = instance-or-first-static-arg, ExtraIdx = first arg for instance calls. +/// Lambda Info = Idx[] of params, ChildIdx = body. Params stored in Info rather than NextIdx chain because the same parameter node may already participate as a New/Call argument. +/// Block ChildIdx = first expr, ExtraIdx = first variable (both chained via NextIdx). +/// ConditionalChildIdx = test, ExtraIdx = ifTrue; ifFalse = ifTrue.NextIdx. /// /// [StructLayout(LayoutKind.Sequential)] public struct ExpressionNode { - /// The kind of this expression node (mirrors ). public ExpressionType NodeType; - - /// The CLR type this expression evaluates to. public Type Type; - - /// - /// Node-kind-specific metadata: - /// - /// Constant → the boxed value (null when using ConstantIndex). - /// Parameter → the parameter name (string, may be null). - /// Call / Invoke → the . - /// New → the . - /// Unary / Binary with custom method → the . - /// Lambda → [] of parameter node indices. - /// - /// Design note: Lambda does NOT chain params via NextIdx because the parameter nodes - /// may already have their NextIdx used as argument chains in New/Call. - /// Storing params as an Idx[] in Info avoids that conflict at the cost of one small - /// heap allocation per lambda node. A future optimisation could pack them into a - /// dedicated ParamsIdx array side-table with a (start, count) slice reference. - /// - /// - /// - /// public object Info; - - /// - /// For nodes: 0-based index into - /// if ≥ 0, otherwise the value lives in - /// directly and is treated as a compile-time literal (no closure slot). - /// + /// ≥ 0: index into . -1: value is inline in Info. public int ConstantIndex; - - /// Next sibling in a linked list (next argument, parameter, or statement). public Idx NextIdx; - - /// First child node (first arg, operand, body, first statement…). public Idx ChildIdx; - - /// - /// Second child slot: - /// - /// Binary → right operand. - /// Lambda → Nil (parameters are stored as Idx[] in Info instead of NextIdx chain). - /// Call (instance) → first argument (ChildIdx is the target). - /// Block → first variable declaration. - /// Conditional → ifTrue branch (ifFalse is ifTrue.NextIdx). - /// - /// public Idx ExtraIdx; } /// -/// Flat expression tree. All nodes live in (a ) -/// and closure constants in . -/// -/// Nodes are 1-indexed: Idx.It == 1 corresponds to Nodes.Items[0]. -/// Idx.It == 0 () means "absent". -/// -/// Factory methods mutate this struct and return the of the new node. -/// Because this is a mutable struct you should hold it as a local variable (or on the heap via -/// a wrapper) and not pass it by value to helpers — use ref parameters instead. +/// Flat expression tree backed by a single flat Nodes array. Hold as a local or heap field — +/// do not pass by value (mutable struct; copy silently forks state). /// public struct ExpressionTree { - // ------------------------------------------------------------------------- - // Storage - // ------------------------------------------------------------------------- - - /// - /// All expression nodes. First 16 slots are held inside the struct itself via - /// ; overflow spills to the heap array. - /// + // First 16 nodes are on the stack; further nodes spill to a heap array. public SmallList, NoArrayPool> Nodes; - - /// - /// Closure constants collected during tree construction. - /// Reference-type values and structs larger than a pointer go here; primitives may - /// stay in directly (ConstantIndex == -1). - /// First 4 slots are held inside the struct itself. - /// + // First 4 closure constants on stack. public SmallList, NoArrayPool> ClosureConstants; - - /// The root node (usually the outermost Lambda). Set by . public Idx RootIdx; - // ------------------------------------------------------------------------- - // Primitive helpers - // ------------------------------------------------------------------------- + public int NodeCount => Nodes.Count; - /// Gets a reference to the node at (1-based, not nil). [UnscopedRef] [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref ExpressionNode NodeAt(Idx idx) @@ -226,13 +111,6 @@ public ref ExpressionNode NodeAt(Idx idx) return ref Nodes.GetSurePresentRef(idx.It - 1); } - /// Total number of nodes added so far. - public int NodeCount => Nodes.Count; - - // ------------------------------------------------------------------------- - // Internal: add a node and return its 1-based Idx - // ------------------------------------------------------------------------- - [MethodImpl(MethodImplOptions.AggressiveInlining)] private Idx AddNode( ExpressionType nodeType, @@ -250,231 +128,120 @@ private Idx AddNode( n.ChildIdx = childIdx; n.ExtraIdx = extraIdx; n.NextIdx = Idx.Nil; - return Idx.Of(Nodes.Count); // Count is already incremented by AddDefaultAndGetRef + return Idx.Of(Nodes.Count); // Count already incremented by AddDefaultAndGetRef } - // ------------------------------------------------------------------------- - // Factory — Constant - // ------------------------------------------------------------------------- + // Primitives with stable identity — safe to keep inline (ConstantIndex == -1). + private static bool IsInlineable(Type t) => + t == typeof(int) || t == typeof(long) || t == typeof(double) || t == typeof(float) || + t == typeof(bool) || t == typeof(string) || t == typeof(char) || + t == typeof(byte) || t == typeof(short) || t == typeof(decimal) || + t == typeof(DateTime) || t == typeof(Guid); - /// - /// Adds a constant node. Reference types and large structs are added to the closure - /// constants array so they can be mutated after compilation; plain primitives (int, bool, - /// string) are stored inline in . - /// public Idx Constant(object value, bool putIntoClosure = false) { if (value == null) return AddNode(ExpressionType.Constant, typeof(object), null); var type = value.GetType(); - if (!putIntoClosure && IsInlineable(type)) return AddNode(ExpressionType.Constant, type, value, constantIndex: -1); - // Add to closure constants var ci = ClosureConstants.Count; ClosureConstants.Add(value); return AddNode(ExpressionType.Constant, type, null, constantIndex: ci); } - /// Typed helper — avoids boxing for common value types. public Idx Constant(T value, bool putIntoClosure = false) => Constant((object)value, putIntoClosure); - // Primitive types that are cheap to keep inline (no closure slot needed by default). - private static bool IsInlineable(Type t) => - t == typeof(int) || t == typeof(long) || t == typeof(double) || t == typeof(float) || - t == typeof(bool) || t == typeof(string) || t == typeof(char) || - t == typeof(byte) || t == typeof(short) || t == typeof(decimal) || - t == typeof(DateTime) || t == typeof(Guid); - - // ------------------------------------------------------------------------- - // Factory — Parameter / Variable - // ------------------------------------------------------------------------- - - /// Adds a parameter node. Use the returned to refer to the same parameter. public Idx Parameter(Type type, string name = null) => AddNode(ExpressionType.Parameter, type, info: name); - /// Alias for (variables are parameters in lambda body). public Idx Variable(Type type, string name = null) => AddNode(ExpressionType.Parameter, type, info: name); - // ------------------------------------------------------------------------- - // Factory — Default - // ------------------------------------------------------------------------- - - /// Adds a default(T) node. public Idx Default(Type type) => AddNode(ExpressionType.Default, type); - // ------------------------------------------------------------------------- - // Factory — Unary - // ------------------------------------------------------------------------- - - /// Adds a unary expression node. public Idx Unary(ExpressionType nodeType, Idx operand, Type type, MethodInfo method = null) => AddNode(nodeType, type, info: method, childIdx: operand); - /// Typed convert. public Idx Convert(Idx operand, Type toType) => Unary(ExpressionType.Convert, operand, toType); - /// Logical not. public Idx Not(Idx operand) => Unary(ExpressionType.Not, operand, typeof(bool)); - /// Negate. public Idx Negate(Idx operand, Type type) => Unary(ExpressionType.Negate, operand, type); - // ------------------------------------------------------------------------- - // Factory — Binary - // ------------------------------------------------------------------------- - - /// Adds a binary expression node. public Idx Binary(ExpressionType nodeType, Idx left, Idx right, Type type, MethodInfo method = null) => AddNode(nodeType, type, info: method, childIdx: left, extraIdx: right); - /// Addition. public Idx Add(Idx left, Idx right, Type type) => Binary(ExpressionType.Add, left, right, type); - /// Subtraction. public Idx Subtract(Idx left, Idx right, Type type) => Binary(ExpressionType.Subtract, left, right, type); - /// Multiply. public Idx Multiply(Idx left, Idx right, Type type) => Binary(ExpressionType.Multiply, left, right, type); - /// Equal. public Idx Equal(Idx left, Idx right) => Binary(ExpressionType.Equal, left, right, typeof(bool)); - /// Assign. public Idx Assign(Idx target, Idx value, Type type) => Binary(ExpressionType.Assign, target, value, type); - // ------------------------------------------------------------------------- - // Factory — New - // ------------------------------------------------------------------------- - - /// - /// Adds a new T(args…) node. - /// Arguments are linked via in the order supplied. - /// public Idx New(ConstructorInfo ctor, params Idx[] args) { var firstArgIdx = LinkList(args); return AddNode(ExpressionType.New, ctor.DeclaringType, info: ctor, childIdx: firstArgIdx); } - // ------------------------------------------------------------------------- - // Factory — Call - // ------------------------------------------------------------------------- - - /// Adds a static or instance method call node. public Idx Call(MethodInfo method, Idx instance, params Idx[] args) { var returnType = method.ReturnType == typeof(void) ? typeof(void) : method.ReturnType; - if (instance.IsNil) - { - // Static call: all args hang from ChildIdx - var firstArgIdx = LinkList(args); - return AddNode(ExpressionType.Call, returnType, info: method, childIdx: firstArgIdx); - } - else - { - // Instance call: target → ChildIdx, args → ExtraIdx - var firstArgIdx = LinkList(args); - return AddNode(ExpressionType.Call, returnType, info: method, - childIdx: instance, extraIdx: firstArgIdx); - } + var firstArgIdx = LinkList(args); + return instance.IsNil + ? AddNode(ExpressionType.Call, returnType, info: method, childIdx: firstArgIdx) + : AddNode(ExpressionType.Call, returnType, info: method, childIdx: instance, extraIdx: firstArgIdx); } - // ------------------------------------------------------------------------- - // Factory — Lambda - // ------------------------------------------------------------------------- - - /// - /// Adds a lambda node. is the body expression; - /// are the parameter nodes (must already exist in - /// — reuse the values returned by - /// ). - /// - /// Parameters are stored in as an array - /// rather than linked via , because a parameter node's - /// NextIdx may already be in use as part of an argument chain (e.g. when the same parameter - /// is passed to a New or Call node). This is a key design trade-off: - /// one small allocation per lambda vs. avoiding silent list corruption. - /// - /// Sets when is true (default). - /// + // Parameters stored in Info as Idx[] rather than chained via NextIdx, because the same + // parameter node may already have its NextIdx used as part of a New/Call argument chain. public Idx Lambda(Type delegateType, Idx body, Idx[] parameters = null, bool isRoot = true) { - // Store params as Idx[] in Info — do NOT call LinkList to avoid corrupting NextIdx - // chains that the same param nodes may already participate in (e.g. as New/Call args). - var lambdaIdx = AddNode(ExpressionType.Lambda, delegateType, - info: parameters, // Idx[] or null - childIdx: body); // ExtraIdx left as Nil (unused for Lambda) + var lambdaIdx = AddNode(ExpressionType.Lambda, delegateType, info: parameters, childIdx: body); if (isRoot) RootIdx = lambdaIdx; return lambdaIdx; } - // ------------------------------------------------------------------------- - // Factory — Conditional - // ------------------------------------------------------------------------- - - /// Adds a conditional (ternary) expression. public Idx Conditional(Idx test, Idx ifTrue, Idx ifFalse, Type type) { - // ifTrue and ifFalse are siblings: link ifFalse as ifTrue.NextIdx - ref var ifTrueNode = ref NodeAt(ifTrue); - ifTrueNode.NextIdx = ifFalse; + NodeAt(ifTrue).NextIdx = ifFalse; // ifFalse hangs off ifTrue.NextIdx return AddNode(ExpressionType.Conditional, type, childIdx: test, extraIdx: ifTrue); } - // ------------------------------------------------------------------------- - // Factory — Block - // ------------------------------------------------------------------------- - - /// Adds a block expression. public Idx Block(Type type, Idx[] exprs, Idx[] variables = null) { var firstExprIdx = LinkList(exprs); - var firstVarIdx = variables == null || variables.Length == 0 - ? Idx.Nil - : LinkList(variables); + var firstVarIdx = variables == null || variables.Length == 0 ? Idx.Nil : LinkList(variables); return AddNode(ExpressionType.Block, type, childIdx: firstExprIdx, extraIdx: firstVarIdx); } - // ------------------------------------------------------------------------- - // Intrusive-list helpers - // ------------------------------------------------------------------------- - - /// - /// Chains the nodes at the given indices into a singly-linked list via - /// and returns the head. - /// The last node's NextIdx is left as . - /// public Idx LinkList(Idx[] indices) { if (indices == null || indices.Length == 0) return Idx.Nil; - for (var i = 0; i < indices.Length - 1; i++) NodeAt(indices[i]).NextIdx = indices[i + 1]; - - // Ensure last node's NextIdx is nil (in case it was previously linked elsewhere) - NodeAt(indices[indices.Length - 1]).NextIdx = Idx.Nil; - + NodeAt(indices[indices.Length - 1]).NextIdx = Idx.Nil; // reset in case node was previously linked return indices[0]; } - /// Iterates the sibling chain starting at . public IEnumerable Siblings(Idx head) { var cur = head; @@ -485,20 +252,7 @@ public IEnumerable Siblings(Idx head) } } - // ------------------------------------------------------------------------- - // Conversion to System.Linq.Expressions - // ------------------------------------------------------------------------- - - /// - /// Converts the flat tree back into a hierarchy so it can be - /// compiled with the standard compiler or FEC. - /// - /// Gaps / obstacles visible here: - /// • We need to materialise a for each Parameter node and - /// cache it by Idx so that shared parameters in nested lambdas resolve correctly. - /// • Closure constants are wrapped in a ConstantExpression; a real FEC - /// integration would inject them into an ArrayClosure instead. - /// + // Builds body after registering params so they are found in paramMap when encountered in the body. public SysExpr ToSystemExpression() => ToSystemExpression(RootIdx, new Dictionary()); private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramMap) @@ -533,42 +287,24 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM case ExpressionType.Lambda: { - // params are stored as Idx[] in Info (not linked via NextIdx — see Lambda factory) var paramIdxs = node.Info as Idx[]; var paramExprs = new List(); if (paramIdxs != null) foreach (var pIdx in paramIdxs) - { - // ToSystemExpression for parameters populates paramMap - var p = (SysParam)ToSystemExpression(pIdx, paramMap); - paramExprs.Add(p); - } - // Build body AFTER registering parameters so they are found in paramMap + paramExprs.Add((SysParam)ToSystemExpression(pIdx, paramMap)); var body = ToSystemExpression(node.ChildIdx, paramMap); return SysExpr.Lambda(node.Type, body, paramExprs); } case ExpressionType.New: - { - var ctor = (ConstructorInfo)node.Info; - var args = SiblingList(node.ChildIdx, paramMap); - return SysExpr.New(ctor, args); - } + return SysExpr.New((ConstructorInfo)node.Info, SiblingList(node.ChildIdx, paramMap)); case ExpressionType.Call: { var method = (MethodInfo)node.Info; - if (method.IsStatic) - { - var args = SiblingList(node.ChildIdx, paramMap); - return SysExpr.Call(method, args); - } - else - { - var target = ToSystemExpression(node.ChildIdx, paramMap); - var args = SiblingList(node.ExtraIdx, paramMap); - return SysExpr.Call(target, method, args); - } + return method.IsStatic + ? SysExpr.Call(method, SiblingList(node.ChildIdx, paramMap)) + : SysExpr.Call(ToSystemExpression(node.ChildIdx, paramMap), method, SiblingList(node.ExtraIdx, paramMap)); } case ExpressionType.Add: @@ -595,11 +331,10 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM case ExpressionType.RightShift: case ExpressionType.Power: case ExpressionType.Coalesce: - { - var left = ToSystemExpression(node.ChildIdx, paramMap); - var right = ToSystemExpression(node.ExtraIdx, paramMap); - return SysExpr.MakeBinary(node.NodeType, left, right, false, node.Info as MethodInfo); - } + return SysExpr.MakeBinary(node.NodeType, + ToSystemExpression(node.ChildIdx, paramMap), + ToSystemExpression(node.ExtraIdx, paramMap), + false, node.Info as MethodInfo); case ExpressionType.Negate: case ExpressionType.NegateChecked: @@ -617,25 +352,22 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM case ExpressionType.PostIncrementAssign: case ExpressionType.PreDecrementAssign: case ExpressionType.PostDecrementAssign: - { - var operand = ToSystemExpression(node.ChildIdx, paramMap); - return SysExpr.MakeUnary(node.NodeType, operand, node.Type, node.Info as MethodInfo); - } + return SysExpr.MakeUnary(node.NodeType, + ToSystemExpression(node.ChildIdx, paramMap), + node.Type, node.Info as MethodInfo); case ExpressionType.Conditional: - { - var test = ToSystemExpression(node.ChildIdx, paramMap); - var ifTrue = ToSystemExpression(node.ExtraIdx, paramMap); - var ifFalse = ToSystemExpression(NodeAt(node.ExtraIdx).NextIdx, paramMap); - return SysExpr.Condition(test, ifTrue, ifFalse, node.Type); - } + return SysExpr.Condition( + ToSystemExpression(node.ChildIdx, paramMap), + ToSystemExpression(node.ExtraIdx, paramMap), + ToSystemExpression(NodeAt(node.ExtraIdx).NextIdx, paramMap), + node.Type); case ExpressionType.Block: { var exprs = SiblingList(node.ChildIdx, paramMap); if (node.ExtraIdx.IsNil) return SysExpr.Block(node.Type, exprs); - var vars = new List(); foreach (var vIdx in Siblings(node.ExtraIdx)) vars.Add((SysParam)ToSystemExpression(vIdx, paramMap)); @@ -656,20 +388,11 @@ private List SiblingList(Idx head, Dictionary paramMap) return list; } - // ------------------------------------------------------------------------- - // Structural equality - // ------------------------------------------------------------------------- - - /// - /// Checks structural equality of two trees in O(n) time by comparing - /// every node field and every closure constant. - /// Win: no tree traversal required — a single loop over the flat arrays suffices. - /// + // O(n) structural equality — no traversal, single pass over the flat arrays. public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b) { if (a.NodeCount != b.NodeCount) return false; if (a.ClosureConstants.Count != b.ClosureConstants.Count) return false; - for (var i = 0; i < a.NodeCount; i++) { ref var na = ref a.Nodes.GetSurePresentRef(i); @@ -682,17 +405,16 @@ public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b) if (na.ChildIdx.It != nb.ChildIdx.It) return false; if (na.ExtraIdx.It != nb.ExtraIdx.It) return false; } - for (var i = 0; i < a.ClosureConstants.Count; i++) if (!Equals(a.ClosureConstants.GetSurePresentRef(i), b.ClosureConstants.GetSurePresentRef(i))) return false; - return true; } private static bool InfoEqual(object infoA, object infoB) { + // Lambda Info is Idx[] — Equals() on arrays checks reference equality, not contents. if (infoA is Idx[] ia && infoB is Idx[] ib) { if (ia.Length != ib.Length) return false; @@ -703,11 +425,6 @@ private static bool InfoEqual(object infoA, object infoB) return Equals(infoA, infoB); } - // ------------------------------------------------------------------------- - // Debug / diagnostic - // ------------------------------------------------------------------------- - - /// Returns a human-readable dump of all nodes (useful for diagnostics). public string Dump() { var sb = new System.Text.StringBuilder(); @@ -733,6 +450,6 @@ public string Dump() private static string InfoStr(object info) => info == null ? "—" : info is MethodBase mb ? mb.Name : - info is Idx[] idxArr ? $"params[{string.Join(",", System.Linq.Enumerable.Select(idxArr, x => x.It))}]" : + info is Idx[] idxArr ? $"params[{string.Join(",", Enumerable.Select(idxArr, x => x.It))}]" : info.ToString(); } diff --git a/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs index 77b0c295..2c5757b2 100644 --- a/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs +++ b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs @@ -1,22 +1,14 @@ -// FlatExpression is only in the FastExpressionCompiler assembly (not the LightExpression variant), -// so these tests are excluded from the LIGHT_EXPRESSION build. +// FlatExpression is only in the FastExpressionCompiler assembly, not the LightExpression variant. #if !LIGHT_EXPRESSION using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; -// FlatExpression lives in a separate namespace — no conditional #if needed here. using FastExpressionCompiler.FlatExpression; namespace FastExpressionCompiler.UnitTests; -/// -/// Demonstration / exploration tests for the POC. -/// -/// These tests double as living documentation of the design space — wins, gaps, and -/// open questions are highlighted inline. -/// public class FlatExpressionTests : ITest { public int Run() @@ -45,20 +37,15 @@ public int Run() Dump_does_not_throw(); - // Round-trip: build flat → System.Linq.Expressions → compile → invoke Roundtrip_lambda_identity_compile_and_invoke(); Roundtrip_lambda_add_compile_and_invoke(); - // Gap showcase: mutating a closure constant after building the flat tree. + // Closure constants can be swapped after tree construction without rebuilding. Closure_constant_is_mutable_after_build(); return 22; } - // ------------------------------------------------------------------------- - // Idx basics - // ------------------------------------------------------------------------- - public void Idx_default_is_nil() { var idx = default(Idx); @@ -74,10 +61,6 @@ public void Idx_of_is_one_based() Asserts.AreEqual(3, idx.It); } - // ------------------------------------------------------------------------- - // Node construction - // ------------------------------------------------------------------------- - public void Build_constant_node_inline() { var tree = default(ExpressionTree); @@ -90,7 +73,7 @@ public void Build_constant_node_inline() Asserts.AreEqual(ExpressionType.Constant, node.NodeType); Asserts.AreEqual(typeof(int), node.Type); Asserts.AreEqual(42, (int)node.Info); - Asserts.AreEqual(-1, node.ConstantIndex); // inline, not in closure + Asserts.AreEqual(-1, node.ConstantIndex); } public void Build_constant_node_in_closure() @@ -99,7 +82,7 @@ public void Build_constant_node_in_closure() var ci = tree.Constant("hello", putIntoClosure: true); ref var node = ref tree.NodeAt(ci); - Asserts.AreEqual(0, node.ConstantIndex); // first closure slot + Asserts.AreEqual(0, node.ConstantIndex); Asserts.AreEqual(1, tree.ClosureConstants.Count); Asserts.AreEqual("hello", (string)tree.ClosureConstants.GetSurePresentRef(0)); } @@ -131,19 +114,18 @@ public void Build_add_two_constants() public void Build_lambda_int_identity() { - // (x) => x (identity lambda: Func) var tree = default(ExpressionTree); var p = tree.Parameter(typeof(int), "x"); var lambdaIdx = tree.Lambda(typeof(Func), body: p, parameters: [p]); - Asserts.AreEqual(2, tree.NodeCount); // param + lambda + Asserts.AreEqual(2, tree.NodeCount); Asserts.AreEqual(lambdaIdx, tree.RootIdx); ref var lambda = ref tree.NodeAt(lambdaIdx); Asserts.AreEqual(ExpressionType.Lambda, lambda.NodeType); - Asserts.AreEqual(p, lambda.ChildIdx); // body + Asserts.AreEqual(p, lambda.ChildIdx); - // params are stored as Idx[] in Info (not via NextIdx/ExtraIdx) + // params stored as Idx[] in Info — not chained via NextIdx (see Lambda factory) var parms = (Idx[])lambda.Info; Asserts.AreEqual(1, parms.Length); Asserts.AreEqual(p, parms[0]); @@ -151,25 +133,23 @@ public void Build_lambda_int_identity() public void Build_lambda_add_two_params() { - // (x, y) => x + y var tree = default(ExpressionTree); var px = tree.Parameter(typeof(int), "x"); var py = tree.Parameter(typeof(int), "y"); var add = tree.Add(px, py, typeof(int)); var lambda = tree.Lambda(typeof(Func), body: add, parameters: [px, py]); - Asserts.AreEqual(4, tree.NodeCount); // px, py, add, lambda + Asserts.AreEqual(4, tree.NodeCount); ref var lambdaNode = ref tree.NodeAt(lambda); - Asserts.AreEqual(add, lambdaNode.ChildIdx); // body + Asserts.AreEqual(add, lambdaNode.ChildIdx); - // params stored as Idx[] in Info, NextIdx is NOT modified var parms = (Idx[])lambdaNode.Info; Asserts.AreEqual(2, parms.Length); Asserts.AreEqual(px, parms[0]); Asserts.AreEqual(py, parms[1]); - // px and py are NOT linked via NextIdx (design decision: avoids list conflicts) + // NextIdx is NOT touched — a param can still appear as a New/Call argument alongside being a lambda param ref var pxNode = ref tree.NodeAt(px); Asserts.IsTrue(pxNode.NextIdx.IsNil); } @@ -187,7 +167,6 @@ public void Build_new_expression() Asserts.AreEqual(typeof(Tuple), newNode.Type); Asserts.AreEqual(ctor, (ConstructorInfo)newNode.Info); - // args are chained var siblings = tree.Siblings(newNode.ChildIdx).ToArray(); Asserts.AreEqual(2, siblings.Length); Asserts.AreEqual(arg1, siblings[0]); @@ -208,31 +187,25 @@ public void Build_call_static_method() public void Build_conditional() { - // x > 0 ? x : -x var tree = default(ExpressionTree); var x = tree.Parameter(typeof(int), "x"); var zero = tree.Constant(0); var test = tree.Binary(ExpressionType.GreaterThan, x, zero, typeof(bool)); var neg = tree.Negate(x, typeof(int)); - // Note: Conditional modifies ifTrue.NextIdx — so ifTrue should be a fresh node. - // Here we re-use x as ifTrue which is fine since NextIdx on parameter node - // was not set yet (it was linked only in ExtraIdx of lambda in other tests). - // For clarity build a fresh constant: var xCopy = tree.Parameter(typeof(int), "x_copy"); var cond = tree.Conditional(test, xCopy, neg, typeof(int)); ref var condNode = ref tree.NodeAt(cond); Asserts.AreEqual(ExpressionType.Conditional, condNode.NodeType); Asserts.AreEqual(test, condNode.ChildIdx); - Asserts.AreEqual(xCopy, condNode.ExtraIdx); // ifTrue - // ifFalse is linked as ifTrue.NextIdx + Asserts.AreEqual(xCopy, condNode.ExtraIdx); + // ifFalse is chained as ifTrue.NextIdx ref var ifTrueNode = ref tree.NodeAt(xCopy); Asserts.AreEqual(neg, ifTrueNode.NextIdx); } public void Build_block_with_variable() { - // { int v = 0; return v; } var tree = default(ExpressionTree); var v = tree.Variable(typeof(int), "v"); var zero = tree.Constant(0); @@ -241,14 +214,10 @@ public void Build_block_with_variable() ref var blockNode = ref tree.NodeAt(blockIdx); Asserts.AreEqual(ExpressionType.Block, blockNode.NodeType); - Asserts.IsFalse(blockNode.ChildIdx.IsNil); // exprs - Asserts.IsFalse(blockNode.ExtraIdx.IsNil); // vars + Asserts.IsFalse(blockNode.ChildIdx.IsNil); + Asserts.IsFalse(blockNode.ExtraIdx.IsNil); } - // ------------------------------------------------------------------------- - // Structural equality - // ------------------------------------------------------------------------- - public void Structural_equality_same_trees() { var t1 = BuildAddTree(); @@ -262,19 +231,14 @@ public void Structural_equality_different_trees() var t2 = default(ExpressionTree); var a = t2.Constant(10); - var b = t2.Constant(99); // different constant value: StructurallyEqual compares Info via Equals + var b = t2.Constant(99); t2.Add(a, b, typeof(int)); Asserts.IsFalse(ExpressionTree.StructurallyEqual(ref t1, ref t2)); } - // ------------------------------------------------------------------------- - // ToSystemExpression - // ------------------------------------------------------------------------- - public void Convert_to_system_expression_constant_lambda() { - // () => 42 var tree = default(ExpressionTree); var c = tree.Constant(42); tree.Lambda(typeof(Func), body: c); @@ -286,7 +250,6 @@ public void Convert_to_system_expression_constant_lambda() public void Convert_to_system_expression_add_lambda() { - // (x, y) => x + y var tree = default(ExpressionTree); var px = tree.Parameter(typeof(int), "x"); var py = tree.Parameter(typeof(int), "y"); @@ -300,7 +263,6 @@ public void Convert_to_system_expression_add_lambda() public void Convert_to_system_expression_new_lambda() { - // (n) => new Tuple(n, "x") var ctor = typeof(Tuple).GetConstructor([typeof(int), typeof(string)]); var tree = default(ExpressionTree); var n = tree.Parameter(typeof(int), "n"); @@ -312,61 +274,41 @@ public void Convert_to_system_expression_new_lambda() Asserts.AreEqual(ExpressionType.New, sysExpr.Body.NodeType); } - // ------------------------------------------------------------------------- - // Round-trip: flat → System.Linq.Expressions → compile → invoke - // ------------------------------------------------------------------------- - public void Roundtrip_lambda_identity_compile_and_invoke() { - // (x) => x var tree = default(ExpressionTree); var p = tree.Parameter(typeof(int), "x"); tree.Lambda(typeof(Func), body: p, parameters: [p]); - var sysExpr = (Expression>)tree.ToSystemExpression(); - var fn = sysExpr.Compile(); + var fn = ((Expression>)tree.ToSystemExpression()).Compile(); Asserts.AreEqual(7, fn(7)); } public void Roundtrip_lambda_add_compile_and_invoke() { - // (x, y) => x + y var tree = default(ExpressionTree); var px = tree.Parameter(typeof(int), "x"); var py = tree.Parameter(typeof(int), "y"); var add = tree.Add(px, py, typeof(int)); tree.Lambda(typeof(Func), body: add, parameters: [px, py]); - var sysExpr = (Expression>)tree.ToSystemExpression(); - var fn = sysExpr.Compile(); + var fn = ((Expression>)tree.ToSystemExpression()).Compile(); Asserts.AreEqual(11, fn(4, 7)); } - // ------------------------------------------------------------------------- - // Gap showcase: mutable closure constant - // ------------------------------------------------------------------------- - public void Closure_constant_is_mutable_after_build() { - // Build a lambda that captures a constant via the closure array. - // After building we can swap the constant value without rebuilding the tree - // (same Idx still points to the same closure slot). var tree = default(ExpressionTree); var c = tree.Constant("initial", putIntoClosure: true); tree.Lambda(typeof(Func), body: c); - // Change the constant at slot 0 + // Swap constant in-place; the Idx still points to the same closure slot. tree.ClosureConstants.GetSurePresentRef(0) = "updated"; - var sysExpr = (Expression>)tree.ToSystemExpression(); - var fn = sysExpr.Compile(); + var fn = ((Expression>)tree.ToSystemExpression()).Compile(); Asserts.AreEqual("updated", fn()); } - // ------------------------------------------------------------------------- - // Diagnostic - // ------------------------------------------------------------------------- - public void Dump_does_not_throw() { var tree = BuildAddTree(); @@ -375,10 +317,6 @@ public void Dump_does_not_throw() Asserts.IsTrue(dump.Contains("ExpressionTree")); } - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- - private static ExpressionTree BuildAddTree() { var tree = default(ExpressionTree); From 76ffb84aef1fc817d880bef6f3b42cc503b78ae3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:03:23 +0000 Subject: [PATCH 4/6] Replace Dictionary with SmallMap16; inline sibling iteration in hot paths Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/869f4a36-48ea-4e91-b89b-1079e333c0d4 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler/FlatExpression.cs | 57 ++++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs index ae37db94..7eeb5f6e 100644 --- a/src/FastExpressionCompiler/FlatExpression.cs +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -242,6 +242,7 @@ public Idx LinkList(Idx[] indices) return indices[0]; } + // Allocates an enumerator — suitable for tests and diagnostics; avoid in hot paths. public IEnumerable Siblings(Idx head) { var cur = head; @@ -253,9 +254,13 @@ public IEnumerable Siblings(Idx head) } // Builds body after registering params so they are found in paramMap when encountered in the body. - public SysExpr ToSystemExpression() => ToSystemExpression(RootIdx, new Dictionary()); + public SysExpr ToSystemExpression() + { + var paramMap = default(SmallMap16); + return ToSystemExpression(RootIdx, ref paramMap); + } - private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramMap) + private SysExpr ToSystemExpression(Idx nodeIdx, ref SmallMap16 paramMap) { if (nodeIdx.IsNil) throw new InvalidOperationException("Cannot convert nil Idx to System.Linq.Expressions"); @@ -274,11 +279,9 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM case ExpressionType.Parameter: { - if (!paramMap.TryGetValue(nodeIdx.It, out var p)) - { + ref var p = ref paramMap.Map.AddOrGetValueRef(nodeIdx.It, out var found); + if (!found) p = SysExpr.Parameter(node.Type, node.Info as string); - paramMap[nodeIdx.It] = p; - } return p; } @@ -291,20 +294,20 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM var paramExprs = new List(); if (paramIdxs != null) foreach (var pIdx in paramIdxs) - paramExprs.Add((SysParam)ToSystemExpression(pIdx, paramMap)); - var body = ToSystemExpression(node.ChildIdx, paramMap); + paramExprs.Add((SysParam)ToSystemExpression(pIdx, ref paramMap)); + var body = ToSystemExpression(node.ChildIdx, ref paramMap); return SysExpr.Lambda(node.Type, body, paramExprs); } case ExpressionType.New: - return SysExpr.New((ConstructorInfo)node.Info, SiblingList(node.ChildIdx, paramMap)); + return SysExpr.New((ConstructorInfo)node.Info, SiblingList(node.ChildIdx, ref paramMap)); case ExpressionType.Call: { var method = (MethodInfo)node.Info; return method.IsStatic - ? SysExpr.Call(method, SiblingList(node.ChildIdx, paramMap)) - : SysExpr.Call(ToSystemExpression(node.ChildIdx, paramMap), method, SiblingList(node.ExtraIdx, paramMap)); + ? SysExpr.Call(method, SiblingList(node.ChildIdx, ref paramMap)) + : SysExpr.Call(ToSystemExpression(node.ChildIdx, ref paramMap), method, SiblingList(node.ExtraIdx, ref paramMap)); } case ExpressionType.Add: @@ -332,8 +335,8 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM case ExpressionType.Power: case ExpressionType.Coalesce: return SysExpr.MakeBinary(node.NodeType, - ToSystemExpression(node.ChildIdx, paramMap), - ToSystemExpression(node.ExtraIdx, paramMap), + ToSystemExpression(node.ChildIdx, ref paramMap), + ToSystemExpression(node.ExtraIdx, ref paramMap), false, node.Info as MethodInfo); case ExpressionType.Negate: @@ -353,24 +356,28 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM case ExpressionType.PreDecrementAssign: case ExpressionType.PostDecrementAssign: return SysExpr.MakeUnary(node.NodeType, - ToSystemExpression(node.ChildIdx, paramMap), + ToSystemExpression(node.ChildIdx, ref paramMap), node.Type, node.Info as MethodInfo); case ExpressionType.Conditional: return SysExpr.Condition( - ToSystemExpression(node.ChildIdx, paramMap), - ToSystemExpression(node.ExtraIdx, paramMap), - ToSystemExpression(NodeAt(node.ExtraIdx).NextIdx, paramMap), + ToSystemExpression(node.ChildIdx, ref paramMap), + ToSystemExpression(node.ExtraIdx, ref paramMap), + ToSystemExpression(NodeAt(node.ExtraIdx).NextIdx, ref paramMap), node.Type); case ExpressionType.Block: { - var exprs = SiblingList(node.ChildIdx, paramMap); + var exprs = SiblingList(node.ChildIdx, ref paramMap); if (node.ExtraIdx.IsNil) return SysExpr.Block(node.Type, exprs); var vars = new List(); - foreach (var vIdx in Siblings(node.ExtraIdx)) - vars.Add((SysParam)ToSystemExpression(vIdx, paramMap)); + var vCur = node.ExtraIdx; + while (!vCur.IsNil) + { + vars.Add((SysParam)ToSystemExpression(vCur, ref paramMap)); + vCur = NodeAt(vCur).NextIdx; + } return SysExpr.Block(node.Type, vars, exprs); } @@ -380,11 +387,15 @@ private SysExpr ToSystemExpression(Idx nodeIdx, Dictionary paramM } } - private List SiblingList(Idx head, Dictionary paramMap) + private List SiblingList(Idx head, ref SmallMap16 paramMap) { var list = new List(); - foreach (var idx in Siblings(head)) - list.Add(ToSystemExpression(idx, paramMap)); + var cur = head; + while (!cur.IsNil) + { + list.Add(ToSystemExpression(cur, ref paramMap)); + cur = NodeAt(cur).NextIdx; + } return list; } From b9df8ea8a6254c9799e0399fa4235ee96a4227df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:32:13 +0000 Subject: [PATCH 5/6] =?UTF-8?q?Compress=20ExpressionNode:=2040=E2=86=9232?= =?UTF-8?q?=20bytes,=20inline=20small=20constants,=20remove=20ConstantInde?= =?UTF-8?q?x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/40e281a9-12ac-41f4-aa81-9a61179a0c47 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler/FlatExpression.cs | 108 ++++++++++++++---- .../FlatExpressionTests.cs | 7 +- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs index 7eeb5f6e..d47290ac 100644 --- a/src/FastExpressionCompiler/FlatExpression.cs +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -65,7 +65,13 @@ public struct Idx : IEquatable /// /// Fat node in . Intrusive linked-list tree encoding: /// -/// Constant Info = boxed value; ConstantIndex ≥ 0 → value lives in ClosureConstants instead. +/// Constant +/// +/// ExtraIdx.It == 0 (nil): value is in Info (boxed, or null for null constant).
+/// ExtraIdx.It > 0: value is ClosureConstants[ExtraIdx.It - 1] (1-based).
+/// ExtraIdx.It == -1: int32-fitting value (bool/byte/int/float/…) stored inline in ChildIdx.It bits — no boxing. +///
+///
/// Parameter Info = name (string or null). /// Unary Info = MethodInfo (nullable), ChildIdx = operand. /// Binary Info = MethodInfo (nullable), ChildIdx = left, ExtraIdx = right. @@ -75,17 +81,25 @@ public struct Idx : IEquatable /// Block ChildIdx = first expr, ExtraIdx = first variable (both chained via NextIdx). /// ConditionalChildIdx = test, ExtraIdx = ifTrue; ifFalse = ifTrue.NextIdx. ///
+/// +/// Layout: 32 bytes on 64-bit (refs first eliminates 4-byte padding after NodeType).
+/// vs LightExpression heap objects (16-byte header + fields):
+/// Constant/Parameter: ~40 bytes heap | Binary/Unary: ~48–56 bytes heap +///
///
[StructLayout(LayoutKind.Sequential)] -public struct ExpressionNode +public struct ExpressionNode // 32 bytes: Type(8)+Info(8)+NodeType(4)+NextIdx(4)+ChildIdx(4)+ExtraIdx(4) { - public ExpressionType NodeType; + // Reference fields placed first to avoid 4-byte padding that would appear after NodeType. public Type Type; public object Info; - /// ≥ 0: index into . -1: value is inline in Info. - public int ConstantIndex; + public ExpressionType NodeType; public Idx NextIdx; + /// First child node, or for Constant with ExtraIdx.It==-1: raw int32 value bits. public Idx ChildIdx; + /// + /// Second child node; for Constant: 0=value in Info, positive=ClosureConstants index (1-based), -1=inline bits in ChildIdx.It. + /// public Idx ExtraIdx; } @@ -116,7 +130,6 @@ private Idx AddNode( ExpressionType nodeType, Type type, object info = null, - int constantIndex = -1, Idx childIdx = default, Idx extraIdx = default) { @@ -124,32 +137,74 @@ private Idx AddNode( n.NodeType = nodeType; n.Type = type; n.Info = info; - n.ConstantIndex = constantIndex; n.ChildIdx = childIdx; n.ExtraIdx = extraIdx; n.NextIdx = Idx.Nil; return Idx.Of(Nodes.Count); // Count already incremented by AddDefaultAndGetRef } - // Primitives with stable identity — safe to keep inline (ConstantIndex == -1). - private static bool IsInlineable(Type t) => - t == typeof(int) || t == typeof(long) || t == typeof(double) || t == typeof(float) || - t == typeof(bool) || t == typeof(string) || t == typeof(char) || - t == typeof(byte) || t == typeof(short) || t == typeof(decimal) || - t == typeof(DateTime) || t == typeof(Guid); + // Types whose value fits in 32 bits — stored inline in ChildIdx.It to avoid boxing. + private static bool FitsInInt32(Type t) => + t == typeof(int) || t == typeof(uint) || t == typeof(bool) || t == typeof(float) || + t == typeof(byte) || t == typeof(sbyte) || t == typeof(short) || t == typeof(ushort) || + t == typeof(char); + + // Encode an inline value as its int32 bit pattern (only call when FitsInInt32 is true). + private static int ToInt32Bits(object value, Type t) + { + if (t == typeof(int)) return (int)value; + if (t == typeof(uint)) return (int)(uint)value; // reinterpret bits + if (t == typeof(bool)) return (bool)value ? 1 : 0; + if (t == typeof(float)) return BitConverter.SingleToInt32Bits((float)value); + if (t == typeof(byte)) return (byte)value; + if (t == typeof(sbyte)) return (sbyte)value; + if (t == typeof(short)) return (short)value; + if (t == typeof(ushort)) return (ushort)value; + if (t == typeof(char)) return (char)value; + return 0; // unreachable + } + + // Decode int32 bit pattern back to a boxed value (only call when FitsInInt32 is true). + internal static object FromInt32Bits(int bits, Type t) + { + if (t == typeof(int)) return bits; + if (t == typeof(uint)) return (uint)bits; + if (t == typeof(bool)) return bits != 0; + if (t == typeof(float)) return BitConverter.Int32BitsToSingle(bits); + if (t == typeof(byte)) return (byte)bits; + if (t == typeof(sbyte)) return (sbyte)bits; + if (t == typeof(short)) return (short)bits; + if (t == typeof(ushort)) return (ushort)bits; + if (t == typeof(char)) return (char)bits; + return null; // unreachable + } + + // Types not fitting in int32 but still safe to keep inline in Info (no special closure treatment needed). + private static bool IsInfoInline(Type t) => + t == typeof(string) || t == typeof(long) || t == typeof(double) || + t == typeof(decimal)|| t == typeof(DateTime)|| t == typeof(Guid); public Idx Constant(object value, bool putIntoClosure = false) { if (value == null) - return AddNode(ExpressionType.Constant, typeof(object), null); + return AddNode(ExpressionType.Constant, typeof(object)); var type = value.GetType(); - if (!putIntoClosure && IsInlineable(type)) - return AddNode(ExpressionType.Constant, type, value, constantIndex: -1); + if (!putIntoClosure) + { + if (FitsInInt32(type)) + // ExtraIdx.It == -1 is the "inline bits" sentinel; ChildIdx.It holds the value. + return AddNode(ExpressionType.Constant, type, + childIdx: new Idx { It = ToInt32Bits(value, type) }, + extraIdx: new Idx { It = -1 }); + if (IsInfoInline(type)) + return AddNode(ExpressionType.Constant, type, info: value); + } var ci = ClosureConstants.Count; ClosureConstants.Add(value); - return AddNode(ExpressionType.Constant, type, null, constantIndex: ci); + // ExtraIdx.It > 0 (1-based) identifies the closure constant slot. + return AddNode(ExpressionType.Constant, type, extraIdx: new Idx { It = ci + 1 }); } public Idx Constant(T value, bool putIntoClosure = false) => @@ -271,9 +326,13 @@ private SysExpr ToSystemExpression(Idx nodeIdx, ref SmallMap16= 0 - ? ClosureConstants.GetSurePresentRef(node.ConstantIndex) - : node.Info; + object value; + if (node.ExtraIdx.It > 0) + value = ClosureConstants.GetSurePresentRef(node.ExtraIdx.It - 1); + else if (node.ExtraIdx.It == -1) + value = FromInt32Bits(node.ChildIdx.It, node.Type); + else + value = node.Info; return SysExpr.Constant(value, node.Type); } @@ -411,7 +470,6 @@ public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b) if (na.NodeType != nb.NodeType) return false; if (na.Type != nb.Type) return false; if (!InfoEqual(na.Info, nb.Info)) return false; - if (na.ConstantIndex != nb.ConstantIndex) return false; if (na.NextIdx.It != nb.NextIdx.It) return false; if (na.ChildIdx.It != nb.ChildIdx.It) return false; if (na.ExtraIdx.It != nb.ExtraIdx.It) return false; @@ -443,10 +501,14 @@ public string Dump() for (var i = 0; i < NodeCount; i++) { ref var n = ref Nodes.GetSurePresentRef(i); + var constStr = n.NodeType == ExpressionType.Constant + ? (n.ExtraIdx.It > 0 ? $"closure[{n.ExtraIdx.It - 1}]" : + n.ExtraIdx.It == -1 ? $"inline:{FromInt32Bits(n.ChildIdx.It, n.Type)}" : + $"info:{n.Info}") + : null; sb.AppendLine( $" [{i + 1}] {n.NodeType,-22} type={n.Type?.Name,-14} " + - $"info={InfoStr(n.Info),-30} " + - $"ci={n.ConstantIndex,2} " + + $"{(constStr != null ? $"val={constStr,-28}" : $"info={InfoStr(n.Info),-28}")} " + $"child={n.ChildIdx} extra={n.ExtraIdx} next={n.NextIdx}"); } if (ClosureConstants.Count > 0) diff --git a/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs index 2c5757b2..2bd0cc68 100644 --- a/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs +++ b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs @@ -72,8 +72,9 @@ public void Build_constant_node_inline() ref var node = ref tree.NodeAt(ci); Asserts.AreEqual(ExpressionType.Constant, node.NodeType); Asserts.AreEqual(typeof(int), node.Type); - Asserts.AreEqual(42, (int)node.Info); - Asserts.AreEqual(-1, node.ConstantIndex); + Asserts.AreEqual(null, node.Info); + Asserts.AreEqual(-1, node.ExtraIdx.It); // inline bits sentinel + Asserts.AreEqual(42, node.ChildIdx.It); // inline int32 bits } public void Build_constant_node_in_closure() @@ -82,7 +83,7 @@ public void Build_constant_node_in_closure() var ci = tree.Constant("hello", putIntoClosure: true); ref var node = ref tree.NodeAt(ci); - Asserts.AreEqual(0, node.ConstantIndex); + Asserts.AreEqual(1, node.ExtraIdx.It); // 1-based closure index Asserts.AreEqual(1, tree.ClosureConstants.Count); Asserts.AreEqual("hello", (string)tree.ClosureConstants.GetSurePresentRef(0)); } From 116a5937f0e36c097f2770f1835bfd3d09aebe65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 07:59:33 +0000 Subject: [PATCH 6/6] Fix netstandard2.0 build: use FloatIntBits union instead of BitConverter.SingleToInt32Bits; add XML docs to all public members Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/8d5fabaa-d5e2-458c-b771-309266696c44 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler/FlatExpression.cs | 56 +++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs index d47290ac..7ec70058 100644 --- a/src/FastExpressionCompiler/FlatExpression.cs +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -48,17 +48,25 @@ namespace FastExpressionCompiler.FlatExpression; [StructLayout(LayoutKind.Sequential)] public struct Idx : IEquatable { + /// Raw 1-based index; 0 means nil. public int It; + /// True when this index is nil (unset). public bool IsNil => It == 0; + /// The nil sentinel value. public static Idx Nil => default; + /// Creates a 1-based index from the given value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Idx Of(int oneBasedIndex) => new Idx { It = oneBasedIndex }; + /// public bool Equals(Idx other) => It == other.It; + /// public override bool Equals(object obj) => obj is Idx other && Equals(other); + /// public override int GetHashCode() => It; + /// public override string ToString() => IsNil ? "nil" : It.ToString(); } @@ -91,9 +99,13 @@ public struct Idx : IEquatable public struct ExpressionNode // 32 bytes: Type(8)+Info(8)+NodeType(4)+NextIdx(4)+ChildIdx(4)+ExtraIdx(4) { // Reference fields placed first to avoid 4-byte padding that would appear after NodeType. + /// Result type of this node. public Type Type; + /// Method/constructor for Call/New/Unary/Binary; parameter name for Parameter; closure key for Constant; parameter array for Lambda. public object Info; + /// Expression kind (mirrors ). public ExpressionType NodeType; + /// Next sibling in an intrusive linked list (arguments, block expressions, etc.). public Idx NextIdx; /// First child node, or for Constant with ExtraIdx.It==-1: raw int32 value bits. public Idx ChildIdx; @@ -110,13 +122,18 @@ public struct ExpressionNode // 32 bytes: Type(8)+Info(8)+NodeType(4)+NextIdx(4 public struct ExpressionTree { // First 16 nodes are on the stack; further nodes spill to a heap array. + /// Flat node storage. First 16 nodes are stack-resident; further nodes spill to a heap array. public SmallList, NoArrayPool> Nodes; // First 4 closure constants on stack. + /// Closure-captured constants. First 4 are stack-resident. public SmallList, NoArrayPool> ClosureConstants; + /// Index of the root expression node (typically a Lambda). public Idx RootIdx; + /// Total number of nodes in this tree. public int NodeCount => Nodes.Count; + /// Returns a reference to the node at the given index. [UnscopedRef] [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref ExpressionNode NodeAt(Idx idx) @@ -155,7 +172,7 @@ private static int ToInt32Bits(object value, Type t) if (t == typeof(int)) return (int)value; if (t == typeof(uint)) return (int)(uint)value; // reinterpret bits if (t == typeof(bool)) return (bool)value ? 1 : 0; - if (t == typeof(float)) return BitConverter.SingleToInt32Bits((float)value); + if (t == typeof(float)) return FloatIntBits.FloatToInt((float)value); if (t == typeof(byte)) return (byte)value; if (t == typeof(sbyte)) return (sbyte)value; if (t == typeof(short)) return (short)value; @@ -170,7 +187,7 @@ internal static object FromInt32Bits(int bits, Type t) if (t == typeof(int)) return bits; if (t == typeof(uint)) return (uint)bits; if (t == typeof(bool)) return bits != 0; - if (t == typeof(float)) return BitConverter.Int32BitsToSingle(bits); + if (t == typeof(float)) return FloatIntBits.IntToFloat(bits); if (t == typeof(byte)) return (byte)bits; if (t == typeof(sbyte)) return (sbyte)bits; if (t == typeof(short)) return (short)bits; @@ -179,11 +196,22 @@ internal static object FromInt32Bits(int bits, Type t) return null; // unreachable } + // Explicit-layout union to reinterpret float/int bits without Unsafe or BitConverter (portable across all TFMs). + [StructLayout(LayoutKind.Explicit)] + private struct FloatIntBits + { + [FieldOffset(0)] public float F; + [FieldOffset(0)] public int I; + public static int FloatToInt(float f) => new FloatIntBits { F = f }.I; + public static float IntToFloat(int i) => new FloatIntBits { I = i }.F; + } + // Types not fitting in int32 but still safe to keep inline in Info (no special closure treatment needed). private static bool IsInfoInline(Type t) => t == typeof(string) || t == typeof(long) || t == typeof(double) || t == typeof(decimal)|| t == typeof(DateTime)|| t == typeof(Guid); + /// Adds a Constant node. Small value types (int, bool, float, etc.) are stored inline without boxing. public Idx Constant(object value, bool putIntoClosure = false) { if (value == null) @@ -207,54 +235,70 @@ public Idx Constant(object value, bool putIntoClosure = false) return AddNode(ExpressionType.Constant, type, extraIdx: new Idx { It = ci + 1 }); } + /// Typed overload of . public Idx Constant(T value, bool putIntoClosure = false) => Constant((object)value, putIntoClosure); + /// Adds a Parameter node with the given type and optional name. public Idx Parameter(Type type, string name = null) => AddNode(ExpressionType.Parameter, type, info: name); + /// Alias for — adds a block-local variable node. public Idx Variable(Type type, string name = null) => AddNode(ExpressionType.Parameter, type, info: name); + /// Adds a Default(type) node. public Idx Default(Type type) => AddNode(ExpressionType.Default, type); + /// Adds a unary expression node. public Idx Unary(ExpressionType nodeType, Idx operand, Type type, MethodInfo method = null) => AddNode(nodeType, type, info: method, childIdx: operand); + /// Adds a Convert node. public Idx Convert(Idx operand, Type toType) => Unary(ExpressionType.Convert, operand, toType); + /// Adds a Not node. public Idx Not(Idx operand) => Unary(ExpressionType.Not, operand, typeof(bool)); + /// Adds a Negate node. public Idx Negate(Idx operand, Type type) => Unary(ExpressionType.Negate, operand, type); + /// Adds a binary expression node. public Idx Binary(ExpressionType nodeType, Idx left, Idx right, Type type, MethodInfo method = null) => AddNode(nodeType, type, info: method, childIdx: left, extraIdx: right); + /// Adds an Add node. public Idx Add(Idx left, Idx right, Type type) => Binary(ExpressionType.Add, left, right, type); + /// Adds a Subtract node. public Idx Subtract(Idx left, Idx right, Type type) => Binary(ExpressionType.Subtract, left, right, type); + /// Adds a Multiply node. public Idx Multiply(Idx left, Idx right, Type type) => Binary(ExpressionType.Multiply, left, right, type); + /// Adds an Equal node (returns bool). public Idx Equal(Idx left, Idx right) => Binary(ExpressionType.Equal, left, right, typeof(bool)); + /// Adds an Assign node. public Idx Assign(Idx target, Idx value, Type type) => Binary(ExpressionType.Assign, target, value, type); + /// Adds a New node calling the given constructor with the provided arguments. public Idx New(ConstructorInfo ctor, params Idx[] args) { var firstArgIdx = LinkList(args); return AddNode(ExpressionType.New, ctor.DeclaringType, info: ctor, childIdx: firstArgIdx); } + /// Adds a Call node. Pass for for static calls. public Idx Call(MethodInfo method, Idx instance, params Idx[] args) { var returnType = method.ReturnType == typeof(void) ? typeof(void) : method.ReturnType; @@ -266,6 +310,7 @@ public Idx Call(MethodInfo method, Idx instance, params Idx[] args) // Parameters stored in Info as Idx[] rather than chained via NextIdx, because the same // parameter node may already have its NextIdx used as part of a New/Call argument chain. + /// Adds a Lambda node. Sets when is true. public Idx Lambda(Type delegateType, Idx body, Idx[] parameters = null, bool isRoot = true) { var lambdaIdx = AddNode(ExpressionType.Lambda, delegateType, info: parameters, childIdx: body); @@ -274,12 +319,14 @@ public Idx Lambda(Type delegateType, Idx body, Idx[] parameters = null, bool isR return lambdaIdx; } + /// Adds a Conditional (ternary) node. public Idx Conditional(Idx test, Idx ifTrue, Idx ifFalse, Type type) { NodeAt(ifTrue).NextIdx = ifFalse; // ifFalse hangs off ifTrue.NextIdx return AddNode(ExpressionType.Conditional, type, childIdx: test, extraIdx: ifTrue); } + /// Adds a Block node containing the given expressions and optional block-local variables. public Idx Block(Type type, Idx[] exprs, Idx[] variables = null) { var firstExprIdx = LinkList(exprs); @@ -287,6 +334,7 @@ public Idx Block(Type type, Idx[] exprs, Idx[] variables = null) return AddNode(ExpressionType.Block, type, childIdx: firstExprIdx, extraIdx: firstVarIdx); } + /// Chains the given indices via and returns the first index. public Idx LinkList(Idx[] indices) { if (indices == null || indices.Length == 0) @@ -298,6 +346,7 @@ public Idx LinkList(Idx[] indices) } // Allocates an enumerator — suitable for tests and diagnostics; avoid in hot paths. + /// Enumerates the sibling chain starting at . Allocates an enumerator — avoid in hot paths. public IEnumerable Siblings(Idx head) { var cur = head; @@ -309,6 +358,7 @@ public IEnumerable Siblings(Idx head) } // Builds body after registering params so they are found in paramMap when encountered in the body. + /// Converts this flat tree to a rooted at . public SysExpr ToSystemExpression() { var paramMap = default(SmallMap16); @@ -459,6 +509,7 @@ private List SiblingList(Idx head, ref SmallMap16 } // O(n) structural equality — no traversal, single pass over the flat arrays. + /// O(n) structural equality check. Compares both trees node-by-node in a single pass — no recursive traversal. public static bool StructurallyEqual(ref ExpressionTree a, ref ExpressionTree b) { if (a.NodeCount != b.NodeCount) return false; @@ -494,6 +545,7 @@ private static bool InfoEqual(object infoA, object infoB) return Equals(infoA, infoB); } + /// Returns a human-readable dump of all nodes and closure constants for diagnostics. public string Dump() { var sb = new System.Text.StringBuilder();