diff --git a/src/FastExpressionCompiler.LightExpression/FastExpressionCompiler.LightExpression.csproj b/src/FastExpressionCompiler.LightExpression/FastExpressionCompiler.LightExpression.csproj index f44de903..be16e18b 100644 --- a/src/FastExpressionCompiler.LightExpression/FastExpressionCompiler.LightExpression.csproj +++ b/src/FastExpressionCompiler.LightExpression/FastExpressionCompiler.LightExpression.csproj @@ -120,6 +120,7 @@ https://github.com/dadhi/FastExpressionCompiler/compare/v5.3.3...v5.4.0 + diff --git a/src/FastExpressionCompiler/FlatExpression.cs b/src/FastExpressionCompiler/FlatExpression.cs new file mode 100644 index 00000000..b3c9208e --- /dev/null +++ b/src/FastExpressionCompiler/FlatExpression.cs @@ -0,0 +1,873 @@ +/* +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. +// NextIdx-based sibling chaining: children linked via node.NextIdx, no count fields. +// Block uses two internal sub-nodes (BlockVarList, BlockExprList) chained via NextIdx to separate vars from exprs. +// 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; +#if LIGHT_EXPRESSION +using FastExpressionCompiler.LightExpression.ImTools; +#else +using FastExpressionCompiler.ImTools; +#endif + +using SysExpr = System.Linq.Expressions.Expression; +using SysParam = System.Linq.Expressions.ParameterExpression; + +/// 1-based index into . default == nil. Backed by a 2-byte short. +[StructLayout(LayoutKind.Sequential)] +public struct Idx : IEquatable +{ + /// Raw 1-based index; 0 means nil. + public short 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) + { + Debug.Assert(oneBasedIndex >= 0 && oneBasedIndex <= short.MaxValue, "Index out of short range"); + return new Idx { It = (short)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(); +} + +/// +/// Compact 24-byte node. Two reference fields followed by the 64-bit data word. +/// Layout: Type(8) + Obj(8) + _data(8) = 24 bytes — no padding waste. +/// +/// Constant inline +/// = true (Obj == ); +/// holds raw bits (up to 8 bytes: bool/int/long/float/double/DateTime/…). +/// Constant closure +/// ChildIdx = 1-based slot in . +/// Constant in Obj +/// ChildIdx = 0; value in (null, string, decimal, Guid, …). +/// Parameter / DefaultObj = name (string or null). +/// Unary Obj = MethodInfo?; ChildIdx = operand; ExtraIdx = nil. +/// Binary Obj = MethodInfo?; ChildIdx = left; ExtraIdx = right. +/// New / Call-staticObj = CtorInfo/MethodInfo; ChildIdx = first arg (args chained via NextIdx). +/// Call instanceObj = MethodInfo; ChildIdx = instance; ExtraIdx = first arg (args chained via NextIdx). +/// Lambda Obj = Idx[] of params (in Obj to avoid NextIdx aliasing); ChildIdx = body. +/// Block ChildIdx → internal BlockVarList node (ChildIdx=first var, NextIdx→BlockExprList); BlockExprList.ChildIdx=first expr. +/// ConditionalChildIdx = test; ExtraIdx = ifTrue; ifFalse must be at ExtraIdx+1 (consecutive). +/// +/// _data bit layout (non-inline-const): bits[63:57]=NodeType(7) | bits[56:41]=ChildIdx(16) | bits[40:25]=NextIdx(16) | bits[24:9]=ExtraIdx(16) | bits[8:0]=spare(9). +/// vs LightExpression heap objects (16-byte GC header + fields): Constant/Parameter ~40 bytes | Binary/Unary ~48–56 bytes. +/// +[StructLayout(LayoutKind.Sequential)] +public struct ExpressionNode // 24 bytes: Type(8)+Obj(8)+_data(8) +{ + /// Result type of this node. + public Type Type; + /// Object payload: method/ctor for Call/New/Unary/Binary; param name for Parameter; type for TypeIs/TypeEqual; Idx[] for Lambda params; constant value for non-inline/non-closure Constant nodes; for inline constants. + public object Obj; + // _data bit layout when not inplace-const (see struct summary above). + // When Obj == InplaceConstValueMarker: all 64 bits = inline constant value. + internal long _data; + + /// True when this Constant node's value is stored inline in (Obj == ). + public bool IsInplaceConst => ReferenceEquals(Obj, ExpressionTree.InplaceConstValueMarker); + /// Expression kind (derived from upper 7 bits of , or when ). + public ExpressionType NodeType => IsInplaceConst ? ExpressionType.Constant : (ExpressionType)((ulong)_data >> 57); + /// First child index, or 1-based closure slot for non-inline Constant nodes. + public Idx ChildIdx => Idx.Of((short)((_data >> 41) & 0xFFFF)); + /// Next sibling index in a linked list (0 = end of list). + public Idx NextIdx => Idx.Of((short)((_data >> 25) & 0xFFFF)); + /// Second child: right for Binary; ifTrue for Conditional; first arg for instance Call/Invoke. + public Idx ExtraIdx => Idx.Of((short)((_data >> 9) & 0xFFFF)); + /// Raw 8-byte constant bits when is true. + public long Data => _data; + + /// Sets the NextIdx bits in _data without touching other fields. Used by LinkList to chain sibling nodes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SetNextIdx(Idx next) => + _data = (_data & ~(0xFFFFL << 25)) | ((long)(ushort)next.It << 25); +} + +/// +/// 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; + + /// Sentinel placed in to signal that holds the full 8-byte inline constant value. + public static readonly object InplaceConstValueMarker = new object(); + + /// 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); + } + + // Packs NodeType + ChildIdx + NextIdx + ExtraIdx into the 64-bit _data word. + // ExpressionType max value is 83 (IsFalse), well within the 7-bit (0–127) field. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long PackData( + ExpressionType nodeType, + short childIdx = 0, + short nextIdx = 0, + short extraIdx = 0) + { + Debug.Assert((int)nodeType >= 0 && (int)nodeType <= 127, "ExpressionType must fit in 7 bits"); + return ((long)nodeType << 57) | // 7 bits at [63:57] + ((long)(ushort)childIdx << 41) | // 16 bits at [56:41] + ((long)(ushort)nextIdx << 25) | // 16 bits at [40:25] + ((long)(ushort)extraIdx << 9); // 16 bits at [24:9] + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Idx AddNode( + ExpressionType nodeType, + Type type, + object obj = null, + short childIdx = 0, + short nextIdx = 0, + short extraIdx = 0) + { + ref var n = ref Nodes.AddDefaultAndGetRef(); + n.Type = type; + n.Obj = obj; + n._data = PackData(nodeType, childIdx, nextIdx, extraIdx); + return Idx.Of(Nodes.Count); // Count already incremented by AddDefaultAndGetRef + } + + // Chains nodes[0]→nodes[1]→…→nil via SetNextIdx; returns nodes[0] (nil when empty). + // Arg nodes must not be shared across multiple sibling chains — NextIdx is intrusive. + private Idx LinkList(Idx[] nodes) + { + if (nodes == null || nodes.Length == 0) return Idx.Nil; + for (var i = 0; i < nodes.Length - 1; i++) + { + Debug.Assert(!nodes[i].IsNil, "LinkList: nil index at position " + i); + NodeAt(nodes[i]).SetNextIdx(nodes[i + 1]); + } + Debug.Assert(!nodes[nodes.Length - 1].IsNil, "LinkList: nil index at last position"); + NodeAt(nodes[nodes.Length - 1]).SetNextIdx(Idx.Nil); + return nodes[0]; + } + + // Types whose value fits in 64 bits — stored inline in _data to avoid boxing. + private static bool FitsInInt64(Type t) + { + switch (Type.GetTypeCode(t)) + { + case TypeCode.Boolean: + case TypeCode.Char: + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Single: + case TypeCode.Int64: + case TypeCode.UInt64: + case TypeCode.Double: + case TypeCode.DateTime: + return true; + default: + return false; + } + } + + // Encode an inline value as its int64 bit pattern (only call when FitsInInt64 is true). + private static long ToInt64Bits(object value, Type t) + { + switch (Type.GetTypeCode(t)) + { + case TypeCode.Int64: return (long)value; + case TypeCode.UInt64: return (long)(ulong)value; + case TypeCode.Double: return BitConverter.DoubleToInt64Bits((double)value); + case TypeCode.DateTime: return ((DateTime)value).ToBinary(); + case TypeCode.Int32: return (int)value; + case TypeCode.UInt32: return (uint)value; + case TypeCode.Boolean: return (bool)value ? 1 : 0; + case TypeCode.Single: return FloatIntBits.FloatToInt((float)value); + case TypeCode.Byte: return (byte)value; + case TypeCode.SByte: return (sbyte)value; + case TypeCode.Int16: return (short)value; + case TypeCode.UInt16: return (ushort)value; + case TypeCode.Char: return (char)value; + default: return 0; // unreachable + } + } + + // Decode int64 bit pattern back to a boxed value (only call when FitsInInt64 is true). + internal static object FromInt64Bits(long bits, Type t) + { + switch (Type.GetTypeCode(t)) + { + case TypeCode.Int64: return bits; + case TypeCode.UInt64: return (ulong)bits; + case TypeCode.Double: return BitConverter.Int64BitsToDouble(bits); + case TypeCode.DateTime: return DateTime.FromBinary(bits); + case TypeCode.Int32: return (int)bits; + case TypeCode.UInt32: return (uint)bits; + case TypeCode.Boolean: return bits != 0; + case TypeCode.Single: return FloatIntBits.IntToFloat((int)bits); + case TypeCode.Byte: return (byte)bits; + case TypeCode.SByte: return (sbyte)bits; + case TypeCode.Int16: return (short)bits; + case TypeCode.UInt16: return (ushort)bits; + case TypeCode.Char: return (char)bits; + default: 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; + } + + /// Adds a Constant node. Value types up to 8 bytes (int, bool, long, double, DateTime, 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 (FitsInInt64(type)) + { + // Obj = InplaceConstValueMarker signals that _data holds the raw constant value. + ref var n = ref Nodes.AddDefaultAndGetRef(); + n.Type = type; + n.Obj = InplaceConstValueMarker; + n._data = ToInt64Bits(value, type); + return Idx.Of(Nodes.Count); + } + // String, decimal, Guid, and other reference/large types go directly in Obj. + return AddNode(ExpressionType.Constant, type, obj: value); + } + + var ci = ClosureConstants.Count; + ClosureConstants.Add(value); + // ChildIdx = 1-based closure slot. + return AddNode(ExpressionType.Constant, type, childIdx: (short)(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, obj: name); + + /// Alias for — adds a block-local variable node. + public Idx Variable(Type type, string name = null) => + AddNode(ExpressionType.Parameter, type, obj: 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, obj: method, childIdx: operand.It); + + /// 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, obj: method, childIdx: left.It, extraIdx: right.It); + + /// 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. Arguments are chained via NextIdx. + public Idx New(ConstructorInfo ctor, params Idx[] args) => + AddNode(ExpressionType.New, ctor.DeclaringType, obj: ctor, childIdx: LinkList(args).It); + + /// Adds a Call node. Pass for for static calls. Arguments are chained via NextIdx. + public Idx Call(MethodInfo method, Idx instance, params Idx[] args) + { + var returnType = method.ReturnType == typeof(void) ? typeof(void) : method.ReturnType; + return instance.IsNil + ? AddNode(ExpressionType.Call, returnType, obj: method, childIdx: LinkList(args).It) + : AddNode(ExpressionType.Call, returnType, obj: method, childIdx: instance.It, extraIdx: LinkList(args).It); + } + + // Parameters stored in Obj as Idx[] rather than chained via NextIdx, because the same + // parameter node may already have its NextIdx occupied 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, obj: parameters, childIdx: body.It); + if (isRoot) + RootIdx = lambdaIdx; + return lambdaIdx; + } + + /// Adds a Conditional (ternary) node. and must be consecutively allocated (ifFalse.It == ifTrue.It + 1). + public Idx Conditional(Idx test, Idx ifTrue, Idx ifFalse, Type type) + { + Debug.Assert(ifFalse.It == ifTrue.It + 1, "ifTrue and ifFalse must be consecutively allocated for Conditional"); + // ExtraIdx = ifTrue; ifFalse is implicit at ExtraIdx+1 (consecutive). + return AddNode(ExpressionType.Conditional, type, childIdx: test.It, extraIdx: ifTrue.It); + } + + // Internal sentinel NodeTypes for Block sub-nodes — stored in _data's 7-bit NodeType field (max 127). + // The public ExpressionType enum currently tops out at 83 (IsFalse); 120/121 are safely in the + // reserved gap 84–127 and will never appear in public expression trees. + private const ExpressionType NodeTypeBlockVarList = (ExpressionType)120; + private const ExpressionType NodeTypeBlockExprList = (ExpressionType)121; + + /// + /// Adds a Block node. Internally allocates two sub-nodes: a BlockVarList node (ChildIdx = first var, NextIdx = BlockExprList) + /// and a BlockExprList node (ChildIdx = first expr). Vars and exprs are each chained via NextIdx. + /// + public Idx Block(Type type, Idx[] exprs, Idx[] variables = null) + { + // Build exprs sub-node first (so its index is known for blockVars.NextIdx). + var blockExprsIdx = AddNode(NodeTypeBlockExprList, typeof(void), childIdx: LinkList(exprs).It); + // Build vars sub-node, pointing NextIdx at blockExprs. + var blockVarsIdx = AddNode(NodeTypeBlockVarList, typeof(void), childIdx: LinkList(variables).It, nextIdx: blockExprsIdx.It); + return AddNode(ExpressionType.Block, type, childIdx: blockVarsIdx.It); + } + + // ── Additional convenience shorthands for binary ops ─────────────────────────────────────── + + /// Adds a NotEqual node (returns bool). + public Idx NotEqual(Idx left, Idx right) => + Binary(ExpressionType.NotEqual, left, right, typeof(bool)); + + /// Adds a LessThan node (returns bool). + public Idx LessThan(Idx left, Idx right) => + Binary(ExpressionType.LessThan, left, right, typeof(bool)); + + /// Adds a LessThanOrEqual node (returns bool). + public Idx LessThanOrEqual(Idx left, Idx right) => + Binary(ExpressionType.LessThanOrEqual, left, right, typeof(bool)); + + /// Adds a GreaterThan node (returns bool). + public Idx GreaterThan(Idx left, Idx right) => + Binary(ExpressionType.GreaterThan, left, right, typeof(bool)); + + /// Adds a GreaterThanOrEqual node (returns bool). + public Idx GreaterThanOrEqual(Idx left, Idx right) => + Binary(ExpressionType.GreaterThanOrEqual, left, right, typeof(bool)); + + /// Adds an AndAlso (short-circuit &&) node. + public Idx AndAlso(Idx left, Idx right) => + Binary(ExpressionType.AndAlso, left, right, typeof(bool)); + + /// Adds an OrElse (short-circuit ||) node. + public Idx OrElse(Idx left, Idx right) => + Binary(ExpressionType.OrElse, left, right, typeof(bool)); + + /// Adds a Coalesce (??) node. + public Idx Coalesce(Idx left, Idx right, Type type) => + Binary(ExpressionType.Coalesce, left, right, type); + + /// Adds an ArrayIndex node. The node must have an array type. + public Idx ArrayIndex(Idx array, Idx index) => + Binary(ExpressionType.ArrayIndex, array, index, NodeAt(array).Type.GetElementType() + ?? throw new ArgumentException("Array node type must be an array type.", nameof(array))); + + // ── Compound-assignment convenience shorthands ────────────────────────────────────────────── + + /// Adds an AddAssign node. + public Idx AddAssign(Idx target, Idx value, Type type) => + Binary(ExpressionType.AddAssign, target, value, type); + + /// Adds a SubtractAssign node. + public Idx SubtractAssign(Idx target, Idx value, Type type) => + Binary(ExpressionType.SubtractAssign, target, value, type); + + /// Adds a MultiplyAssign node. + public Idx MultiplyAssign(Idx target, Idx value, Type type) => + Binary(ExpressionType.MultiplyAssign, target, value, type); + + // ── MemberAccess ──────────────────────────────────────────────────────────────────────────── + // Obj = MemberInfo; ChildIdx = instance (nil for static). + + /// Adds a MemberAccess node for a field or property. Pass for static members. + public Idx MemberAccess(Idx instance, MemberInfo member) + { + Type memberType; + if (member is PropertyInfo pi) + memberType = pi.PropertyType; + else if (member is FieldInfo fi) + memberType = fi.FieldType; + else + throw new ArgumentException($"MemberAccess requires a FieldInfo or PropertyInfo, got {member.GetType().Name}.", nameof(member)); + return AddNode(ExpressionType.MemberAccess, memberType, obj: member, childIdx: instance.It); + } + + /// Adds a MemberAccess node for a field. + public Idx Field(Idx instance, FieldInfo field) => MemberAccess(instance, field); + + /// Adds a MemberAccess node for a property. + public Idx Property(Idx instance, PropertyInfo property) => MemberAccess(instance, property); + + // ── Invoke ────────────────────────────────────────────────────────────────────────────────── + // ChildIdx = delegate expression; ExtraIdx = first argument (args chained via NextIdx). + + /// Adds an Invoke node (delegate invocation). Arguments are chained via NextIdx. + public Idx Invoke(Idx delegateExpr, Type returnType, params Idx[] args) => + AddNode(ExpressionType.Invoke, returnType, + childIdx: delegateExpr.It, + extraIdx: LinkList(args).It); + + // ── TypeIs / TypeEqual ────────────────────────────────────────────────────────────────────── + // Obj = Type to test against; ChildIdx = expression. + + /// Adds a TypeIs node (returns bool; true when expr is a subtype of ). + public Idx TypeIs(Idx expr, Type type) => + AddNode(ExpressionType.TypeIs, typeof(bool), obj: type, childIdx: expr.It); + + /// Adds a TypeEqual node (returns bool; true when expr's exact runtime type equals ). + public Idx TypeEqual(Idx expr, Type type) => + AddNode(ExpressionType.TypeEqual, typeof(bool), obj: type, childIdx: expr.It); + + // ── NewArrayInit / NewArrayBounds ─────────────────────────────────────────────────────────── + // Type = array type; ChildIdx = first element/bound (elements chained via NextIdx). + + /// Adds a NewArrayInit node (creates and initializes a 1D array). Elements are chained via NextIdx. + public Idx NewArrayInit(Type elementType, params Idx[] elements) => + AddNode(ExpressionType.NewArrayInit, elementType.MakeArrayType(), + childIdx: LinkList(elements).It); + + /// Adds a NewArrayBounds node (creates an array given dimension bounds). Bounds are chained via NextIdx. + public Idx NewArrayBounds(Type elementType, params Idx[] bounds) => + AddNode(ExpressionType.NewArrayBounds, elementType.MakeArrayType(), + childIdx: LinkList(bounds).It); + + // Allocates an enumerator — suitable for tests/diagnostics; avoid in hot paths. + /// Enumerates the sibling chain starting at via NextIdx. Allocates an enumerator — avoid in hot paths. + public IEnumerable Siblings(Idx firstIdx) + { + for (var cur = firstIdx; !cur.IsNil; cur = NodeAt(cur).NextIdx) + yield return cur; + } + + // 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.IsInplaceConst) + value = FromInt64Bits(node.Data, node.Type); + else if (node.ChildIdx.It > 0) + value = ClosureConstants.GetSurePresentRef(node.ChildIdx.It - 1); + else + value = node.Obj; + 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.Obj as string); + return p; + } + + case ExpressionType.Default: + return SysExpr.Default(node.Type); + + case ExpressionType.Lambda: + { + var paramIdxs = node.Obj 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.Obj, SiblingListSE(node.ChildIdx, ref paramMap)); + + case ExpressionType.NewArrayInit: + return SysExpr.NewArrayInit(node.Type.GetElementType(), SiblingListSE(node.ChildIdx, ref paramMap)); + + case ExpressionType.NewArrayBounds: + return SysExpr.NewArrayBounds(node.Type.GetElementType(), SiblingListSE(node.ChildIdx, ref paramMap)); + + case ExpressionType.Call: + { + var method = (MethodInfo)node.Obj; + return method.IsStatic + ? SysExpr.Call(method, SiblingListSE(node.ChildIdx, ref paramMap)) + : SysExpr.Call(ToSystemExpression(node.ChildIdx, ref paramMap), method, SiblingListSE(node.ExtraIdx, ref paramMap)); + } + + case ExpressionType.Invoke: + return SysExpr.Invoke(ToSystemExpression(node.ChildIdx, ref paramMap), SiblingListSE(node.ExtraIdx, ref paramMap)); + + case ExpressionType.MemberAccess: + { + var member = (MemberInfo)node.Obj; + return SysExpr.MakeMemberAccess(node.ChildIdx.IsNil ? null : ToSystemExpression(node.ChildIdx, ref paramMap), member); + } + + case ExpressionType.TypeIs: + return SysExpr.TypeIs(ToSystemExpression(node.ChildIdx, ref paramMap), (Type)node.Obj); + + case ExpressionType.TypeEqual: + return SysExpr.TypeEqual(ToSystemExpression(node.ChildIdx, ref paramMap), (Type)node.Obj); + + case ExpressionType.Conditional: + // ifTrue = ExtraIdx; ifFalse = ExtraIdx+1 (consecutive allocation required). + return SysExpr.Condition( + ToSystemExpression(node.ChildIdx, ref paramMap), + ToSystemExpression(node.ExtraIdx, ref paramMap), + ToSystemExpression(Idx.Of(node.ExtraIdx.It + 1), ref paramMap), + node.Type); + + case ExpressionType.Block: + { + // Block.ChildIdx → BlockVarList node; BlockVarList.NextIdx → BlockExprList node. + ref var varsNode = ref NodeAt(node.ChildIdx); + ref var exprsNode = ref NodeAt(varsNode.NextIdx); + var exprs = SiblingListSE(exprsNode.ChildIdx, ref paramMap); + var vars = new List(); + for (var vi = varsNode.ChildIdx; !vi.IsNil; vi = NodeAt(vi).NextIdx) + vars.Add((SysParam)ToSystemExpression(vi, ref paramMap)); + return SysExpr.Block(node.Type, vars, exprs); + } + + default: + // All Binary and Unary node types: use ExtraIdx presence to distinguish. + if (!node.ExtraIdx.IsNil) + return SysExpr.MakeBinary(node.NodeType, + ToSystemExpression(node.ChildIdx, ref paramMap), + ToSystemExpression(node.ExtraIdx, ref paramMap), + false, node.Obj as MethodInfo); + return SysExpr.MakeUnary(node.NodeType, + ToSystemExpression(node.ChildIdx, ref paramMap), + node.Type, node.Obj as MethodInfo); + } + } + + private List SiblingListSE(Idx firstIdx, ref SmallMap16 paramMap) + { + var list = new List(); + for (var cur = firstIdx; !cur.IsNil; cur = NodeAt(cur).NextIdx) + list.Add(ToSystemExpression(cur, ref paramMap)); + return list; + } + +#if LIGHT_EXPRESSION + /// Converts this flat tree to a rooted at . + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("FastExpressionCompiler is not supported in trimming scenarios.")] + public FastExpressionCompiler.LightExpression.Expression ToLightExpression() + { + var paramMap = default(SmallMap16); + return ToLightExpression(RootIdx, ref paramMap); + } + + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("FastExpressionCompiler is not supported in trimming scenarios.")] + private FastExpressionCompiler.LightExpression.Expression ToLightExpression( + Idx nodeIdx, ref SmallMap16 paramMap) + { + if (nodeIdx.IsNil) + throw new InvalidOperationException("Cannot convert nil Idx to LightExpression"); + + ref var node = ref NodeAt(nodeIdx); + + switch (node.NodeType) + { + case ExpressionType.Constant: + { + object value; + if (node.IsInplaceConst) + value = FromInt64Bits(node.Data, node.Type); + else if (node.ChildIdx.It > 0) + value = ClosureConstants.GetSurePresentRef(node.ChildIdx.It - 1); + else + value = node.Obj; + return FastExpressionCompiler.LightExpression.Expression.Constant(value, node.Type); + } + + case ExpressionType.Parameter: + { + ref var p = ref paramMap.Map.AddOrGetValueRef(nodeIdx.It, out var found); + if (!found) + p = FastExpressionCompiler.LightExpression.Expression.Parameter(node.Type, node.Obj as string); + return p; + } + + case ExpressionType.Default: + return FastExpressionCompiler.LightExpression.Expression.Default(node.Type); + + case ExpressionType.Lambda: + { + var paramIdxs = node.Obj as Idx[]; + var paramExprs = new List(); + if (paramIdxs != null) + foreach (var pIdx in paramIdxs) + paramExprs.Add((FastExpressionCompiler.LightExpression.ParameterExpression)ToLightExpression(pIdx, ref paramMap)); + var body = ToLightExpression(node.ChildIdx, ref paramMap); + return FastExpressionCompiler.LightExpression.Expression.Lambda(node.Type, body, paramExprs); + } + + case ExpressionType.New: + return FastExpressionCompiler.LightExpression.Expression.New( + (ConstructorInfo)node.Obj, SiblingListLE(node.ChildIdx, ref paramMap)); + + case ExpressionType.NewArrayInit: + return FastExpressionCompiler.LightExpression.Expression.NewArrayInit( + node.Type.GetElementType(), SiblingListLE(node.ChildIdx, ref paramMap)); + + case ExpressionType.NewArrayBounds: + return FastExpressionCompiler.LightExpression.Expression.NewArrayBounds( + node.Type.GetElementType(), SiblingListLE(node.ChildIdx, ref paramMap)); + + case ExpressionType.Call: + { + var method = (MethodInfo)node.Obj; + return method.IsStatic + ? FastExpressionCompiler.LightExpression.Expression.Call(method, SiblingListLE(node.ChildIdx, ref paramMap)) + : FastExpressionCompiler.LightExpression.Expression.Call(ToLightExpression(node.ChildIdx, ref paramMap), method, SiblingListLE(node.ExtraIdx, ref paramMap)); + } + + case ExpressionType.Invoke: + return FastExpressionCompiler.LightExpression.Expression.Invoke( + ToLightExpression(node.ChildIdx, ref paramMap), SiblingListLE(node.ExtraIdx, ref paramMap)); + + case ExpressionType.MemberAccess: + { + var member = (MemberInfo)node.Obj; + var instance = node.ChildIdx.IsNil ? null : ToLightExpression(node.ChildIdx, ref paramMap); + if (member is FieldInfo fi) + return FastExpressionCompiler.LightExpression.Expression.Field(instance, fi); + return FastExpressionCompiler.LightExpression.Expression.Property(instance, (PropertyInfo)member); + } + + case ExpressionType.TypeIs: + return FastExpressionCompiler.LightExpression.Expression.TypeIs( + ToLightExpression(node.ChildIdx, ref paramMap), (Type)node.Obj); + + case ExpressionType.TypeEqual: + return FastExpressionCompiler.LightExpression.Expression.TypeEqual( + ToLightExpression(node.ChildIdx, ref paramMap), (Type)node.Obj); + + case ExpressionType.Conditional: + // ifTrue = ExtraIdx; ifFalse = ExtraIdx+1 (consecutive allocation required). + return FastExpressionCompiler.LightExpression.Expression.Condition( + ToLightExpression(node.ChildIdx, ref paramMap), + ToLightExpression(node.ExtraIdx, ref paramMap), + ToLightExpression(Idx.Of(node.ExtraIdx.It + 1), ref paramMap), + node.Type); + + case ExpressionType.Block: + { + // Block.ChildIdx → BlockVarList node; BlockVarList.NextIdx → BlockExprList node. + ref var varsNode = ref NodeAt(node.ChildIdx); + ref var exprsNode = ref NodeAt(varsNode.NextIdx); + var exprs = SiblingListLE(exprsNode.ChildIdx, ref paramMap); + var vars = new List(); + for (var vi = varsNode.ChildIdx; !vi.IsNil; vi = NodeAt(vi).NextIdx) + vars.Add((FastExpressionCompiler.LightExpression.ParameterExpression)ToLightExpression(vi, ref paramMap)); + return FastExpressionCompiler.LightExpression.Expression.Block(node.Type, vars, exprs); + } + + default: + // All Binary and Unary node types: use ExtraIdx presence to distinguish. + if (!node.ExtraIdx.IsNil) + return FastExpressionCompiler.LightExpression.Expression.MakeBinary(node.NodeType, + ToLightExpression(node.ChildIdx, ref paramMap), + ToLightExpression(node.ExtraIdx, ref paramMap), + false, node.Obj as MethodInfo); + return FastExpressionCompiler.LightExpression.Expression.MakeUnary(node.NodeType, + ToLightExpression(node.ChildIdx, ref paramMap), + node.Type, node.Obj as MethodInfo); + } + } + + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("FastExpressionCompiler is not supported in trimming scenarios.")] + private List SiblingListLE( + Idx firstIdx, ref SmallMap16 paramMap) + { + var list = new List(); + for (var cur = firstIdx; !cur.IsNil; cur = NodeAt(cur).NextIdx) + list.Add(ToLightExpression(cur, ref paramMap)); + return list; + } +#endif + + // 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._data != nb._data) return false; + if (na.Type != nb.Type) return false; + if (!ObjEqual(na.Obj, nb.Obj)) 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 ObjEqual(object objA, object objB) + { + // Both inline-const markers are the same singleton. + if (ReferenceEquals(objA, InplaceConstValueMarker) && ReferenceEquals(objB, InplaceConstValueMarker)) + return true; + // Lambda Obj is Idx[] — Equals() on arrays checks reference equality, not contents. + if (objA is Idx[] ia && objB 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(objA, objB); + } + + /// 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.IsInplaceConst ? $"inline:{FromInt64Bits(n.Data, n.Type)}" : + n.ChildIdx.It > 0 ? $"closure[{n.ChildIdx.It - 1}]" : + $"obj:{n.Obj}") + : null; + sb.AppendLine( + $" [{i + 1}] {n.NodeType,-22} type={n.Type?.Name,-14} " + + $"{(constStr != null ? $"val={constStr,-28}" : $"obj={ObjStr(n.Obj),-28}")} " + + $"child={n.ChildIdx} next={n.NextIdx} extra={n.ExtraIdx}"); + } + 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 ObjStr(object obj) => + obj == null || ReferenceEquals(obj, InplaceConstValueMarker) ? "—" : + obj is MethodBase mb ? mb.Name : + obj is Idx[] idxArr ? $"params[{string.Join(",", Enumerable.Select(idxArr, x => x.It))}]" : + obj.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..ff158814 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/FlatExpressionTests.cs @@ -0,0 +1,342 @@ +// 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() + { + ExpressionNode_is_24_bytes(); + 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 23; + } + + public void ExpressionNode_is_24_bytes() + { +#if !NET472 + Asserts.AreEqual(24, System.Runtime.CompilerServices.Unsafe.SizeOf()); +#endif + } + + 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.IsTrue(ReferenceEquals(node.Obj, ExpressionTree.InplaceConstValueMarker)); + Asserts.IsTrue(node.IsInplaceConst); // Obj == InplaceConstValueMarker + Asserts.AreEqual(42L, node.Data); // full 8-byte inline value + } + + 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((short)1, node.ChildIdx.It); // 1-based closure slot in ChildIdx + 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.Obj); + } + + 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 Obj — not chained via NextIdx (see Lambda factory) + var parms = (Idx[])lambda.Obj; + 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.Obj; + Asserts.AreEqual(2, parms.Length); + Asserts.AreEqual(px, parms[0]); + Asserts.AreEqual(py, parms[1]); + } + + 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.Obj); + + // Args chained via NextIdx: ChildIdx = arg1, arg1.NextIdx = arg2. + Asserts.AreEqual(arg1, newNode.ChildIdx); + Asserts.AreEqual(arg2, tree.NodeAt(arg1).NextIdx); + } + + 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.Obj); + } + + 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)); + // ifTrue and ifFalse must be allocated consecutively (ifFalse.It == ifTrue.It + 1). + var xCopy = tree.Parameter(typeof(int), "x_copy"); // ifTrue + var neg = tree.Negate(x, typeof(int)); // ifFalse (right after xCopy) + 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 implicit at ExtraIdx+1 + Asserts.AreEqual(neg, Idx.Of(condNode.ExtraIdx.It + 1)); + } + + 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)); + // Block internally allocates 2 sub-nodes: BlockExprList + BlockVarList. + var blockIdx = tree.Block(typeof(int), exprs: [assign], variables: [v]); + + ref var blockNode = ref tree.NodeAt(blockIdx); + Asserts.AreEqual(ExpressionType.Block, blockNode.NodeType); + + // Block.ChildIdx -> BlockVarList node + ref var blockVarsNode = ref tree.NodeAt(blockNode.ChildIdx); + // BlockVarList.ChildIdx -> first var (v) + Asserts.AreEqual(v, blockVarsNode.ChildIdx); + + // BlockVarList.NextIdx -> BlockExprList node + ref var blockExprsNode = ref tree.NodeAt(blockVarsNode.NextIdx); + // BlockExprList.ChildIdx -> first expr (assign) + Asserts.AreEqual(assign, blockExprsNode.ChildIdx); + } + + 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