diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs new file mode 100644 index 00000000..7ec70058 --- /dev/null +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -0,0 +1,580 @@ +/* +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. +*/ + +// 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 + +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 == nil. +[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(); +} + +/// +/// Fat node in . Intrusive linked-list tree encoding: +/// +/// 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. +/// 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. +///
+/// +/// 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 // 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; + /// + /// Second child node; for Constant: 0=value in Info, positive=ClosureConstants index (1-based), -1=inline bits in ChildIdx.It. + /// + public Idx ExtraIdx; +} + +/// +/// 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 +{ + // 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) + { + Debug.Assert(!idx.IsNil, "Cannot dereference a nil Idx"); + return ref Nodes.GetSurePresentRef(idx.It - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Idx AddNode( + ExpressionType nodeType, + Type type, + object info = null, + Idx childIdx = default, + Idx extraIdx = default) + { + ref var n = ref Nodes.AddDefaultAndGetRef(); + n.NodeType = nodeType; + n.Type = type; + n.Info = info; + n.ChildIdx = childIdx; + n.ExtraIdx = extraIdx; + n.NextIdx = Idx.Nil; + return Idx.Of(Nodes.Count); // Count already incremented by AddDefaultAndGetRef + } + + // 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 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; + 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 FloatIntBits.IntToFloat(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 + } + + // 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) + return AddNode(ExpressionType.Constant, typeof(object)); + + var type = value.GetType(); + 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); + // ExtraIdx.It > 0 (1-based) identifies the closure constant slot. + 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; + 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); + } + + // 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); + if (isRoot) + RootIdx = lambdaIdx; + 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); + var firstVarIdx = variables == null || variables.Length == 0 ? Idx.Nil : LinkList(variables); + 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) + return Idx.Nil; + for (var i = 0; i < indices.Length - 1; i++) + NodeAt(indices[i]).NextIdx = indices[i + 1]; + NodeAt(indices[indices.Length - 1]).NextIdx = Idx.Nil; // reset in case node was previously linked + return indices[0]; + } + + // 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; + while (!cur.IsNil) + { + yield return cur; + cur = NodeAt(cur).NextIdx; + } + } + + // 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); + return ToSystemExpression(RootIdx, ref paramMap); + } + + private SysExpr ToSystemExpression(Idx nodeIdx, ref SmallMap16 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: + { + 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); + } + + case ExpressionType.Parameter: + { + ref var p = ref paramMap.Map.AddOrGetValueRef(nodeIdx.It, out var found); + if (!found) + p = SysExpr.Parameter(node.Type, node.Info as string); + return p; + } + + case ExpressionType.Default: + return SysExpr.Default(node.Type); + + case ExpressionType.Lambda: + { + var paramIdxs = node.Info as Idx[]; + var paramExprs = new List(); + if (paramIdxs != null) + foreach (var pIdx in paramIdxs) + 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, ref paramMap)); + + case ExpressionType.Call: + { + var method = (MethodInfo)node.Info; + return method.IsStatic + ? SysExpr.Call(method, SiblingList(node.ChildIdx, ref paramMap)) + : SysExpr.Call(ToSystemExpression(node.ChildIdx, ref paramMap), method, SiblingList(node.ExtraIdx, ref paramMap)); + } + + 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: + return SysExpr.MakeBinary(node.NodeType, + ToSystemExpression(node.ChildIdx, ref paramMap), + ToSystemExpression(node.ExtraIdx, ref paramMap), + 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: + return SysExpr.MakeUnary(node.NodeType, + ToSystemExpression(node.ChildIdx, ref paramMap), + node.Type, node.Info as MethodInfo); + + case ExpressionType.Conditional: + return SysExpr.Condition( + 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, ref paramMap); + if (node.ExtraIdx.IsNil) + return SysExpr.Block(node.Type, exprs); + var vars = new List(); + 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); + } + + default: + throw new NotSupportedException( + $"FlatExpression → System.Linq.Expressions: NodeType {node.NodeType} is not yet mapped."); + } + } + + private List SiblingList(Idx head, ref SmallMap16 paramMap) + { + var list = new List(); + var cur = head; + while (!cur.IsNil) + { + list.Add(ToSystemExpression(cur, ref paramMap)); + cur = NodeAt(cur).NextIdx; + } + return list; + } + + // 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; + 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.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) + { + // 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; + for (var k = 0; k < ia.Length; k++) + if (ia[k].It != ib[k].It) return false; + return true; + } + 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(); + sb.AppendLine($"ExpressionTree NodeCount={NodeCount} ClosureConstants={ClosureConstants.Count} RootIdx={RootIdx}"); + 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} " + + $"{(constStr != null ? $"val={constStr,-28}" : $"info={InfoStr(n.Info),-28}")} " + + $"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(",", 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..2bd0cc68 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs @@ -0,0 +1,330 @@ +// 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; + +using FastExpressionCompiler.FlatExpression; + +namespace FastExpressionCompiler.UnitTests; + +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(); + + Roundtrip_lambda_identity_compile_and_invoke(); + Roundtrip_lambda_add_compile_and_invoke(); + + // Closure constants can be swapped after tree construction without rebuilding. + Closure_constant_is_mutable_after_build(); + + return 22; + } + + 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); + } + + 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(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() + { + var tree = default(ExpressionTree); + var ci = tree.Constant("hello", putIntoClosure: true); + + ref var node = ref tree.NodeAt(ci); + Asserts.AreEqual(1, node.ExtraIdx.It); // 1-based closure index + 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() + { + 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); + Asserts.AreEqual(lambdaIdx, tree.RootIdx); + + ref var lambda = ref tree.NodeAt(lambdaIdx); + Asserts.AreEqual(ExpressionType.Lambda, lambda.NodeType); + Asserts.AreEqual(p, lambda.ChildIdx); + + // 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]); + } + + public void Build_lambda_add_two_params() + { + 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); + + ref var lambdaNode = ref tree.NodeAt(lambda); + Asserts.AreEqual(add, lambdaNode.ChildIdx); + + var parms = (Idx[])lambdaNode.Info; + Asserts.AreEqual(2, parms.Length); + Asserts.AreEqual(px, parms[0]); + Asserts.AreEqual(py, parms[1]); + + // 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); + } + + 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); + + 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() + { + 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)); + 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); + // ifFalse is chained as ifTrue.NextIdx + ref var ifTrueNode = ref tree.NodeAt(xCopy); + Asserts.AreEqual(neg, ifTrueNode.NextIdx); + } + + public void Build_block_with_variable() + { + 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); + Asserts.IsFalse(blockNode.ExtraIdx.IsNil); + } + + 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); + t2.Add(a, b, typeof(int)); + + Asserts.IsFalse(ExpressionTree.StructurallyEqual(ref t1, ref t2)); + } + + public void Convert_to_system_expression_constant_lambda() + { + 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() + { + 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() + { + 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); + } + + public void Roundtrip_lambda_identity_compile_and_invoke() + { + var tree = default(ExpressionTree); + var p = tree.Parameter(typeof(int), "x"); + tree.Lambda(typeof(Func), body: p, parameters: [p]); + + var fn = ((Expression>)tree.ToSystemExpression()).Compile(); + Asserts.AreEqual(7, fn(7)); + } + + public void Roundtrip_lambda_add_compile_and_invoke() + { + 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 fn = ((Expression>)tree.ToSystemExpression()).Compile(); + Asserts.AreEqual(11, fn(4, 7)); + } + + public void Closure_constant_is_mutable_after_build() + { + var tree = default(ExpressionTree); + var c = tree.Constant("initial", putIntoClosure: true); + tree.Lambda(typeof(Func), body: c); + + // Swap constant in-place; the Idx still points to the same closure slot. + tree.ClosureConstants.GetSurePresentRef(0) = "updated"; + + var fn = ((Expression>)tree.ToSystemExpression()).Compile(); + Asserts.AreEqual("updated", fn()); + } + + public void Dump_does_not_throw() + { + var tree = BuildAddTree(); + var dump = tree.Dump(); + Asserts.IsNotNull(dump); + Asserts.IsTrue(dump.Contains("ExpressionTree")); + } + + 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