From c0eb736f927831c7614d140eff1beb9612c634cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:05:02 +0000 Subject: [PATCH 1/3] Initial plan From 4d79f4f121c2bfc8a258752a0bdc2255a9dbef82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:20:21 +0000 Subject: [PATCH 2/3] Add FlatExpression.cs with ExprNode/ExprTree structs and FlatExpressionTests Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/106c0b3a-7cd7-4eb7-a391-c8ac412e1222 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FlatExpression.cs | 1017 +++++++++++++++++ .../FlatExpressionTests.cs | 252 ++++ .../Program.cs | 1 + 3 files changed, 1270 insertions(+) create mode 100644 src/FastExpressionCompiler.LightExpression/FlatExpression.cs create mode 100644 test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs new file mode 100644 index 00000000..cb2d7b91 --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -0,0 +1,1017 @@ +#nullable disable +#pragma warning disable CS1591 + +namespace FastExpressionCompiler.LightExpression; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using FastExpressionCompiler.LightExpression.ImTools; + +using LE = FastExpressionCompiler.LightExpression; + +/// +/// A single node in the flat expression tree. Packs all node metadata into a 64-bit +/// _data field and two object references (Type, Obj). +/// +/// _data layout (high-to-low bits): +/// +/// bits 63–56: (8 bits) +/// bits 55–48: Extra – node-type-specific auxiliary byte (8 bits) +/// bits 47–32: unused / reserved (16 bits) +/// bits 31–16: ChildCount – number of children (16 bits) +/// bits 15– 0: FirstChildPoolIdx – offset into child pool (16 bits) +/// +/// +/// +[StructLayout(LayoutKind.Sequential)] +public struct ExprNode +{ + /// Sentinel value meaning "no index". + public const ushort NoIdx = 0xFFFF; + + /// The CLR type of the expression result. + public Type Type; + + /// + /// Node-type-specific payload: + /// + /// Constant – the constant value (boxed) + /// Parameter – the parameter name (string, may be null) + /// New – (null for struct default-ctor) + /// Call – + /// Field – + /// Property (MemberAccess) – + /// Index – (null for array element access) + /// Binary – or null + /// Unary – or null + /// Conditional – explicit result or null (derive from IfTrue) + /// Loop – LabelTarget[] {breakLabel, continueLabel}, either may be null + /// Try – [] (may be null) + /// Label – + /// Goto – + /// Switch – object[] {[], comparison (may be null)} + /// TypeBinary – the operand to test against + /// MemberInit – [] + /// ListInit – [] + /// NewArrayInit/NewArrayBounds – null (element type is derived from Type) + /// + /// + public object Obj; + + private ulong _data; + + /// The of this node. + public ExpressionType NodeType + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ExpressionType)((_data >> 56) & 0xFF); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _data = (_data & ~(0xFFUL << 56)) | ((ulong)(byte)value << 56); + } + + /// + /// Node-type-specific auxiliary byte: + /// + /// Parameter – 1 if by-ref, 0 otherwise + /// Lambda – number of parameters (the body is the last child) + /// Block – number of variables (the rest of children are expressions) + /// Goto – value + /// Try – variant: 0=Catch, 1=Finally, 2=CatchFinally, 3=Fault + /// Call – 1 if first child is the instance, 0 if fully static + /// Index – 1 if first child is the instance (indexer or array), always 1 for this node type + /// + /// + internal byte Extra + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (byte)((_data >> 48) & 0xFF); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _data = (_data & ~(0xFFUL << 48)) | ((ulong)value << 48); + } + + /// Number of child nodes. + public ushort ChildCount + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)((_data >> 16) & 0xFFFF); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _data = (_data & ~(0xFFFFUL << 16)) | ((ulong)value << 16); + } + + /// + /// Offset into where the children of this node start. + /// means no children. + /// + public ushort FirstChildPoolIdx + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (ushort)(_data & 0xFFFF); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _data = (_data & ~0xFFFFUL) | (ulong)value; + } + + internal void SetData(ExpressionType nodeType, byte extra, ushort childCount, ushort firstChildPoolIdx) + { + _data = ((ulong)(byte)nodeType << 56) | + ((ulong)extra << 48) | + ((ulong)childCount << 16) | + (ulong)firstChildPoolIdx; + } +} + +/// +/// Data-oriented, flat representation of an expression tree. +/// +/// Small trees (up to 16 nodes, 16 child-pool slots) stay fully on the stack. +/// Nodes are appended to ; child indices are appended to +/// using a non-intrusive approach that allows the same node to appear as a child of multiple parents. +/// +/// +/// Factory methods return a node index. +/// Call to convert back to a LightExpression for compilation. +/// +/// +public struct ExprTree +{ + /// All expression nodes; first 16 slots live on the stack. + public SmallList, NoArrayPool> Nodes; + + /// + /// Non-intrusive child-index pool. Each parent node holds a contiguous slice + /// [FirstChildPoolIdx .. FirstChildPoolIdx + ChildCount - 1]. + /// First 16 slots live on the stack. + /// + public SmallList, NoArrayPool> ChildPool; + + // ── private helpers ─────────────────────────────────────────────────────── + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort AddNode(ExpressionType nodeType, Type type, object obj, + byte extra, ushort childCount, ushort firstChildPoolIdx) + { + var node = new ExprNode { Type = type, Obj = obj }; + node.SetData(nodeType, extra, childCount, firstChildPoolIdx); + return (ushort)Nodes.Add(in node); + } + + /// Appends to the child pool and returns the first pool index. + private ushort AppendChildren(ushort[] children) + { + var start = (ushort)ChildPool.Count; + foreach (var c in children) + ChildPool.Add(c); + return start; + } + + private ushort AddNodeWithChildren(ExpressionType nodeType, Type type, object obj, byte extra, ushort[] children) + { + ushort poolStart = ExprNode.NoIdx; + if (children != null && children.Length > 0) + poolStart = AppendChildren(children); + return AddNode(nodeType, type, obj, extra, (ushort)(children?.Length ?? 0), poolStart); + } + + // ── Constant ────────────────────────────────────────────────────────────── + + /// Creates a Constant node. + public ushort Constant(object value, Type type = null) + { + if (value == null) + return AddNode(ExpressionType.Constant, type ?? typeof(object), null, 0, 0, ExprNode.NoIdx); + type ??= value.GetType(); + return AddNode(ExpressionType.Constant, type, value, 0, 0, ExprNode.NoIdx); + } + + /// Creates a Constant node with a strongly typed value. + public ushort Constant(T value) => + AddNode(ExpressionType.Constant, typeof(T), value, 0, 0, ExprNode.NoIdx); + + // ── Parameter / Variable ────────────────────────────────────────────────── + + /// Creates a Parameter/Variable node. + public ushort Parameter(Type type, string name = null) + { + if (type.IsByRef) + return AddNode(ExpressionType.Parameter, type.GetElementType(), name, extra: 1, 0, ExprNode.NoIdx); + return AddNode(ExpressionType.Parameter, type, name, 0, 0, ExprNode.NoIdx); + } + + /// Creates a strongly-typed Parameter/Variable node. + public ushort Parameter(string name = null) => + AddNode(ExpressionType.Parameter, typeof(T), name, 0, 0, ExprNode.NoIdx); + + /// Alias for . + public ushort Variable(Type type, string name = null) => Parameter(type, name); + + /// Alias for . + public ushort Variable(string name = null) => Parameter(name); + + // ── New ─────────────────────────────────────────────────────────────────── + + /// Creates a New node for a value type default constructor. + public ushort New(Type valueType) => + AddNode(ExpressionType.New, valueType, null, 0, 0, ExprNode.NoIdx); + + /// Creates a New node with no arguments. + public ushort New(ConstructorInfo ctor) => + AddNode(ExpressionType.New, ctor.DeclaringType, ctor, 0, 0, ExprNode.NoIdx); + + /// Creates a New node with arguments. + public ushort New(ConstructorInfo ctor, params ushort[] args) => + AddNodeWithChildren(ExpressionType.New, ctor.DeclaringType, ctor, 0, args); + + // ── Call ────────────────────────────────────────────────────────────────── + + /// Creates a static method call node. + public ushort Call(MethodInfo method, params ushort[] args) => + AddNodeWithChildren(ExpressionType.Call, method.ReturnType, method, extra: 0, args); + + /// Creates an instance method call node; is the first child. + public ushort Call(ushort instance, MethodInfo method, params ushort[] args) + { + var allChildren = new ushort[1 + (args?.Length ?? 0)]; + allChildren[0] = instance; + if (args != null) + for (var i = 0; i < args.Length; i++) + allChildren[i + 1] = args[i]; + return AddNodeWithChildren(ExpressionType.Call, method.ReturnType, method, extra: 1, allChildren); + } + + // ── Field ───────────────────────────────────────────────────────────────── + + /// Creates a static field access node. + public ushort Field(FieldInfo field) => + AddNode(ExpressionType.MemberAccess, field.FieldType, field, 0, 0, ExprNode.NoIdx); + + /// Creates an instance field access node. + public ushort Field(ushort instance, FieldInfo field) => + AddNodeWithChildren(ExpressionType.MemberAccess, field.FieldType, field, extra: 1, new[] { instance }); + + // ── Property ───────────────────────────────────────────────────────────── + + /// Creates a static property access node. + public ushort Property(PropertyInfo property) => + AddNode(ExpressionType.MemberAccess, property.PropertyType, property, 0, 0, ExprNode.NoIdx); + + /// Creates an instance property access node. + public ushort Property(ushort instance, PropertyInfo property) => + AddNodeWithChildren(ExpressionType.MemberAccess, property.PropertyType, property, extra: 1, new[] { instance }); + + // ── Index (array access / indexer) ──────────────────────────────────────── + + /// Creates an array element access node. + public ushort ArrayAccess(ushort array, params ushort[] indexes) + { + var elemType = Nodes.GetSurePresentRef(array).Type.GetElementType(); + var all = new ushort[1 + indexes.Length]; + all[0] = array; + for (var i = 0; i < indexes.Length; i++) all[i + 1] = indexes[i]; + return AddNodeWithChildren(ExpressionType.Index, elemType, null, extra: 0, all); + } + + /// Creates a property indexer node. + public ushort Index(ushort instance, PropertyInfo indexer, params ushort[] args) + { + var all = new ushort[1 + args.Length]; + all[0] = instance; + for (var i = 0; i < args.Length; i++) all[i + 1] = args[i]; + return AddNodeWithChildren(ExpressionType.Index, indexer.PropertyType, indexer, extra: 0, all); + } + + // ── Binary ──────────────────────────────────────────────────────────────── + + /// Creates a binary expression node. is inferred from left if null. + public ushort Binary(ExpressionType op, ushort left, ushort right, Type type = null, MethodInfo method = null) + { + type ??= Nodes.GetSurePresentRef(left).Type; + return AddNodeWithChildren(op, type, method, 0, new[] { left, right }); + } + + /// Creates an Assign node. + public ushort Assign(ushort left, ushort right) => + Binary(ExpressionType.Assign, left, right); + + /// Creates an Add node. + public ushort Add(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.Add, left, right, null, method); + + /// Creates a Subtract node. + public ushort Subtract(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.Subtract, left, right, null, method); + + /// Creates a Multiply node. + public ushort Multiply(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.Multiply, left, right, null, method); + + /// Creates a Divide node. + public ushort Divide(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.Divide, left, right, null, method); + + /// Creates a Modulo node. + public ushort Modulo(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.Modulo, left, right, null, method); + + /// Creates an Equal node. + public ushort Equal(ushort left, ushort right) => + Binary(ExpressionType.Equal, left, right, typeof(bool)); + + /// Creates a NotEqual node. + public ushort NotEqual(ushort left, ushort right) => + Binary(ExpressionType.NotEqual, left, right, typeof(bool)); + + /// Creates a LessThan node. + public ushort LessThan(ushort left, ushort right) => + Binary(ExpressionType.LessThan, left, right, typeof(bool)); + + /// Creates a LessThanOrEqual node. + public ushort LessThanOrEqual(ushort left, ushort right) => + Binary(ExpressionType.LessThanOrEqual, left, right, typeof(bool)); + + /// Creates a GreaterThan node. + public ushort GreaterThan(ushort left, ushort right) => + Binary(ExpressionType.GreaterThan, left, right, typeof(bool)); + + /// Creates a GreaterThanOrEqual node. + public ushort GreaterThanOrEqual(ushort left, ushort right) => + Binary(ExpressionType.GreaterThanOrEqual, left, right, typeof(bool)); + + /// Creates an AndAlso (short-circuit AND) node. + public ushort AndAlso(ushort left, ushort right) => + Binary(ExpressionType.AndAlso, left, right, typeof(bool)); + + /// Creates an OrElse (short-circuit OR) node. + public ushort OrElse(ushort left, ushort right) => + Binary(ExpressionType.OrElse, left, right, typeof(bool)); + + /// Creates an And (bitwise) node. + public ushort And(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.And, left, right, null, method); + + /// Creates an Or (bitwise) node. + public ushort Or(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.Or, left, right, null, method); + + /// Creates an ExclusiveOr node. + public ushort ExclusiveOr(ushort left, ushort right, MethodInfo method = null) => + Binary(ExpressionType.ExclusiveOr, left, right, null, method); + + /// Creates a Coalesce node. + public ushort Coalesce(ushort left, ushort right) + { + var leftType = Nodes.GetSurePresentRef(left).Type; + var rightType = Nodes.GetSurePresentRef(right).Type; + var resultType = leftType.IsGenericType && leftType.GetGenericTypeDefinition() == typeof(Nullable<>) + ? leftType.GetGenericArguments()[0] + : leftType; + if (!resultType.IsAssignableFrom(rightType)) + resultType = rightType; + return Binary(ExpressionType.Coalesce, left, right, resultType); + } + + /// Creates an ArrayIndex node. + public ushort ArrayIndex(ushort array, ushort index) + { + var elemType = Nodes.GetSurePresentRef(array).Type.GetElementType(); + return Binary(ExpressionType.ArrayIndex, array, index, elemType); + } + + // ── Unary ───────────────────────────────────────────────────────────────── + + /// Creates a unary expression node. + public ushort Unary(ExpressionType op, ushort operand, Type type = null, MethodInfo method = null) + { + type ??= Nodes.GetSurePresentRef(operand).Type; + return AddNodeWithChildren(op, type, method, 0, new[] { operand }); + } + + /// Creates a Convert node. + public ushort Convert(ushort operand, Type type, MethodInfo method = null) => + Unary(ExpressionType.Convert, operand, type, method); + + /// Creates a Not node. + public ushort Not(ushort operand) => Unary(ExpressionType.Not, operand); + + /// Creates a Negate node. + public ushort Negate(ushort operand) => Unary(ExpressionType.Negate, operand); + + /// Creates an ArrayLength node. + public ushort ArrayLength(ushort array) => Unary(ExpressionType.ArrayLength, array, typeof(int)); + + /// Creates a TypeAs node. + public ushort TypeAs(ushort operand, Type type) => Unary(ExpressionType.TypeAs, operand, type); + + /// Creates a Throw node. + public ushort Throw(ushort operand, Type type = null) => + Unary(ExpressionType.Throw, operand, type ?? typeof(void)); + + /// Creates an Unbox node. + public ushort Unbox(ushort operand, Type type) => Unary(ExpressionType.Unbox, operand, type); + + /// Creates a Quote node. + public ushort Quote(ushort operand) => Unary(ExpressionType.Quote, operand); + + // ── Lambda ──────────────────────────────────────────────────────────────── + + /// + /// Creates a Lambda node. Extra = paramCount. children = params[0..n-1] + body. + /// + public ushort Lambda(Type delegateType, ushort body, params ushort[] parameters) + { + var paramCount = parameters?.Length ?? 0; + var all = new ushort[paramCount + 1]; + if (parameters != null) + for (var i = 0; i < paramCount; i++) all[i] = parameters[i]; + all[paramCount] = body; + return AddNodeWithChildren(ExpressionType.Lambda, delegateType, null, (byte)paramCount, all); + } + + /// Creates a strongly-typed Lambda node. + public ushort Lambda(ushort body, params ushort[] parameters) + where TDelegate : System.Delegate => + Lambda(typeof(TDelegate), body, parameters); + + // ── Block ───────────────────────────────────────────────────────────────── + + /// + /// Creates a Block node. Extra = varCount. children = vars[0..m-1] + exprs[0..k-1]. + /// + public ushort Block(ushort[] variables, ushort[] expressions, Type type = null) + { + var varCount = variables?.Length ?? 0; + var exprCount = expressions?.Length ?? 0; + var all = new ushort[varCount + exprCount]; + if (variables != null) + for (var i = 0; i < varCount; i++) all[i] = variables[i]; + if (expressions != null) + for (var i = 0; i < exprCount; i++) all[varCount + i] = expressions[i]; + + if (exprCount == 0) + type ??= typeof(void); + else + type ??= Nodes.GetSurePresentRef(expressions[exprCount - 1]).Type; + return AddNodeWithChildren(ExpressionType.Block, type, null, (byte)varCount, all); + } + + /// Creates a Block node with no variables. + public ushort Block(params ushort[] expressions) => Block(null, expressions); + + /// Creates a Block node with no variables and an explicit result type. + public ushort Block(Type type, params ushort[] expressions) => Block(null, expressions, type); + + // ── Conditional ─────────────────────────────────────────────────────────── + + /// Creates a Conditional (if-then-else) node. + public ushort Condition(ushort test, ushort ifTrue, ushort ifFalse, Type type = null) + { + type ??= Nodes.GetSurePresentRef(ifTrue).Type; + return AddNodeWithChildren(ExpressionType.Conditional, type, null, 0, new[] { test, ifTrue, ifFalse }); + } + + /// Creates an IfThen (void conditional) node. + public ushort IfThen(ushort test, ushort ifTrue) => + AddNodeWithChildren(ExpressionType.Conditional, typeof(void), null, 0, new[] { test, ifTrue }); + + /// Creates an IfThenElse node (type derived from ifTrue). + public ushort IfThenElse(ushort test, ushort ifTrue, ushort ifFalse) => + Condition(test, ifTrue, ifFalse); + + // ── Loop ────────────────────────────────────────────────────────────────── + + /// Creates a Loop node. + public ushort Loop(ushort body, LabelTarget breakLabel = null, LabelTarget continueLabel = null) + { + var loopType = breakLabel?.Type ?? typeof(void); + return AddNodeWithChildren(ExpressionType.Loop, loopType, + new LabelTarget[] { breakLabel, continueLabel }, 0, new[] { body }); + } + + // ── Try ─────────────────────────────────────────────────────────────────── + + // Extra: 0=Catch, 1=Finally, 2=CatchFinally, 3=Fault + + /// Creates a TryCatch node. + public ushort TryCatch(ushort body, params CatchBlock[] handlers) + { + var bodyType = Nodes.GetSurePresentRef(body).Type; + return AddNodeWithChildren(ExpressionType.Try, bodyType, handlers, extra: 0, new[] { body }); + } + + /// Creates a TryFinally node. + public ushort TryFinally(ushort body, ushort @finally) + { + var bodyType = Nodes.GetSurePresentRef(body).Type; + return AddNodeWithChildren(ExpressionType.Try, bodyType, null, extra: 1, new[] { body, @finally }); + } + + /// Creates a TryCatchFinally node. + public ushort TryCatchFinally(ushort body, ushort @finally, params CatchBlock[] handlers) + { + var bodyType = Nodes.GetSurePresentRef(body).Type; + return AddNodeWithChildren(ExpressionType.Try, bodyType, handlers, extra: 2, new[] { body, @finally }); + } + + /// Creates a TryFault node. + public ushort TryFault(ushort body, ushort fault) + { + var bodyType = Nodes.GetSurePresentRef(body).Type; + return AddNodeWithChildren(ExpressionType.Try, bodyType, null, extra: 3, new[] { body, fault }); + } + + // ── Label / Goto ────────────────────────────────────────────────────────── + + /// Creates a Label node. + public ushort Label(LabelTarget target) => + AddNode(ExpressionType.Label, target.Type, target, 0, 0, ExprNode.NoIdx); + + /// Creates a Label node with a default value expression. + public ushort Label(LabelTarget target, ushort defaultValue) => + AddNodeWithChildren(ExpressionType.Label, target.Type, target, 0, new[] { defaultValue }); + + // Goto Extra: 0=Goto, 1=Return, 2=Break, 3=Continue + private ushort MakeGoto(GotoExpressionKind kind, LabelTarget target, ushort valueIdx, Type type) + { + type ??= typeof(void); + if (valueIdx != ExprNode.NoIdx) + return AddNodeWithChildren(ExpressionType.Goto, type, target, (byte)kind, new[] { valueIdx }); + return AddNode(ExpressionType.Goto, type, target, (byte)kind, 0, ExprNode.NoIdx); + } + + /// Creates a Goto node. + public ushort Goto(LabelTarget target, Type type = null) => + MakeGoto(GotoExpressionKind.Goto, target, ExprNode.NoIdx, type); + + /// Creates a Goto node with a value. + public ushort Goto(LabelTarget target, ushort value, Type type = null) => + MakeGoto(GotoExpressionKind.Goto, target, value, type ?? Nodes.GetSurePresentRef(value).Type); + + /// Creates a Return node. + public ushort Return(LabelTarget target, Type type = null) => + MakeGoto(GotoExpressionKind.Return, target, ExprNode.NoIdx, type); + + /// Creates a Return node with a value. + public ushort Return(LabelTarget target, ushort value, Type type = null) => + MakeGoto(GotoExpressionKind.Return, target, value, type ?? Nodes.GetSurePresentRef(value).Type); + + /// Creates a Break node. + public ushort Break(LabelTarget target, Type type = null) => + MakeGoto(GotoExpressionKind.Break, target, ExprNode.NoIdx, type); + + /// Creates a Break node with a value. + public ushort Break(LabelTarget target, ushort value, Type type = null) => + MakeGoto(GotoExpressionKind.Break, target, value, type ?? Nodes.GetSurePresentRef(value).Type); + + /// Creates a Continue node. + public ushort Continue(LabelTarget target, Type type = null) => + MakeGoto(GotoExpressionKind.Continue, target, ExprNode.NoIdx, type); + + // ── Switch ──────────────────────────────────────────────────────────────── + + /// Creates a Switch node. + public ushort Switch(ushort switchValue, SwitchCase[] cases, ushort defaultBody = ExprNode.NoIdx, + Type type = null, MethodInfo comparison = null) + { + type ??= cases?.Length > 0 ? cases[0].Body.Type : + defaultBody != ExprNode.NoIdx ? Nodes.GetSurePresentRef(defaultBody).Type : typeof(void); + + var children = defaultBody != ExprNode.NoIdx + ? new[] { switchValue, defaultBody } + : new[] { switchValue }; + + return AddNodeWithChildren(ExpressionType.Switch, type, + new object[] { cases ?? Array.Empty(), comparison }, 0, children); + } + + // ── TypeBinary ──────────────────────────────────────────────────────────── + + /// Creates a TypeIs node. + public ushort TypeIs(ushort operand, Type type) => + AddNodeWithChildren(ExpressionType.TypeIs, typeof(bool), type, 0, new[] { operand }); + + /// Creates a TypeEqual node. + public ushort TypeEqual(ushort operand, Type type) => + AddNodeWithChildren(ExpressionType.TypeEqual, typeof(bool), type, 0, new[] { operand }); + + // ── Default ─────────────────────────────────────────────────────────────── + + /// Creates a Default node. + public ushort Default(Type type) => + AddNode(ExpressionType.Default, type, null, 0, 0, ExprNode.NoIdx); + + // ── Invoke ──────────────────────────────────────────────────────────────── + + /// + /// Creates an Invocation node. The first child is the expression to invoke, + /// followed by the arguments. + /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2080", + Justification = "The Invoke method exists on all delegate types")] + public ushort Invoke(ushort expression, params ushort[] args) + { + var exprType = Nodes.GetSurePresentRef(expression).Type; + Type returnType; + if (exprType.IsSubclassOf(typeof(System.Delegate)) || exprType == typeof(System.Delegate)) + { + var invokeMethod = exprType.GetMethod("Invoke"); + returnType = invokeMethod?.ReturnType ?? typeof(object); + } + else + returnType = typeof(object); + + var all = new ushort[1 + (args?.Length ?? 0)]; + all[0] = expression; + if (args != null) + for (var i = 0; i < args.Length; i++) all[i + 1] = args[i]; + return AddNodeWithChildren(ExpressionType.Invoke, returnType, null, 0, all); + } + + // ── NewArray ────────────────────────────────────────────────────────────── + + /// Creates a NewArrayInit node (array with initializers). + public ushort NewArrayInit(Type elementType, params ushort[] elements) => + AddNodeWithChildren(ExpressionType.NewArrayInit, elementType.MakeArrayType(), null, 0, elements); + + /// Creates a NewArrayBounds node (multi-dim array with bounds). + public ushort NewArrayBounds(Type elementType, params ushort[] bounds) => + AddNodeWithChildren(ExpressionType.NewArrayBounds, elementType.MakeArrayType(), null, 0, bounds); + + // ── MemberInit ──────────────────────────────────────────────────────────── + + /// + /// Creates a MemberInit node. The single child is the New expression; + /// bindings are stored in . + /// + public ushort MemberInit(ushort newExpr, params MemberBinding[] bindings) + { + var type = Nodes.GetSurePresentRef(newExpr).Type; + return AddNodeWithChildren(ExpressionType.MemberInit, type, bindings, 0, new[] { newExpr }); + } + + // ── ListInit ────────────────────────────────────────────────────────────── + + /// + /// Creates a ListInit node. The single child is the New expression; + /// initializers are stored in . + /// + public ushort ListInit(ushort newExpr, params ElementInit[] initializers) + { + var type = Nodes.GetSurePresentRef(newExpr).Type; + return AddNodeWithChildren(ExpressionType.ListInit, type, initializers, 0, new[] { newExpr }); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Conversion to LightExpression + // ══════════════════════════════════════════════════════════════════════════ + + /// + /// Converts the node at to a . + /// The returned expression can be compiled with FastExpressionCompiler. + /// + [RequiresUnreferencedCode(Trimming.Message)] + public LE.Expression ToLightExpression(ushort rootIdx) + { + var paramCache = new LE.ParameterExpression[Nodes.Count]; + return ConvertNode(rootIdx, paramCache); + } + + /// + /// Converts the node at to a . + /// Throws if the node is not a Lambda. + /// + [RequiresUnreferencedCode(Trimming.Message)] + public LE.LambdaExpression ToLambdaExpression(ushort rootIdx) + { + if (Nodes.GetSurePresentRef(rootIdx).NodeType != ExpressionType.Lambda) + throw new ArgumentException("The node at rootIdx is not a Lambda node.", nameof(rootIdx)); + return (LE.LambdaExpression)ToLightExpression(rootIdx); + } + + /// + /// Converts the node at to a strongly typed + /// . + /// + [RequiresUnreferencedCode(Trimming.Message)] + public LE.Expression ToExpression(ushort rootIdx) + where TDelegate : System.Delegate => + (LE.Expression)ToLightExpression(rootIdx); + + // ── private conversion helpers ──────────────────────────────────────────── + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertNode(ushort idx, LE.ParameterExpression[] paramCache) + { + ref var node = ref Nodes.GetSurePresentRef(idx); + switch (node.NodeType) + { + case ExpressionType.Constant: + return LE.Expression.Constant(node.Obj, node.Type); + + case ExpressionType.Parameter: + { + var cached = paramCache[idx]; + if (cached != null) return cached; + var param = node.Extra == 1 + ? LE.Expression.Parameter(node.Type.MakeByRefType(), (string)node.Obj) + : LE.Expression.Parameter(node.Type, (string)node.Obj); + return paramCache[idx] = param; + } + + case ExpressionType.New: + return ConvertNew(ref node, idx, paramCache); + + case ExpressionType.Call: + return ConvertCall(ref node, idx, paramCache); + + case ExpressionType.MemberAccess: + return ConvertMemberAccess(ref node, idx, paramCache); + + case ExpressionType.Index: + return ConvertIndex(ref node, idx, paramCache); + + case ExpressionType.Lambda: + return ConvertLambda(ref node, idx, paramCache); + + case ExpressionType.Block: + return ConvertBlock(ref node, idx, paramCache); + + case ExpressionType.Conditional: + return ConvertConditional(ref node, idx, paramCache); + + case ExpressionType.Loop: + return ConvertLoop(ref node, idx, paramCache); + + case ExpressionType.Try: + return ConvertTry(ref node, idx, paramCache); + + case ExpressionType.Label: + return ConvertLabel(ref node, idx, paramCache); + + case ExpressionType.Goto: + return ConvertGoto(ref node, idx, paramCache); + + case ExpressionType.Switch: + return ConvertSwitch(ref node, idx, paramCache); + + case ExpressionType.TypeIs: + return LE.Expression.TypeIs(ConvertChild(idx, 0, paramCache), (Type)node.Obj); + + case ExpressionType.TypeEqual: + return LE.Expression.TypeEqual(ConvertChild(idx, 0, paramCache), (Type)node.Obj); + + case ExpressionType.Default: + return LE.Expression.Default(node.Type); + + case ExpressionType.Invoke: + return ConvertInvoke(ref node, idx, paramCache); + + case ExpressionType.NewArrayInit: + return LE.Expression.NewArrayInit(node.Type.GetElementType(), ConvertChildren(idx, paramCache)); + + case ExpressionType.NewArrayBounds: + return LE.Expression.NewArrayBounds(node.Type.GetElementType(), ConvertChildren(idx, paramCache)); + + case ExpressionType.MemberInit: + return LE.Expression.MemberInit( + (LE.NewExpression)ConvertChild(idx, 0, paramCache), + (MemberBinding[])node.Obj); + + case ExpressionType.ListInit: + return LE.Expression.ListInit( + (LE.NewExpression)ConvertChild(idx, 0, paramCache), + (ElementInit[])node.Obj); + + default: + return ConvertBinaryOrUnary(ref node, idx, paramCache); + } + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertNew(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + if (node.Obj == null) + return LE.Expression.New(node.Type); + var ctor = (ConstructorInfo)node.Obj; + if (node.ChildCount == 0) + return LE.Expression.New(ctor); + return LE.Expression.New(ctor, ConvertChildren(idx, paramCache)); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertCall(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var method = (MethodInfo)node.Obj; + if (node.Extra == 1) // has instance + { + var instance = ConvertChild(idx, 0, paramCache); + var args = ConvertChildrenFrom(idx, 1, paramCache); + return LE.Expression.Call(instance, method, args); + } + return LE.Expression.Call(method, ConvertChildren(idx, paramCache)); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertMemberAccess(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + if (node.Obj is FieldInfo field) + return node.Extra == 1 + ? LE.Expression.Field(ConvertChild(idx, 0, paramCache), field) + : LE.Expression.Field(null, field); + var prop = (PropertyInfo)node.Obj; + return node.Extra == 1 + ? LE.Expression.Property(ConvertChild(idx, 0, paramCache), prop) + : LE.Expression.Property(null, prop); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertIndex(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var obj = ConvertChild(idx, 0, paramCache); + var args = ConvertChildrenFrom(idx, 1, paramCache); + if (node.Obj is PropertyInfo indexer) + return LE.Expression.Property(obj, indexer, args); + return LE.Expression.ArrayAccess(obj, args); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertLambda(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var paramCount = node.Extra; + var parameters = new LE.ParameterExpression[paramCount]; + for (var i = 0; i < paramCount; i++) + { + var paramIdx = GetChildIdx(idx, i); + // ensure the parameter is in the cache before converting the body + if (paramCache[paramIdx] == null) + ConvertNode(paramIdx, paramCache); + parameters[i] = paramCache[paramIdx]; + } + var body = ConvertChild(idx, paramCount, paramCache); + return LE.Expression.Lambda(node.Type, body, parameters); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertBlock(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var varCount = node.Extra; + var exprCount = node.ChildCount - varCount; + + var variables = new LE.ParameterExpression[varCount]; + for (var i = 0; i < varCount; i++) + { + var varIdx = GetChildIdx(idx, i); + if (paramCache[varIdx] == null) + ConvertNode(varIdx, paramCache); + variables[i] = paramCache[varIdx]; + } + + var exprs = new LE.Expression[exprCount]; + for (var i = 0; i < exprCount; i++) + exprs[i] = ConvertChild(idx, varCount + i, paramCache); + + if (varCount == 0) + return LE.Expression.Block(node.Type, exprs); + return LE.Expression.Block(node.Type, variables, exprs); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertConditional(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var test = ConvertChild(idx, 0, paramCache); + var ifTrue = ConvertChild(idx, 1, paramCache); + if (node.ChildCount == 2) + return LE.Expression.IfThen(test, ifTrue); + var ifFalse = ConvertChild(idx, 2, paramCache); + return LE.Expression.Condition(test, ifTrue, ifFalse, node.Type); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertLoop(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var body = ConvertChild(idx, 0, paramCache); + var labels = (LabelTarget[])node.Obj; + var breakLabel = labels?[0]; + var continueLabel = labels?[1]; + return LE.Expression.Loop(body, breakLabel, continueLabel); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertTry(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var body = ConvertChild(idx, 0, paramCache); + var handlers = (CatchBlock[])node.Obj; + return node.Extra switch + { + 0 => LE.Expression.TryCatch(body, handlers ?? Array.Empty()), + 1 => LE.Expression.TryFinally(body, ConvertChild(idx, 1, paramCache)), + 2 => LE.Expression.TryCatchFinally(body, ConvertChild(idx, 1, paramCache), + handlers ?? Array.Empty()), + 3 => LE.Expression.TryFault(body, ConvertChild(idx, 1, paramCache)), + _ => throw new InvalidOperationException($"Unknown Try variant {node.Extra}") + }; + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertLabel(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var target = (LabelTarget)node.Obj; + if (node.ChildCount == 0) + return LE.Expression.Label(target); + return LE.Expression.Label(target, ConvertChild(idx, 0, paramCache)); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertGoto(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var target = (LabelTarget)node.Obj; + var kind = (GotoExpressionKind)node.Extra; + LE.Expression value = node.ChildCount > 0 ? ConvertChild(idx, 0, paramCache) : null; + return LE.Expression.MakeGoto(kind, target, value, node.Type); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertSwitch(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var payload = (object[])node.Obj; + var cases = (SwitchCase[])payload[0]; + var comparison = (MethodInfo)payload[1]; + var switchValue = ConvertChild(idx, 0, paramCache); + LE.Expression defaultBody = node.ChildCount > 1 ? ConvertChild(idx, 1, paramCache) : null; + return LE.Expression.Switch(node.Type, switchValue, defaultBody, comparison, cases); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertInvoke(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + var expr = ConvertChild(idx, 0, paramCache); + var args = ConvertChildrenFrom(idx, 1, paramCache); + return LE.Expression.Invoke(expr, args); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertBinaryOrUnary(ref ExprNode node, ushort idx, LE.ParameterExpression[] paramCache) + { + if (node.ChildCount == 2) + { + var left = ConvertChild(idx, 0, paramCache); + var right = ConvertChild(idx, 1, paramCache); + var method = node.Obj as MethodInfo; + if (node.NodeType == ExpressionType.Coalesce) + return LE.Expression.Coalesce(left, right); + if (node.NodeType == ExpressionType.ArrayIndex) + return LE.Expression.ArrayIndex(left, right); + return LE.Expression.MakeBinary(node.NodeType, left, right, + liftToNull: false, method, conversion: null); + } + if (node.ChildCount == 1) + { + var operand = ConvertChild(idx, 0, paramCache); + var method = node.Obj as MethodInfo; + return LE.Expression.MakeUnary(node.NodeType, operand, node.Type, method); + } + throw new NotSupportedException($"Cannot convert ExprNode with NodeType={node.NodeType} and ChildCount={node.ChildCount}"); + } + + // ── child access helpers ────────────────────────────────────────────────── + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort GetChildIdx(ushort parentIdx, int childOffset) + { + ref var parent = ref Nodes.GetSurePresentRef(parentIdx); + return ChildPool.GetSurePresentRef(parent.FirstChildPoolIdx + childOffset); + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression ConvertChild(ushort parentIdx, int childOffset, LE.ParameterExpression[] paramCache) => + ConvertNode(GetChildIdx(parentIdx, childOffset), paramCache); + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression[] ConvertChildren(ushort parentIdx, LE.ParameterExpression[] paramCache) + { + ref var parent = ref Nodes.GetSurePresentRef(parentIdx); + var count = parent.ChildCount; + if (count == 0) return Array.Empty(); + var result = new LE.Expression[count]; + var poolBase = parent.FirstChildPoolIdx; + for (var i = 0; i < count; i++) + result[i] = ConvertNode(ChildPool.GetSurePresentRef(poolBase + i), paramCache); + return result; + } + + [RequiresUnreferencedCode(Trimming.Message)] + private LE.Expression[] ConvertChildrenFrom(ushort parentIdx, int startOffset, LE.ParameterExpression[] paramCache) + { + ref var parent = ref Nodes.GetSurePresentRef(parentIdx); + var count = parent.ChildCount - startOffset; + if (count <= 0) return Array.Empty(); + var result = new LE.Expression[count]; + var poolBase = parent.FirstChildPoolIdx; + for (var i = 0; i < count; i++) + result[i] = ConvertNode(ChildPool.GetSurePresentRef(poolBase + startOffset + i), paramCache); + return result; + } +} diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs new file mode 100644 index 00000000..81c06ba3 --- /dev/null +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Reflection; +using System.Linq.Expressions; +using FastExpressionCompiler.LightExpression; +using LE = FastExpressionCompiler.LightExpression; + +namespace FastExpressionCompiler.LightExpression.UnitTests +{ + public class FlatExpressionTests : ITest + { + public int Run() + { + Constant_and_Lambda_roundtrip(); + Parameter_reuse_in_body(); + Binary_arithmetic(); + Block_with_variable_and_assign(); + Conditional_ifThen_and_ifThenElse(); + New_and_Call(); + Field_and_Property_access(); + Unary_convert_and_not(); + Lambda_compiled_and_invoked(); + Nested_lambda(); + Loop_with_break(); + TryCatch_roundtrip(); + ArrayAccess_and_NewArrayInit(); + TypeIs_and_TypeEqual(); + Default_expression(); + return 16; + } + + // ── helpers ─────────────────────────────────────────────────────────── + + private static ExprTree E => default; + + // ── tests ───────────────────────────────────────────────────────────── + + public void Constant_and_Lambda_roundtrip() + { + var e = default(ExprTree); + var c = e.Constant(42); + var lam = e.Lambda>(c); + + var le = e.ToLightExpression(lam); + Asserts.IsNotNull(le); + Asserts.AreEqual(ExpressionType.Lambda, le.NodeType); + + var f = ((LE.LambdaExpression)le).CompileFast>(); + Asserts.AreEqual(42, f()); + } + + public void Parameter_reuse_in_body() + { + var e = default(ExprTree); + var p = e.Parameter("x"); + var body = e.Add(p, e.Constant(10)); + var lam = e.Lambda>(body, p); + + var le = (LE.LambdaExpression)e.ToLightExpression(lam); + var f = le.CompileFast>(); + Asserts.AreEqual(15, f(5)); + Asserts.AreEqual(20, f(10)); + } + + public void Binary_arithmetic() + { + var e = default(ExprTree); + var p = e.Parameter("n"); + var body = e.Multiply(e.Add(p, e.Constant(2)), e.Constant(3)); + var lam = e.Lambda>(body, p); + + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(12, f(2)); // (2+2)*3 + Asserts.AreEqual(15, f(3)); // (3+2)*3 + } + + public void Block_with_variable_and_assign() + { + var e = default(ExprTree); + var v = e.Variable("tmp"); + var assign = e.Assign(v, e.Constant(7)); + var result = e.Add(v, e.Constant(3)); + var block = e.Block(new[] { v }, new[] { assign, result }); + var lam = e.Lambda>(block); + + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(10, f()); + } + + public void Conditional_ifThen_and_ifThenElse() + { + var e = default(ExprTree); + + // ifThenElse: x > 0 ? 1 : -1 + var p = e.Parameter("x"); + var test = e.GreaterThan(p, e.Constant(0)); + var body = e.Condition(test, e.Constant(1), e.Constant(-1)); + var lam = e.Lambda>(body, p); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(1, f(5)); + Asserts.AreEqual(-1, f(-1)); + } + + public class Foo + { + public int Value; + public int X { get; set; } + public Foo(int value) => Value = value; + public int Add(int n) => Value + n; + } + + public void New_and_Call() + { + var e = default(ExprTree); + var ctor = typeof(Foo).GetConstructor(new[] { typeof(int) }); + var method = typeof(Foo).GetMethod(nameof(Foo.Add)); + + var p = e.Parameter("n"); + var newFoo = e.New(ctor, e.Constant(10)); + var call = e.Call(newFoo, method, p); + var lam = e.Lambda>(call, p); + + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(15, f(5)); + } + + public void Field_and_Property_access() + { + var e = default(ExprTree); + var field = typeof(Foo).GetField(nameof(Foo.Value)); + var prop = typeof(Foo).GetProperty(nameof(Foo.X)); + var ctor = typeof(Foo).GetConstructor(new[] { typeof(int) }); + + // field access: new Foo(42).Value + { + var newFoo = e.New(ctor, e.Constant(42)); + var fldAccess = e.Field(newFoo, field); + var lam = e.Lambda>(fldAccess); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(42, f()); + } + } + + public void Unary_convert_and_not() + { + { + var e = default(ExprTree); + var p = e.Parameter("x"); + var conv = e.Convert(p, typeof(long)); + var lam = e.Lambda>(conv, p); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(42L, f(42)); + } + { + var e = default(ExprTree); + var p = e.Parameter("b"); + var notExpr = e.Not(p); + var lam = e.Lambda>(notExpr, p); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(false, f(true)); + Asserts.AreEqual(true, f(false)); + } + } + + public void Lambda_compiled_and_invoked() + { + var e = default(ExprTree); + var a = e.Parameter("a"); + var b = e.Parameter("b"); + var body = e.Add(a, b); + var lam = e.Lambda>(body, a, b); + + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(7, f(3, 4)); + } + + public void Nested_lambda() + { + var e = default(ExprTree); + var x = e.Parameter("x"); + var inner = e.Lambda>(e.Add(x, e.Constant(1))); + var outer = e.Lambda>>(inner, x); + + var f = e.ToLambdaExpression(outer).CompileFast>>(); + Asserts.AreEqual(6, f(5)()); + } + + public void Loop_with_break() + { + var e = default(ExprTree); + var breakLabel = Expression.Label(typeof(int), "break"); + var v = e.Variable("i"); + var assignInit = e.Assign(v, e.Constant(0)); + var breakExpr = e.Break(breakLabel, e.Convert(v, typeof(int))); + var assignInc = e.Assign(v, e.Add(v, e.Constant(1))); + var cond = e.IfThen(e.GreaterThanOrEqual(v, e.Constant(3)), breakExpr); + var loopBody = e.Block(new[] { assignInc, cond }); + var loop = e.Loop(loopBody, breakLabel); + var block = e.Block(new[] { v }, new[] { assignInit, loop }); + var lam = e.Lambda>(block); + + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(3, f()); + } + + public void TryCatch_roundtrip() + { + var e = default(ExprTree); + var exVar = LE.Expression.Parameter(typeof(InvalidOperationException), "ex"); + var catchBlock = LE.Expression.Catch(exVar, LE.Expression.Constant(99)); + + var body = e.Constant(1); + var tryCatch = e.TryCatch(body, catchBlock); + var lam = e.Lambda>(tryCatch); + + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(1, f()); + } + + public void ArrayAccess_and_NewArrayInit() + { + var e = default(ExprTree); + + // new int[] { 10, 20, 30 }[1] + var arr = e.NewArrayInit(typeof(int), e.Constant(10), e.Constant(20), e.Constant(30)); + var elem = e.ArrayAccess(arr, e.Constant(1)); + var lam = e.Lambda>(elem); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(20, f()); + } + + public void TypeIs_and_TypeEqual() + { + var e = default(ExprTree); + var p = e.Parameter("o"); + + var typeIs = e.TypeIs(p, typeof(string)); + var lam = e.Lambda>(typeIs, p); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(true, f("hello")); + Asserts.AreEqual(false, f(42)); + } + + public void Default_expression() + { + var e = default(ExprTree); + var defInt = e.Default(typeof(int)); + var lam = e.Lambda>(defInt); + var f = e.ToLambdaExpression(lam).CompileFast>(); + Asserts.AreEqual(0, f()); + } + } +} diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index b68e26c9..73041830 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -162,6 +162,7 @@ void Run(Func run, string name = null) Run(new LightExpression.UnitTests.ValueTypeTests().Run); Run(new LightExpression.UnitTests.NestedLambdasSharedToExpressionCodeStringTest().Run); Run(new LightExpression.UnitTests.LightExpressionTests().Run); + Run(new LightExpression.UnitTests.FlatExpressionTests().Run); Run(new ToCSharpStringTests().Run); Run(new LightExpression.UnitTests.ToCSharpStringTests().Run); From b199a3c150757fb55e467f06b1b1405af264e587 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:23:43 +0000 Subject: [PATCH 3/3] Fix ArrayAccess extra flag and improve Extra doc comment Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/106c0b3a-7cd7-4eb7-a391-c8ac412e1222 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- src/FastExpressionCompiler.LightExpression/FlatExpression.cs | 3 ++- .../FlatExpressionTests.cs | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index cb2d7b91..6e44f2a4 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -84,6 +84,7 @@ public ExpressionType NodeType /// Goto – value /// Try – variant: 0=Catch, 1=Finally, 2=CatchFinally, 3=Fault /// Call – 1 if first child is the instance, 0 if fully static + /// MemberAccess – 1 if first child is the instance, 0 if static /// Index – 1 if first child is the instance (indexer or array), always 1 for this node type /// /// @@ -272,7 +273,7 @@ public ushort ArrayAccess(ushort array, params ushort[] indexes) var all = new ushort[1 + indexes.Length]; all[0] = array; for (var i = 0; i < indexes.Length; i++) all[i + 1] = indexes[i]; - return AddNodeWithChildren(ExpressionType.Index, elemType, null, extra: 0, all); + return AddNodeWithChildren(ExpressionType.Index, elemType, null, extra: 1, all); } /// Creates a property indexer node. diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs index 81c06ba3..6a5321c2 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/FlatExpressionTests.cs @@ -30,8 +30,6 @@ public int Run() // ── helpers ─────────────────────────────────────────────────────────── - private static ExprTree E => default; - // ── tests ───────────────────────────────────────────────────────────── public void Constant_and_Lambda_roundtrip()