From cdeee2caa543bd31b4861525468f09501f8ea39c Mon Sep 17 00:00:00 2001 From: dadhi Date: Fri, 30 Oct 2026 00:00:26 +0100 Subject: [PATCH 01/19] FlatExpression: drop node-collection lists and closure-usage tracking, require non-empty leading children Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FlatExpression.cs | 337 +----------------- .../LightExpressionTests.cs | 324 ----------------- 2 files changed, 19 insertions(+), 642 deletions(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index b5ca2b2b6..e73278355 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -167,32 +167,6 @@ internal bool HasSameShape(ref ExprNode other) => _child == other._child; } -/// Maps a lambda node to a captured outer parameter or variable. -/// Uses the same 16-bit storage range as the flat node links and identities. -[StructLayout(LayoutKind.Explicit, Size = 6)] -public struct LambdaClosureParameterUsage -{ - /// The lambda node idx in the flat-expression node array. - [FieldOffset(0)] - public ushort LambdaIdx; - - /// The parameter-usage expression node idx in the flat-expression node array. - [FieldOffset(2)] - public ushort ParameterIdx; - - /// The shared parameter/variable identity stored in . - [FieldOffset(4)] - public ushort ParameterId; - - /// Creates the lambda capture mapping. - public LambdaClosureParameterUsage(ushort lambdaIdx, ushort parameterIdx, ushort parameterId) - { - LambdaIdx = lambdaIdx; - ParameterIdx = parameterIdx; - ParameterId = parameterId; - } -} - /// Stores an expression tree as flat nodes plus separate closure constants. public struct ExprTree : IEquatable { @@ -214,28 +188,6 @@ public struct ExprTree : IEquatable /// Gets or sets closure constants that are referenced from constant nodes. public SmallList, NoArrayPool> ClosureConstants; - /// Gets or sets all lambda node idxs added during construction. - /// The root lambda idx is stored in ; other entries are nested lambdas. - public SmallList, NoArrayPool> LambdaNodes; - - /// Gets or sets all block node idxs that carry explicit variable declarations. - /// A tracked block uses children.Count == 2: one child list for variables and one for expressions. - public SmallList, NoArrayPool> BlocksWithVariables; - - /// Gets or sets all node idxs, - /// including return, break, and continue. - public SmallList, NoArrayPool> GotoNodes; - - /// Gets or sets all expression node idxs. - public SmallList, NoArrayPool> LabelNodes; - - /// Gets or sets all node idxs for try/catch, try/finally, try/fault, and combined forms. - public SmallList, NoArrayPool> TryCatchNodes; - - /// Gets or sets captured outer parameter and variable usages for lambdas. - /// The stored idxs use the same 16-bit range as and . - public SmallList, NoArrayPool> LambdaClosureParameterUsages; - // Import-only identity maps (valid during FromSysExpr / FromLightExpr). private SmallMap16> _parameterIds; private SmallMap16> _labelIds; @@ -456,22 +408,19 @@ private ushort WithThreeChildren(ExpressionType nodeType, Type type, object obj, private ushort WithTwoOrMoreChildren(ExpressionType nodeType, Type type, object obj, byte flags, ExprNodeKind kind, ushort ch0, ushort ch1, ushort[] more) { + Debug.Assert(ch0 != 0 && ch1 != 0, "Both leading children must be provided."); var ownerIdx = AddNode(nodeType, type, obj, flags, kind); ushort first = 0, prev = 0, count = 0; - // todo: @wip if the children id is missing it is more of an error - if (ch0 != 0) - AppendPreparedChild(MayBeCloneChildForOwner(ch0, ownerIdx), ref first, ref prev, ref count); - if (ch1 != 0) - AppendPreparedChild(MayBeCloneChildForOwner(ch1, ownerIdx), ref first, ref prev, ref count); + AppendPreparedChild(MayBeCloneChildForOwner(ch0, ownerIdx), ref first, ref prev, ref count); + AppendPreparedChild(MayBeCloneChildForOwner(ch1, ownerIdx), ref first, ref prev, ref count); if (more != null) { for (var i = 0; i < more.Length; ++i) { - var ch = more[i]; - if (ch == 0) continue; - AppendPreparedChild(MayBeCloneChildForOwner(ch, ownerIdx), ref first, ref prev, ref count); + Debug.Assert(more[i] != 0, "Child index must not be zero."); + AppendPreparedChild(MayBeCloneChildForOwner(more[i], ownerIdx), ref first, ref prev, ref count); } } @@ -662,7 +611,7 @@ public ushort Condition(ushort test, ushort ifTrue, ushort ifFalse, Type type = WithThreeChildren(ExpressionType.Conditional, type ?? Nodes[ifTrue].Type, null, default, default, test, ifTrue, ifFalse); /// Block layout: first child = BlockExprs sub-node (expression list); - /// optional following siblings = variable declarations. Tracked in when vars present. + /// optional following siblings = variable declarations. public ushort Block(Type type, ushort[] vars, params ushort[] exprs) { if (exprs == null || exprs.Length == 0) @@ -671,10 +620,7 @@ public ushort Block(Type type, ushort[] vars, params ushort[] exprs) var exprsSubNode = WithChildren(ExpressionType.Block, null, null, default, ExprNodeKind.BlockExprs, exprs); type ??= Nodes[exprs[exprs.Length - 1]].Type; - var blockIdx = WithOneOrMoreChildren(ExpressionType.Block, type, null, default, default, exprsSubNode, vars); - if (vars != null && vars.Length != 0) - BlocksWithVariables.Add(blockIdx); - return blockIdx; + return WithOneOrMoreChildren(ExpressionType.Block, type, null, default, default, exprsSubNode, vars); } /// Adds a block node without explicit variables. @@ -682,7 +628,7 @@ public ushort Block(Type type, ushort[] vars, params ushort[] exprs) public ushort Block(params ushort[] exprs) => Block(null, null, exprs); - /// Adds a lambda node. Layout: body then parameters. Tracks and captures. + /// Adds a lambda node. Layout: body then parameters. [MethodImpl(MethodImplOptions.AggressiveInlining)] #if NET10_0_OR_GREATER public ushort Lambda(Type delegateType, ushort bodyIdx, params ReadOnlySpan pars) @@ -733,8 +679,6 @@ public ushort Lambda(Type delegateType, ushort bodyIdx, params ushort[] pars) prevRef.NextIdx = lambdaIdx; // close the child chain with a link back to the lambda node - LambdaNodes.Add(lambdaIdx); - CollectLambdaClosureParameterUsages(lambdaIdx); return lambdaIdx; } @@ -791,23 +735,17 @@ private ushort LabelTargetWithId(Type type, string name, ushort id) => /// Adds a label-expression node. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ushort Label(ushort target, ushort defaultValue = 0) - { - var idx = defaultValue == 0 + public ushort Label(ushort target, ushort defaultValue = 0) => + defaultValue == 0 ? WithOneChild(ExpressionType.Label, Nodes[target].Type, null, default, default, target) : WithTwoChildren(ExpressionType.Label, Nodes[target].Type, null, default, default, target, defaultValue); - LabelNodes.Add(idx); - return idx; - } /// Adds a goto-family node. Kind is stored in flags. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort MakeGoto(GotoExpressionKind gotoKind, ushort target, ushort value = 0, Type type = null) { var resultType = type ?? (value != 0 ? Nodes[value].Type : typeof(void)); - var idx = WithTwoChildren(ExpressionType.Goto, resultType, null, (byte)gotoKind, default, target, value); - GotoNodes.Add(idx); - return idx; + return WithTwoChildren(ExpressionType.Goto, resultType, null, (byte)gotoKind, default, target, value); } /// Adds a goto node. @@ -880,36 +818,20 @@ public ushort MakeCatchBlock(Type test, ushort variable, ushort body, ushort fil } /// Adds a try/catch node. Layout: body, handlers… - public ushort TryCatch(ushort body, params ushort[] handlers) - { - var idx = WithOneOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, handlers); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryCatch(ushort body, params ushort[] handlers) => + WithOneOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, handlers); /// Adds a try/finally node. - public ushort TryFinally(ushort body, ushort @finally) - { - var idx = WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryFinally(ushort body, ushort @finally) => + WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally); /// Adds a try/fault node. - public ushort TryFault(ushort body, ushort fault) - { - var idx = WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, TryFaultFlag, default, body, fault); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryFault(ushort body, ushort fault) => + WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, TryFaultFlag, default, body, fault); /// Adds a try node with optional finally block and catch handlers. Layout: body, finally, handlers… - public ushort TryCatchFinally(ushort body, ushort @finally, params ushort[] handlers) - { - var idx = WithTwoOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally, handlers); - TryCatchNodes.Add(idx); - return idx; - } + public ushort TryCatchFinally(ushort body, ushort @finally, params ushort[] handlers) => + WithTwoOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally, handlers); /// Adds a type-test node. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1260,217 +1182,6 @@ private static int GetId(ref SmallMap16> ids, object return id; } - private void CollectLambdaClosureParameterUsages(ushort lambdaIdx) - { - var children = GetChildren(lambdaIdx); - if (children.Count == 0) - return; - - ChildIdxs lambdaParameterIds = default; - for (var i = 1; i < children.Count; ++i) - lambdaParameterIds.Add(ToStoredUShortIdx(Nodes[children[i]].ChildIdx)); - - SmallList, NoArrayPool> localParameterIds = default; - SmallList, NoArrayPool> captures = default; - CollectClosureParameterUsages(children[0], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - - for (var i = 0; i < captures.Count; ++i) - LambdaClosureParameterUsages.Add(captures[i]); - } - - private void CollectClosureParameterUsages( - ushort idx, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - if (idx == 0) - return; - - ref var node = ref Nodes.GetSurePresentRef(idx); - - // Group / metadata kinds share some ExpressionType values with real expressions. - // Always walk their children as a plain sibling list — never apply Block/Lambda layouts. - if (node.Kind == ExprNodeKind.BlockExprs || - node.Kind == ExprNodeKind.SwitchCases || - node.Kind == ExprNodeKind.SwitchCase || - node.Kind == ExprNodeKind.CatchBlock || - node.Kind == ExprNodeKind.ObjectReference || - node.Kind == ExprNodeKind.UInt16Pair || - node.Kind == ExprNodeKind.LabelTarget || - node.Kind == ExprNodeKind.MemberAssignment || - node.Kind == ExprNodeKind.MemberMemberBinding || - node.Kind == ExprNodeKind.MemberListBinding || - node.Kind == ExprNodeKind.ElementInit) - { - if (node.Kind == ExprNodeKind.CatchBlock) - { - CollectCatchBlockClosureParameterUsages(idx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - } - - WalkClosureChildren(idx, ref node, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - } - - if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0) - { - if (node.NodeType == ExpressionType.Parameter) - { - var parameterId = ToStoredUShortIdx(node.ChildIdx); - if (!Contains(ref lambdaParameterIds, parameterId) && - !Contains(ref localParameterIds, parameterId)) - AddClosureParameterUsage(lambdaIdx, idx, parameterId, ref captures); - } - return; - } - - switch (node.NodeType) - { - case ExpressionType.Parameter: - { - var parameterId = ToStoredUShortIdx(node.ChildIdx); - if (!Contains(ref lambdaParameterIds, parameterId) && - !Contains(ref localParameterIds, parameterId)) - AddClosureParameterUsage(lambdaIdx, idx, parameterId, ref captures); - return; - } - case ExpressionType.Lambda: - PropagateNestedLambdaClosureParameterUsages(idx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - case ExpressionType.Block: - { - // Layout: first child = BlockExprs (expression list); remaining siblings = variables. - var localCount = localParameterIds.Count; - var exprListIdx = node.ChildIdx; - if (exprListIdx == 0) - return; - - ref var exprList = ref Nodes.GetSurePresentRef(exprListIdx); - - var varIdx = exprList.NextIdx; - for (var i = 1; i < node.ChildCount && varIdx != 0 && varIdx != idx; ++i) - { - ref var v = ref Nodes.GetSurePresentRef(varIdx); - localParameterIds.Add(ToStoredUShortIdx(v.ChildIdx)); - varIdx = v.NextIdx; - } - - if (exprList.Is(ExprNodeKind.BlockExprs)) - WalkClosureChildren(exprListIdx, ref exprList, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - else - CollectClosureParameterUsages(exprListIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - - localParameterIds.Count = localCount; - return; - } - case ExpressionType.Try: - { - WalkClosureChildren(idx, ref node, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - return; - } - } - - WalkClosureChildren(idx, ref node, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - } - - private void WalkClosureChildren( - ushort idx, - ref ExprNode node, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - if (node.ChildCount == 0 || node.ChildIdx == 0) - return; - - // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this - // owner (last-child.NextIdx is the parent up-link). - var cIdx = node.ChildIdx; - for (var i = 0; i < node.ChildCount && cIdx != 0 && cIdx != idx; ++i) - { - var next = Nodes.GetSurePresentRef(cIdx).NextIdx; - CollectClosureParameterUsages(cIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - cIdx = next; - } - } - - private void CollectCatchBlockClosureParameterUsages( - ushort idx, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - ref var node = ref Nodes.GetSurePresentRef(idx); - Debug.Assert(node.Is(ExprNodeKind.CatchBlock)); - - var localCount = localParameterIds.Count; - var childIdx = node.ChildIdx; - if (node.HasFlag(CatchHasVariableFlag)) - { - localParameterIds.Add(ToStoredUShortIdx(Nodes.GetSurePresentRef(childIdx).ChildIdx)); - childIdx = Nodes.GetSurePresentRef(childIdx).NextIdx; - } - - var bodyIdx = childIdx; - childIdx = Nodes.GetSurePresentRef(childIdx).NextIdx; - if (node.HasFlag(CatchHasFilterFlag)) - CollectClosureParameterUsages(childIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - CollectClosureParameterUsages(bodyIdx, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); - localParameterIds.Count = localCount; - } - - private void PropagateNestedLambdaClosureParameterUsages( - ushort nestedLambdaIdx, - ushort lambdaIdx, - ref ChildIdxs lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, - ref SmallList, NoArrayPool> captures) - { - for (var i = 0; i < LambdaClosureParameterUsages.Count; ++i) - { - ref var usage = ref LambdaClosureParameterUsages[i]; - if (usage.LambdaIdx != nestedLambdaIdx) - continue; - if (Contains(ref lambdaParameterIds, usage.ParameterId) || - Contains(ref localParameterIds, usage.ParameterId)) - continue; - AddClosureParameterUsage(lambdaIdx, usage.ParameterIdx, usage.ParameterId, ref captures); - } - } - - private static void AddClosureParameterUsage( - ushort lambdaIdx, - ushort parameterIdx, - ushort parameterId, - ref SmallList, NoArrayPool> captures) - { - for (var i = 0; i < captures.Count; ++i) - if (captures[i].ParameterId == parameterId) - return; - captures.Add(new LambdaClosureParameterUsage(lambdaIdx, parameterIdx, parameterId)); - } - - private ChildIdxs GetChildren(int idx) - { - ref var node = ref Nodes.GetSurePresentRef(idx); - if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0 || node.ChildIdx == 0) - return default; - var count = node.ChildCount; - ChildIdxs children = default; - // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this - // owner (last-child.NextIdx is the parent up-link). - var childIdx = node.ChildIdx; - for (var i = 0; i < count && childIdx != 0 && childIdx != idx; ++i) - { - children.Add(childIdx); - childIdx = Nodes.GetSurePresentRef(childIdx).NextIdx; - } - return children; - } /// Reconstructs the flat tree as a System.Linq expression tree. [RequiresUnreferencedCode(LightExpression.Trimming.Message)] [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", @@ -1565,16 +1276,6 @@ private static Type GetArrayElementType(Type arrayType, int depth) return elementType ?? typeof(object); } - private static bool Contains(ref SmallList ids, ushort value) - where TStack : struct, IStack - where TPool : struct, ISmallArrayPool - { - for (var i = 0; i < ids.Count; ++i) - if (ids[i] == value) - return true; - return false; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ushort ToStoredUShortIdx(int idx) => checked((ushort)idx); diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index f3636675a..4542bbbd7 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -42,23 +42,9 @@ public int Run() Flat_lambda_multiple_parameter_refs_all_yield_same_identity(); Flat_block_variables_and_refs_yield_same_identity(); Flat_nested_lambda_captures_outer_parameter_identity(); - Flat_lambda_closure_parameter_usages_track_captured_outer_parameter_during_direct_construction(); - Flat_lambda_closure_parameter_usages_propagate_across_nested_lambdas_during_direct_construction(); - Flat_lambda_closure_parameter_usages_track_captured_block_variable_during_direct_construction(); - Flat_lambda_closure_parameter_usages_track_captures_from_expression_conversion(); Flat_out_of_order_decl_block_in_lambda_compiles_correctly(); Flat_enum_constant_stored_inline_roundtrip(); Flat_metadata_uses_16_bit_idx_storage(); - Flat_lambda_nodes_tracks_all_lambdas_during_direct_construction(); - Flat_lambda_nodes_tracks_deeply_nested_lambdas_during_direct_construction(); - Flat_lambda_nodes_tracks_lambdas_from_expression_conversion(); - Flat_lambda_nodes_has_single_entry_for_root_only_lambda(); - Flat_blocks_with_variables_tracked_during_direct_construction(); - Flat_goto_and_label_nodes_tracked_during_direct_construction(); - Flat_try_catch_nodes_tracked_during_direct_construction(); - Flat_blocks_with_variables_tracked_from_expression_conversion(); - Flat_goto_and_label_nodes_tracked_from_expression_conversion(); - Flat_try_catch_nodes_tracked_from_expression_conversion(); Flat_equal_lambdas_with_different_parameter_names_are_structurally_equal_and_hash_equal(); Flat_equal_nested_lambdas_with_captures_are_structurally_equal_and_hash_equal(); Flat_standalone_parameters_use_name_in_structural_equality(); @@ -1021,80 +1007,6 @@ public void Flat_nested_lambda_captures_outer_parameter_identity() Asserts.AreSame(sysOuter.Parameters[0], sysInner.Body); } - public void Flat_lambda_closure_parameter_usages_track_captured_outer_parameter_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - var inner = fe.Lambda>(x); - fe.RootIdx = fe.Lambda>>(inner, x); - - Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIdx); - Asserts.AreEqual(fe.Nodes[x].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); - } - - public void Flat_lambda_closure_parameter_usages_propagate_across_nested_lambdas_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - var inner = fe.Lambda>(x); - var middle = fe.Lambda>>(inner); - fe.RootIdx = fe.Lambda>>>(middle, x); - - Asserts.AreEqual(2, fe.LambdaClosureParameterUsages.Count); - - var foundInner = false; - var foundMiddle = false; - for (var i = 0; i < fe.LambdaClosureParameterUsages.Count; ++i) - { - ref var usage = ref fe.LambdaClosureParameterUsages[i]; - Asserts.AreEqual(fe.Nodes[x].ChildIdx, usage.ParameterId); - if (usage.LambdaIdx == inner) foundInner = true; - if (usage.LambdaIdx == middle) foundMiddle = true; - } - - Asserts.IsTrue(foundInner); - Asserts.IsTrue(foundMiddle); - } - - public void Flat_lambda_closure_parameter_usages_track_captured_block_variable_during_direct_construction() - { - var fe = default(ExprTree); - var v = fe.Variable(typeof(int), "v"); - var inner = fe.Lambda>(v); - fe.RootIdx = fe.Lambda>>( - fe.Block(typeof(Func), new[] { v }, - fe.Assign(v, fe.ConstantInt(42)), - inner)); - - Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIdx); - Asserts.AreEqual(fe.Nodes[v].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); - } - - public void Flat_lambda_closure_parameter_usages_track_captures_from_expression_conversion() - { - var x = SysExpr.Parameter(typeof(int), "x"); - var sysLambda = SysExpr.Lambda>>( - SysExpr.Lambda>(x), - x); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(fe.Nodes[fe.LambdaClosureParameterUsages[0].ParameterIdx].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); - - var nestedLambdaCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; ++i) - if (fe.LambdaNodes[i] != fe.RootIdx) - { - ++nestedLambdaCount; - Asserts.AreEqual(fe.LambdaNodes[i], fe.LambdaClosureParameterUsages[0].LambdaIdx); - } - - Asserts.AreEqual(1, nestedLambdaCount); - } - /// /// End-to-end compile-and-run test with a block containing two variables, /// verifying that out-of-order parameter decls and variable refs produce @@ -1160,7 +1072,6 @@ void Check(TEnum enumValue) where TEnum : Enum public void Flat_metadata_uses_16_bit_idx_storage() { Asserts.AreEqual(24, typeof(ExprNode).StructLayoutAttribute.Size); - Asserts.AreEqual(6, typeof(LambdaClosureParameterUsage).StructLayoutAttribute.Size); var fe = default(ExprTree); // Index 0 is reserved as the absent-child sentinel, so MaxValue-1 real nodes fill storage. @@ -1171,241 +1082,6 @@ public void Flat_metadata_uses_16_bit_idx_storage() Asserts.Throws(() => fe.ParameterOf("overflow")); } - /// - /// When building a flat expression directly, calling Lambda() for a nested lambda - /// and then for the root lambda should result in both indices recorded in LambdaNodes. - /// The root is identified by RootIdx; all others are nested. - /// - public void Flat_lambda_nodes_tracks_all_lambdas_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - - // Build: outer: x => () => x - var inner = fe.Lambda>(x); - fe.RootIdx = fe.Lambda>>(inner, x); - - // Both the root and nested lambda indices should be recorded - Asserts.AreEqual(2, fe.LambdaNodes.Count); - - // Check that inner and root are both in LambdaNodes - var foundInner = false; - var foundRoot = false; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - { - if (fe.LambdaNodes[i] == inner) foundInner = true; - if (fe.LambdaNodes[i] == fe.RootIdx) foundRoot = true; - } - Asserts.IsTrue(foundInner); - Asserts.IsTrue(foundRoot); - - // Nested lambdas are all LambdaNodes entries that are not the root - var nestedCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] != fe.RootIdx) - ++nestedCount; - Asserts.AreEqual(1, nestedCount); - } - - /// - /// When building a flat expression with multiple levels of nesting, - /// all lambda node indices are captured in LambdaNodes. - /// - public void Flat_lambda_nodes_tracks_deeply_nested_lambdas_during_direct_construction() - { - var fe = default(ExprTree); - var x = fe.ParameterOf("x"); - - // Build: outer: x => (() => (() => x)) - var innermost = fe.Lambda>(x); - var middle = fe.Lambda>>(innermost); - fe.RootIdx = fe.Lambda>>>(middle, x); - - // All three lambda nodes should be recorded - Asserts.AreEqual(3, fe.LambdaNodes.Count); - - // Count nested (non-root) lambdas - var nestedCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] != fe.RootIdx) - ++nestedCount; - Asserts.AreEqual(2, nestedCount); - } - - /// - /// When converting a System.Linq expression tree with nested lambdas via FromExpression, - /// the resulting ExprTree should have all lambda indices populated in LambdaNodes. - /// - public void Flat_lambda_nodes_tracks_lambdas_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - // Build: p => () => p using System.Linq.Expressions - var sysLambda = SysExpr.Lambda>>( - SysExpr.Lambda>(p), - p); - - var fe = sysLambda.ToFlatExpression(); - - // Both root and nested lambda indices should be recorded - Asserts.AreEqual(2, fe.LambdaNodes.Count); - - // The root lambda must be in the list - var foundRoot = false; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] == fe.RootIdx) { foundRoot = true; break; } - Asserts.IsTrue(foundRoot); - - // Exactly one nested lambda - var nestedCount = 0; - for (var i = 0; i < fe.LambdaNodes.Count; i++) - if (fe.LambdaNodes[i] != fe.RootIdx) - ++nestedCount; - Asserts.AreEqual(1, nestedCount); - } - - /// - /// A flat expression with no nested lambdas (root-only) should have exactly one - /// entry in LambdaNodes (the root itself). - /// - public void Flat_lambda_nodes_has_single_entry_for_root_only_lambda() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - fe.RootIdx = fe.Lambda>(fe.Add(p, fe.ConstantInt(1)), p); - - Asserts.AreEqual(1, fe.LambdaNodes.Count); - Asserts.AreEqual(fe.RootIdx, fe.LambdaNodes[0]); - } - - /// - /// Block nodes with explicit variable declarations are recorded in BlocksWithVariables; - /// blocks without variables produce no entry. - /// - public void Flat_blocks_with_variables_tracked_during_direct_construction() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - var v = fe.Variable(typeof(int), "v"); - - // Block with one variable: should be tracked - var blockWithVar = fe.Block(typeof(int), new[] { v }, fe.Assign(v, p), v); - // Block without variables: should NOT be tracked - var blockNoVar = fe.Block(fe.Add(p, fe.ConstantInt(1))); - - fe.RootIdx = fe.Lambda>(fe.Block(blockWithVar, blockNoVar), p); - - Asserts.AreEqual(1, fe.BlocksWithVariables.Count); - Asserts.AreEqual(blockWithVar, fe.BlocksWithVariables[0]); - } - - /// - /// Goto and label expression nodes are recorded in GotoNodes and LabelNodes respectively. - /// - public void Flat_goto_and_label_nodes_tracked_during_direct_construction() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - var target = fe.Label(typeof(int), "done"); - - var gotoNode = fe.Goto(target, p, typeof(int)); - var labelNode = fe.Label(target, fe.ConstantInt(0)); - - fe.RootIdx = fe.Lambda>(fe.Block(gotoNode, labelNode), p); - - Asserts.AreEqual(1, fe.GotoNodes.Count); - Asserts.AreEqual(gotoNode, fe.GotoNodes[0]); - - Asserts.AreEqual(1, fe.LabelNodes.Count); - Asserts.AreEqual(labelNode, fe.LabelNodes[0]); - } - - /// - /// Try/catch, try/finally and try/fault node indices are all recorded in TryCatchNodes. - /// - public void Flat_try_catch_nodes_tracked_during_direct_construction() - { - var fe = default(ExprTree); - var p = fe.ParameterOf("p"); - - var tryCatchNode = fe.TryCatch( - fe.Add(p, fe.ConstantInt(1)), - fe.Catch(typeof(Exception), fe.ConstantInt(-1))); - - var tryFinallyNode = fe.TryFinally( - fe.Add(p, fe.ConstantInt(2)), - fe.Default(typeof(void))); - - fe.RootIdx = fe.Lambda>( - fe.Block(tryCatchNode, tryFinallyNode), p); - - Asserts.AreEqual(2, fe.TryCatchNodes.Count); - - var foundTryCatch = false; - var foundTryFinally = false; - for (var i = 0; i < fe.TryCatchNodes.Count; i++) - { - if (fe.TryCatchNodes[i] == tryCatchNode) foundTryCatch = true; - if (fe.TryCatchNodes[i] == tryFinallyNode) foundTryFinally = true; - } - Asserts.IsTrue(foundTryCatch); - Asserts.IsTrue(foundTryFinally); - } - - /// - /// When converting a System.Linq expression tree, blocks with variables are - /// recorded in BlocksWithVariables; plain blocks are not. - /// - public void Flat_blocks_with_variables_tracked_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - var v = SysExpr.Variable(typeof(int), "v"); - // block with variable - var sysBlock = SysExpr.Block(new[] { v }, SysExpr.Assign(v, p), v); - var sysLambda = SysExpr.Lambda>(sysBlock, p); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.BlocksWithVariables.Count); - } - - /// - /// When converting a System.Linq expression tree with goto/label, both - /// GotoNodes and LabelNodes are populated. - /// - public void Flat_goto_and_label_nodes_tracked_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - var target = SysExpr.Label(typeof(int), "done"); - var sysLambda = SysExpr.Lambda>( - SysExpr.Block( - SysExpr.Goto(target, p, typeof(int)), - SysExpr.Label(target, SysExpr.Constant(0))), - p); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.GotoNodes.Count); - Asserts.AreEqual(1, fe.LabelNodes.Count); - } - - /// - /// When converting a System.Linq expression tree with a try/catch, - /// TryCatchNodes is populated. - /// - public void Flat_try_catch_nodes_tracked_from_expression_conversion() - { - var p = SysExpr.Parameter(typeof(int), "p"); - var sysLambda = SysExpr.Lambda>( - SysExpr.TryCatch( - SysExpr.Add(p, SysExpr.Constant(1)), - SysExpr.Catch(typeof(Exception), SysExpr.Constant(-1))), - p); - - var fe = sysLambda.ToFlatExpression(); - - Asserts.AreEqual(1, fe.TryCatchNodes.Count); - } - public void Flat_equal_lambdas_with_different_parameter_names_are_structurally_equal_and_hash_equal() { var x = Parameter(typeof(int), "x"); From 1adf731ffa8669aab8e4451cd6b6dc8cb9bc9ac6 Mon Sep 17 00:00:00 2001 From: dadhi Date: Fri, 30 Oct 2026 00:21:27 +0100 Subject: [PATCH 02/19] Flat expr: explicit presence flags for Switch default/Try finally, CsCheck fuzzer, fix SwitchCase, Block Eq/Hash and WithOneChild stale ref Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FlatExpression.cs | 48 +++----- .../LightExpressionPropertyTests.cs | 112 ++++++++++++++++++ .../LightExpressionTests.cs | 1 + 3 files changed, 132 insertions(+), 29 deletions(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index e73278355..2e0e66a85 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -178,6 +178,8 @@ public struct ExprTree : IEquatable private const byte CatchHasVariableFlag = 1; private const byte CatchHasFilterFlag = 2; private const byte TryFaultFlag = 1; + private const byte TryHasFinallyFlag = 2; + private const byte SwitchHasDefaultFlag = 1; /// Gets or sets the root node idx. public int RootIdx; @@ -365,7 +367,10 @@ private ushort WithOneChild(ExpressionType nodeType, Type type, object obj, byte var ownerIdx = (ushort)idx; if (ch0 != 0) - Nodes.GetSurePresentRef(ownerIdx).SetChildrenInfo(1, MayBeCloneChildForOwner(ch0, ownerIdx)); + { + var child = MayBeCloneChildForOwner(ch0, ownerIdx); // may grow Nodes, so take the owner ref after it + Nodes.GetSurePresentRef(ownerIdx).SetChildrenInfo(1, child); + } return ownerIdx; } @@ -770,8 +775,7 @@ public ushort Loop(ushort body, ushort @break = 0, ushort @continue = 0) /// Adds a switch-case node. Layout: test values then body. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort SwitchCase(ushort body, params ushort[] testValues) => - WithOneOrMoreChildren(default, null, null, default, ExprNodeKind.SwitchCase, 0, - AppendUShort(testValues, body)); + WithChildren(default, null, null, default, ExprNodeKind.SwitchCase, AppendUShort(testValues, body)); /// Adds a switch node. Layout: switchValue, optional defaultBody, optional SwitchCases group. public ushort Switch(Type type, ushort switchValue, ushort defaultBody, MethodInfo comparison, params ushort[] cases) @@ -779,7 +783,7 @@ public ushort Switch(Type type, ushort switchValue, ushort defaultBody, MethodIn var casesIdx = cases == null || cases.Length == 0 ? (ushort)0 : WithChildren(ExpressionType.Switch, type, null, default, ExprNodeKind.SwitchCases, cases); - return WithThreeChildren(ExpressionType.Switch, type, comparison, default, default, switchValue, defaultBody, casesIdx); + return WithThreeChildren(ExpressionType.Switch, type, comparison, defaultBody != 0 ? SwitchHasDefaultFlag : (byte)0, default, switchValue, defaultBody, casesIdx); } /// Adds a switch node without an explicit default case or comparer. @@ -823,7 +827,7 @@ public ushort TryCatch(ushort body, params ushort[] handlers) => /// Adds a try/finally node. public ushort TryFinally(ushort body, ushort @finally) => - WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally); + WithTwoChildren(ExpressionType.Try, Nodes[body].Type, null, TryHasFinallyFlag, default, body, @finally); /// Adds a try/fault node. public ushort TryFault(ushort body, ushort fault) => @@ -831,7 +835,7 @@ public ushort TryFault(ushort body, ushort fault) => /// Adds a try node with optional finally block and catch handlers. Layout: body, finally, handlers… public ushort TryCatchFinally(ushort body, ushort @finally, params ushort[] handlers) => - WithTwoOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, default, default, body, @finally, handlers); + WithTwoOrMoreChildren(ExpressionType.Try, Nodes[body].Type, null, TryHasFinallyFlag, default, body, @finally, handlers); /// Adds a type-test node. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1383,7 +1387,7 @@ public bool Eq(ref ExprTree xTree, ref ExprTree yTree) } break; - case ExpressionType.Block: + case ExpressionType.Block when x.Kind != ExprNodeKind.BlockExprs: // Layout: first child = BlockExprs; remaining siblings = variables. if (x.ChildCount == 0) return false; @@ -1545,7 +1549,7 @@ private int HashNode(ref ExprTree tree, int idx) case ExpressionType.Lambda: return HashLambda(ref tree, idx, h); - case ExpressionType.Block: + case ExpressionType.Block when node.Kind != ExprNodeKind.BlockExprs: return HashBlock(ref tree, idx, h); } @@ -1842,20 +1846,10 @@ public SysExpr ReadExpression(int idx) { // Layout: switchValue, optional defaultBody, optional SwitchCases group. var children = GetChildren(idx); - var defaultBody = default(SysExpr); - ChildIdxs caseIdxs = default; - if (children.Count > 1) - { - ref var lastChild = ref _tree.Nodes[children[children.Count - 1]]; - if (lastChild.Is(ExprNodeKind.SwitchCases)) - { - caseIdxs = GetChildren(children[children.Count - 1]); - if (children.Count == 3) - defaultBody = ReadExpression(children[1]); - } - else - defaultBody = ReadExpression(children[1]); - } + var hasDefault = node.HasFlag(SwitchHasDefaultFlag); + var defaultBody = hasDefault ? ReadExpression(children[1]) : null; + var casesIdx = hasDefault ? 2 : 1; + var caseIdxs = children.Count > casesIdx ? GetChildren(children[casesIdx]) : default; var cases = new SysSwitchCase[caseIdxs.Count]; for (var i = 0; i < cases.Length; ++i) cases[i] = ReadSwitchCase(caseIdxs[i]); @@ -1868,13 +1862,9 @@ public SysExpr ReadExpression(int idx) if (node.HasFlag(TryFaultFlag)) return SysExpr.TryFault(ReadExpression(children[0]), ReadExpression(children[1])); - var handlerStart = 1; - var @finally = default(SysExpr); - if (children.Count > 1 && !_tree.Nodes[children[1]].Is(ExprNodeKind.CatchBlock)) - { - @finally = ReadExpression(children[1]); - handlerStart = 2; - } + var hasFinally = node.HasFlag(TryHasFinallyFlag); + var @finally = hasFinally ? ReadExpression(children[1]) : null; + var handlerStart = hasFinally ? 2 : 1; var handlers = new SysCatchBlock[children.Count - handlerStart]; for (var i = 0; i < handlers.Length; ++i) diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 7fa152f49..9a7d68c58 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq.Expressions; using FastExpressionCompiler.FlatExpression; +using SysExpr = System.Linq.Expressions.Expression; +using SysSwitchCase = System.Linq.Expressions.SwitchCase; +using SysCatchBlock = System.Linq.Expressions.CatchBlock; using static FastExpressionCompiler.LightExpression.Expression; #if NET8_0_OR_GREATER using CsCheck; @@ -26,6 +29,114 @@ public void Can_property_test_generated_flat_expression_roundtrip_structurally() print: testCase => $"{testCase.Seed}: {testCase.Spec}"); } + /// Fuzzes presence/absence of every optional child (switch default/cases, try finally/catch, catch variable/filter, + /// loop break/continue, goto value, label default): the bits decide what is present; the flat tree must report exactly that. + public void Can_fuzz_optional_children_presence_in_flat_expression() + { + Gen.Int[0, (1 << 10) - 1].Sample( + bits => CheckOptionalChildren(bits), + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: bits => System.Convert.ToString(bits, 2).PadLeft(10, '0')); + } + + private static bool CheckOptionalChildren(int bits) + { + var swDefault = (bits & 1) != 0; + var swCases = (bits >> 1 & 3) % 3; + if (!swDefault && swCases == 0) swCases = 1; // switch needs a default or at least one case + var hasFinally = (bits & 8) != 0; + var hasCatch = (bits & 16) != 0 || !hasFinally; // try needs a catch or a finally + var catchVar = hasCatch && (bits & 32) != 0; + var catchFilter = hasCatch && (bits & 64) != 0; + var loopBreak = (bits & 128) != 0; + var loopContinue = (bits & 256) != 0; + var gotoValue = (bits & 512) != 0; + var labelDefault = gotoValue; // a non-void label needs its default value + + var p = SysExpr.Parameter(typeof(int), "p"); + var r = SysExpr.Variable(typeof(int), "r"); + var t = SysExpr.Variable(typeof(int), "t"); + var i = SysExpr.Variable(typeof(int), "i"); + var g = SysExpr.Variable(typeof(int), "g"); + var fin = SysExpr.Variable(typeof(int), "fin"); + SysExpr Int(int x) => SysExpr.Constant(x); + + var cases = new SysSwitchCase[swCases]; + for (var k = 0; k < cases.Length; ++k) + cases[k] = SysExpr.SwitchCase(SysExpr.Assign(r, Int(10 + k)), Int(k)); + var @switch = SysExpr.Switch(typeof(void), p, swDefault ? SysExpr.Assign(r, Int(7)) : null, null, cases); + + var tryBody = SysExpr.Condition(SysExpr.LessThan(p, Int(0)), SysExpr.Throw(SysExpr.New(typeof(Exception)), typeof(int)), p); + var filter = catchFilter ? SysExpr.Equal(Int(1), Int(1)) : null; + var handlers = !hasCatch ? Array.Empty() : new[] + { + catchVar ? SysExpr.Catch(SysExpr.Variable(typeof(Exception), "ex"), Int(-1), filter) : SysExpr.Catch(typeof(Exception), Int(-1), filter) + }; + var @finally = SysExpr.Assign(fin, Int(1)); + var @try = !hasFinally ? SysExpr.TryCatch(tryBody, handlers) + : hasCatch ? SysExpr.TryCatchFinally(tryBody, @finally, handlers) + : SysExpr.TryFinally(tryBody, @finally); + + var brk = loopBreak ? SysExpr.Label("brk") : null; + var cont = loopContinue ? SysExpr.Label("cont") : null; + var exit = SysExpr.Label("exit"); + var loop = SysExpr.Block( + SysExpr.Loop(SysExpr.Block( + SysExpr.Assign(i, SysExpr.Add(i, Int(1))), + cont != null ? SysExpr.IfThen(SysExpr.LessThan(i, Int(2)), SysExpr.Continue(cont)) : SysExpr.Empty(), + SysExpr.IfThen(SysExpr.GreaterThanOrEqual(i, Int(3)), loopBreak ? SysExpr.Break(brk) : SysExpr.Goto(exit))), brk, cont), + SysExpr.Label(exit)); + + var l = SysExpr.Label(gotoValue ? typeof(int) : typeof(void), "l"); + var gotoLabel = gotoValue + ? SysExpr.Block(SysExpr.Goto(l, Int(5)), SysExpr.Label(l, labelDefault ? Int(9) : null)) + : SysExpr.Block(typeof(int), SysExpr.Goto(l), SysExpr.Label(l), Int(3)); + + var lambda = SysExpr.Lambda>( + SysExpr.Block(typeof(int), new[] { r, t, i, g, fin }, + @switch, SysExpr.Assign(t, @try), loop, SysExpr.Assign(g, gotoLabel), + SysExpr.Add(SysExpr.Add(r, t), SysExpr.Add(SysExpr.Add(i, g), fin))), + p); + + var fe = lambda.ToFlatExpression(); + var back2 = ((System.Linq.Expressions.Expression>)fe.ToExpression()).ToFlatExpression(); + Ensure(fe.Equals(back2), "structural roundtrip: Equals"); + Ensure(fe.GetHashCode() == back2.GetHashCode(), "structural roundtrip: GetHashCode"); + + int Count(Func match) + { + var n = 0; + for (var k = 1; k < fe.Nodes.Count; ++k) + if (match(fe.Nodes[k])) ++n; + return n; + } + bool Is(ExprNode n, ExpressionType type, int childCount) => + n.Kind == ExprNodeKind.Expression && n.NodeType == type && n.ChildCount == childCount; + + Ensure(Count(n => Is(n, ExpressionType.Switch, 1 + (swDefault ? 1 : 0) + (swCases > 0 ? 1 : 0))) == 1, "switch children"); + Ensure(Count(n => Is(n, ExpressionType.Loop, 1 + (loopBreak ? 1 : 0) + (loopContinue ? 1 : 0))) == 1, "loop children"); + Ensure(Count(n => Is(n, ExpressionType.Try, 1 + (hasFinally ? 1 : 0) + (hasCatch ? 1 : 0))) == 1, "try children"); + Ensure(Count(n => n.Kind == ExprNodeKind.CatchBlock && n.ChildCount == 1 + (catchVar ? 1 : 0) + (catchFilter ? 1 : 0)) == (hasCatch ? 1 : 0), "catch children"); + Ensure(Count(n => Is(n, ExpressionType.Goto, 2)) == (gotoValue ? 1 : 0), "goto value"); + Ensure(Count(n => Is(n, ExpressionType.Label, 2)) == (labelDefault ? 1 : 0), "label default"); + + if (catchFilter) return true; // FEC does not support catch filters, the structure is checked above + + var fast = fe.ToLightExpression() is FastExpressionCompiler.LightExpression.LambdaExpression fl + ? fl.CompileFast>(ifFastFailedReturnNull: true) : null; + Ensure(fast != null, "CompileFast failed"); + var expected = lambda.Compile(); + // an uncaught throw (no catch) must happen in both + static int Run(Func f, int arg) { try { return f(arg); } catch (Exception) { return int.MinValue; } } + foreach (var arg in new[] { -1, 0, 1, 2, 5 }) + Ensure(Run(fast, arg) == Run(expected, arg), $"result for {arg}"); + return true; + } + + private static void Ensure(bool ok, string what) + { + if (!ok) throw new InvalidOperationException("Property failed: " + what); + } + private static FastExpressionCompiler.LightExpression.Expression> CreateGeneratedLightExpression(IntSpec spec) { var parameter = ParameterOf("p"); @@ -310,5 +421,6 @@ private bool EqBlock(FastExpressionCompiler.LightExpression.BlockExpression x, F } #else public void Can_property_test_generated_flat_expression_roundtrip_structurally() { } + public void Can_fuzz_optional_children_presence_in_flat_expression() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 4542bbbd7..b7235a1ee 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -37,6 +37,7 @@ public int Run() Can_build_flat_expression_directly_with_light_expression_like_api(); Can_build_flat_expression_control_flow_directly(); Can_property_test_generated_flat_expression_roundtrip_structurally(); + Can_fuzz_optional_children_presence_in_flat_expression(); Flat_lambda_parameter_ref_before_decl_preserves_identity(); Flat_lambda_multiple_parameter_refs_all_yield_same_identity(); From 1e87f0ca9446c3578f53d3e34bc68158415ba41a Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 21:52:54 +0100 Subject: [PATCH 03/19] Experimental flat-native collect and emit path with comparison test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FlatExpressionCompiler.cs | 243 ++++++++++++++++++ .../LightExpressionPropertyTests.cs | 59 +++++ .../LightExpressionTests.cs | 2 + 3 files changed, 304 insertions(+) create mode 100644 src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs new file mode 100644 index 000000000..9de83cd9a --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -0,0 +1,243 @@ +// Experimental: the initial compilation path for the ExprTree, to learn how much of FEC could work on the flat nodes directly. +#nullable disable +namespace FastExpressionCompiler.LightExpression; + +using System; +using System.Reflection.Emit; +using FastExpressionCompiler.FlatExpression; +using FastExpressionCompiler.LightExpression.ImTools; +using ExprType = System.Linq.Expressions.ExpressionType; + +public static partial class ExpressionCompiler +{ + /// Compiles the flat tree. The int/bool subset goes straight to IL from the nodes, no Expression objects are made; + /// everything else is converted to the LightExpression and compiled the usual way. + public static TDelegate CompileFast(this ref ExprTree tree, bool ifFastFailedReturnNull = false, + CompilerFlags flags = CompilerFlags.Default) where TDelegate : class => + tree.TryCompileFlat() + ?? ((LambdaExpression)tree.ToLightExpression()).CompileFast(ifFastFailedReturnNull, flags); + + struct FlatVar + { + public ushort Id; + public short Arg; // the argument index (the closure is 0) or -1 for the local + public LocalBuilder Local; + } + + /// The collect phase on the flat nodes: one linear pass over the contiguous array, no recursion, no stack, no per-node casts. + /// What the tree construction already guarantees is not checked here: the child counts, the not-null children, the parameter + /// and label ids, the closure constants (they are in the separate list, so `IsClosureBoundConstant` is already answered). + /// Returns `OK` if the tree is the int/bool subset which can emit. + public static Result TryCollectFlatInfo(this ref ExprTree tree) + { + var count = tree.Nodes.Count; + if (count < 2 || tree.ClosureConstants.Count != 0) + return Result.NotSupported_UnknownExpression; + + var lambdas = 0; + for (var i = 1; i < count; ++i) + { + ref var n = ref tree.Nodes.GetSurePresentRef(i); + if (n.Kind == ExprNodeKind.BlockExprs) + continue; + if (n.Kind != ExprNodeKind.Expression) + return Result.NotSupported_UnknownExpression; + + var t = n.Type; + switch (n.NodeType) + { + case ExprType.Add: + case ExprType.Subtract: + case ExprType.Multiply: + case ExprType.Conditional: + case ExprType.Assign: + case ExprType.Block: + if (t != typeof(int)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Parameter: + if (t != typeof(int) || n.Flags != 0) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Constant: + if (!ReferenceEquals(n.Obj, ExprNode.InlineValueMarker) || t != typeof(int) && t != typeof(bool)) + return Result.NotSupported_UnknownExpression; + break; + case ExprType.Equal: + case ExprType.GreaterThan: + if (tree.Nodes.GetSurePresentRef(n.ChildIdx).Type != typeof(int)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.AndAlso: + case ExprType.OrElse: + case ExprType.Not: + if (t != typeof(bool)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.Lambda: + if (++lambdas != 1) return Result.NotSupported_UnknownExpression; // the nested lambdas need the closure + break; + default: + return Result.NotSupported_UnknownExpression; + } + } + + return lambdas == 1 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; + } + + /// Returns null if the tree is not the supported subset or the delegate type is not the lambda type. + public static TDelegate TryCompileFlat(this ref ExprTree tree) where TDelegate : class + { + if (tree.TryCollectFlatInfo() != Result.OK) + return null; + + ref var lambda = ref tree.Nodes.GetSurePresentRef(tree.RootIdx); + if (lambda.Type != typeof(TDelegate)) + return null; + + var paramCount = lambda.ChildCount - 1; + var paramTypes = new Type[paramCount + 1]; + paramTypes[0] = typeof(ArrayClosure); + for (var i = 1; i <= paramCount; ++i) + paramTypes[i] = typeof(int); + + var dynMethod = new DynamicMethod(string.Empty, typeof(int), paramTypes, typeof(ArrayClosure), true); + var il = dynMethod.GetILGenerator(); + + var vars = default(SmallList, NoArrayPool>); + var bodyIdx = lambda.ChildIdx; + var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + for (var i = 1; i <= paramCount; ++i) + { + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); + vars.Add(new FlatVar { Id = par.ChildIdx, Arg = (short)i }); + parIdx = par.NextIdx; + } + + EmitFlat(ref tree, bodyIdx, il, ref vars, false); + il.Emit(OpCodes.Ret); + return (TDelegate)(object)dynMethod.CreateDelegate(typeof(TDelegate), EmptyArrayClosure); + } + + private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, + ref SmallList, NoArrayPool> vars, bool ignoreResult) + { + ref var n = ref tree.Nodes.GetSurePresentRef(idx); + var nodeType = n.NodeType; + switch (nodeType) + { + case ExprType.Constant: + if (!ignoreResult) + il.Emit(OpCodes.Ldc_I4, (int)n.InlineValue); + return; + + case ExprType.Parameter: + if (ignoreResult) + return; + for (var i = vars.Count - 1; i >= 0; --i) + { + ref var v = ref vars.GetSurePresentRef(i); + if (v.Id != n.ChildIdx) + continue; + if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); else il.Emit(OpCodes.Ldloc, v.Local); + return; + } + throw new InvalidOperationException("The parameter is not in scope"); // todo: @wip by construction check + + case ExprType.Assign: + { + var leftIdx = n.ChildIdx; + ref var left = ref tree.Nodes.GetSurePresentRef(leftIdx); + EmitFlat(ref tree, left.NextIdx, il, ref vars, false); + if (!ignoreResult) + il.Emit(OpCodes.Dup); + for (var i = vars.Count - 1; i >= 0; --i) + { + ref var v = ref vars.GetSurePresentRef(i); + if (v.Id != left.ChildIdx) + continue; + if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else il.Emit(OpCodes.Stloc, v.Local); + return; + } + throw new InvalidOperationException("The assigned parameter is not in scope"); + } + + case ExprType.Not: + EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + break; + + case ExprType.Conditional: + { + var testIdx = n.ChildIdx; + var ifTrueIdx = tree.Nodes.GetSurePresentRef(testIdx).NextIdx; + var ifFalseIdx = tree.Nodes.GetSurePresentRef(ifTrueIdx).NextIdx; + var elseLabel = il.DefineLabel(); + var endLabel = il.DefineLabel(); + EmitFlat(ref tree, testIdx, il, ref vars, false); + il.Emit(OpCodes.Brfalse, elseLabel); + EmitFlat(ref tree, ifTrueIdx, il, ref vars, ignoreResult); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(elseLabel); + EmitFlat(ref tree, ifFalseIdx, il, ref vars, ignoreResult); + il.MarkLabel(endLabel); + return; + } + + case ExprType.AndAlso: + case ExprType.OrElse: + { + var isAnd = nodeType == ExprType.AndAlso; + var rightIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + var shortCircuit = il.DefineLabel(); + var endLabel = il.DefineLabel(); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + il.Emit(isAnd ? OpCodes.Brfalse : OpCodes.Brtrue, shortCircuit); + EmitFlat(ref tree, rightIdx, il, ref vars, false); + il.Emit(OpCodes.Br, endLabel); + il.MarkLabel(shortCircuit); + il.Emit(isAnd ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1); + il.MarkLabel(endLabel); + break; + } + + case ExprType.Block: + { + // layout: the first child is the BlockExprs list, the rest are the variables + var exprsIdx = n.ChildIdx; + ref var exprs = ref tree.Nodes.GetSurePresentRef(exprsIdx); + var varIdx = exprs.NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); + vars.Add(new FlatVar { Id = v.ChildIdx, Arg = -1, Local = il.DeclareLocal(typeof(int)) }); + varIdx = v.NextIdx; + } + + var last = exprs.ChildCount - 1; + var exprIdx = exprs.ChildIdx; + for (var i = 0; i <= last; ++i) + { + EmitFlat(ref tree, exprIdx, il, ref vars, i != last || ignoreResult); + exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; + } + return; + } + + default: // the binary int/bool operations + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, false); + switch (nodeType) + { + case ExprType.Add: il.Emit(OpCodes.Add); break; + case ExprType.Subtract: il.Emit(OpCodes.Sub); break; + case ExprType.Multiply: il.Emit(OpCodes.Mul); break; + case ExprType.Equal: il.Emit(OpCodes.Ceq); break; + default: il.Emit(OpCodes.Cgt); break; // GreaterThan, the rest is rejected by the collect + } + break; + } + } + + if (ignoreResult) + il.Emit(OpCodes.Pop); + } +} diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 9a7d68c58..76a2e0d18 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -29,6 +29,63 @@ public void Can_property_test_generated_flat_expression_roundtrip_structurally() print: testCase => $"{testCase.Seed}: {testCase.Spec}"); } + /// The flat-native collect+emit must agree with System.Linq on the generated int/bool trees and must not fall back. + public void Can_compile_generated_flat_expression_natively() + { + Gen.Int[0, int.MaxValue / 2].Sample(seed => + { + var spec = GeneratedIntSpecFactory.Create(seed, maxDepth: 3, maxBreadth: 3); + var fe = CreateGeneratedFlatExpression(spec); + Ensure(fe.TryCollectFlatInfo() == ExpressionCompiler.Result.OK, "flat collect"); + var flat = fe.TryCompileFlat>(); + Ensure(flat != null, "flat native compile"); + var expected = CreateGeneratedLightExpression(spec).CompileSys(); + for (var arg = -3; arg <= 3; ++arg) + Ensure(flat(arg) == expected(arg), $"result for {arg}"); + return true; + }, + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GeneratedIntSpecFactory.Create(seed, 3, 3)}"); + } + + /// Prints how long the phases of the compilation take on the flat and on the light trees; asserts nothing but the agreement. + public void Experiment_compare_flat_and_light_compile_phases() + { + const int n = 200, reps = 20; + var flats = new ExprTree[n]; + var lights = new FastExpressionCompiler.LightExpression.Expression>[n]; + var nodes = 0; + for (var i = 0; i < n; ++i) + { + var spec = GeneratedIntSpecFactory.Create(i + 1, maxDepth: 4, maxBreadth: 3); + flats[i] = CreateGeneratedFlatExpression(spec); + lights[i] = CreateGeneratedLightExpression(spec); + nodes += flats[i].Nodes.Count; + } + + void Time(string name, int repeat, Action body) + { + for (var i = 0; i < n; ++i) body(i); // warm up + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (var r = 0; r < repeat; ++r) + for (var i = 0; i < n; ++i) body(i); + sw.Stop(); + Console.WriteLine($"//flat-vs-light {name,-34} {sw.Elapsed.TotalMilliseconds * 1000 / (n * repeat),9:F2} us/expr"); + } + + Console.WriteLine($"//flat-vs-light {n} exprs, {nodes / n} flat nodes avg"); + Time("collect: flat linear scan", reps * 10, i => flats[i].TryCollectFlatInfo()); + Time("collect: light TryCollectInfo", reps * 10, i => + { + var ctx = new ExpressionCompiler.CompilerContext(ExpressionCompiler.ClosureStatus.ToBeCollected, (FastExpressionCompiler.LightExpression.IParameterProvider)lights[i], CompilerFlags.Default); + ExpressionCompiler.TryCollectInfo(ref ctx, lights[i].Body, null, ref ctx.NestedLambdas); + }); + Time("compile: flat native (collect+emit)", reps, i => flats[i].TryCompileFlat>()); + Time("compile: light FEC (collect+emit)", reps, i => lights[i].CompileFast(ifFastFailedReturnNull: true)); + Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpression()).CompileFast>(true)); + Time("convert only: flat->sys->light", reps, i => flats[i].ToLightExpression()); + Time("compile: System.Compile", 1, i => lights[i].CompileSys()); + } + /// Fuzzes presence/absence of every optional child (switch default/cases, try finally/catch, catch variable/filter, /// loop break/continue, goto value, label default): the bits decide what is present; the flat tree must report exactly that. public void Can_fuzz_optional_children_presence_in_flat_expression() @@ -422,5 +479,7 @@ private bool EqBlock(FastExpressionCompiler.LightExpression.BlockExpression x, F #else public void Can_property_test_generated_flat_expression_roundtrip_structurally() { } public void Can_fuzz_optional_children_presence_in_flat_expression() { } + public void Can_compile_generated_flat_expression_natively() { } + public void Experiment_compare_flat_and_light_compile_phases() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index b7235a1ee..9bc26abbd 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -38,6 +38,8 @@ public int Run() Can_build_flat_expression_control_flow_directly(); Can_property_test_generated_flat_expression_roundtrip_structurally(); Can_fuzz_optional_children_presence_in_flat_expression(); + Can_compile_generated_flat_expression_natively(); + Experiment_compare_flat_and_light_compile_phases(); Flat_lambda_parameter_ref_before_decl_preserves_identity(); Flat_lambda_multiple_parameter_refs_all_yield_same_identity(); From 39722840126ad7c4d519f8146172c14f2d41b345 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 22:04:57 +0100 Subject: [PATCH 04/19] Flat native emit: pop block scope, use body type as the return type, test bool and fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FlatExpressionCompiler.cs | 10 ++++++++-- .../LightExpressionPropertyTests.cs | 17 +++++++++++++++++ .../LightExpressionTests.cs | 1 + 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index 9de83cd9a..2d401433e 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -91,17 +91,21 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where if (lambda.Type != typeof(TDelegate)) return null; + var bodyIdx = lambda.ChildIdx; + var returnType = tree.Nodes.GetSurePresentRef(bodyIdx).Type; + if (returnType != typeof(int) && returnType != typeof(bool)) + return null; + var paramCount = lambda.ChildCount - 1; var paramTypes = new Type[paramCount + 1]; paramTypes[0] = typeof(ArrayClosure); for (var i = 1; i <= paramCount; ++i) paramTypes[i] = typeof(int); - var dynMethod = new DynamicMethod(string.Empty, typeof(int), paramTypes, typeof(ArrayClosure), true); + var dynMethod = new DynamicMethod(string.Empty, returnType, paramTypes, typeof(ArrayClosure), true); var il = dynMethod.GetILGenerator(); var vars = default(SmallList, NoArrayPool>); - var bodyIdx = lambda.ChildIdx; var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; for (var i = 1; i <= paramCount; ++i) { @@ -204,6 +208,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var exprsIdx = n.ChildIdx; ref var exprs = ref tree.Nodes.GetSurePresentRef(exprsIdx); var varIdx = exprs.NextIdx; + var scopeStart = vars.Count; for (var i = 1; i < n.ChildCount; ++i) { ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); @@ -218,6 +223,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, EmitFlat(ref tree, exprIdx, il, ref vars, i != last || ignoreResult); exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; } + vars.Count = scopeStart; // the block variables go out of the scope return; } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 76a2e0d18..40d6466d4 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -47,6 +47,22 @@ public void Can_compile_generated_flat_expression_natively() iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GeneratedIntSpecFactory.Create(seed, 3, 3)}"); } + /// The bool body compiles natively; the tree outside the subset (the string constant) falls back and still works. + public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() + { + var fe = default(ExprTree); + var p = fe.ParameterOf("p"); + fe.RootIdx = fe.Lambda>(fe.MakeBinary(ExpressionType.GreaterThan, p, fe.ConstantInt(1)), p); + var native = fe.TryCompileFlat>(); + Ensure(native != null && native(2) && !native(1), "bool lambda native"); + + var fe2 = default(ExprTree); + var p2 = fe2.ParameterOf("p"); + fe2.RootIdx = fe2.Lambda>(fe2.ConstantOf("x"), p2); + Ensure(fe2.TryCompileFlat>() == null, "string is outside the subset"); + Ensure(fe2.CompileFast>()(1) == "x", "fallback result"); + } + /// Prints how long the phases of the compilation take on the flat and on the light trees; asserts nothing but the agreement. public void Experiment_compare_flat_and_light_compile_phases() { @@ -480,6 +496,7 @@ private bool EqBlock(FastExpressionCompiler.LightExpression.BlockExpression x, F public void Can_property_test_generated_flat_expression_roundtrip_structurally() { } public void Can_fuzz_optional_children_presence_in_flat_expression() { } public void Can_compile_generated_flat_expression_natively() { } + public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() { } public void Experiment_compare_flat_and_light_compile_phases() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 9bc26abbd..fbdcb29ca 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -39,6 +39,7 @@ public int Run() Can_property_test_generated_flat_expression_roundtrip_structurally(); Can_fuzz_optional_children_presence_in_flat_expression(); Can_compile_generated_flat_expression_natively(); + Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); Experiment_compare_flat_and_light_compile_phases(); Flat_lambda_parameter_ref_before_decl_preserves_identity(); From f79d5694ca15116484ea0d4674a97c3f4d163948 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 22:17:13 +0100 Subject: [PATCH 05/19] Direct Flat -> Light conversion without System; ideas notes in .scratch Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 28 ++ .../FlatExpression.cs | 103 +++--- .../FlatToLightExpression.cs | 345 ++++++++++++++++++ .../LightExpressionPropertyTests.cs | 7 +- 4 files changed, 431 insertions(+), 52 deletions(-) create mode 100644 .scratch/flat-ideas.md create mode 100644 src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md new file mode 100644 index 000000000..e459599b3 --- /dev/null +++ b/.scratch/flat-ideas.md @@ -0,0 +1,28 @@ +# Flat expression: compile-path ideas and their status + +Status: `todo` / `tried: ` / `dropped: `. +Baseline (Release net10.0, 200 exprs, ~35 nodes, noisy machine - compare rows within one run only): +flat collect 1.6-2.6us, Light collect 6.4-7.5us, flat native compile 12-13us, Light FEC 26-37us, +flat->sys->light->FEC 64-108us, System.Compile 169-198us. + +## Steps +1. `tried: done` Direct Flat -> Light converter (`FlatToLightExpression.cs`, mirrors `SysExprBuilder`), Flat -> System kept. + convert only 43.6us -> 15.0us; flat->light->FEC 45us vs 108us via System (same run). Verified: all tests, the generated-tree + property test, and the optional-children fuzzer (direct == via-system structurally, compiles and agrees with System). + Cost: ~330 duplicated lines (the two builders must be kept in sync). `GetChildren`/`ReadInlineValue` are shared statics on `ExprTree`. +2. `todo` Light -> Flat directly (now Light -> Sys -> Flat) and Sys -> Flat (exists). + Finding: `fe.Equals(fe.ToLightExpression().ToFlatExpression())` is false for the optional-children fuzz tree even via System, + so Light -> Sys -> Flat is not structurally faithful there; investigate when writing the direct Light -> Flat. +3. `todo` Widen the native emitter subset node by node, fuzzer guards each step. + +## Construction-time ideas +- `todo` Profile node access: the scan is ~45ns/node, far from memory bound. Suspect the Stack32 indexer / Type compares. +- `todo` Parent index per node (2 bytes/node, one store at attach): ancestor walk gives scope check and closure-capture detection without an explicit stack. +- `todo` Aggregate flags OR-ed from the children at construction (has lambda / try / goto / closure const / non-int): root flags let simple trees skip collect. +- `todo` Seal pass: renumber to post-order once; subtree = contiguous range, O(1) ancestor test, sizes, usage counts in one pass. +- `todo` SOA split: hot fields (NodeType, ChildIdx, NextIdx, ChildCount) apart from Type/Obj; intern Type to a ushort id. +- `todo` Contiguous child lists instead of first-child + sibling chain (only with Seal). +- `todo` The builders take `ExprTree` by value (big Stack32 copy); pass by `in`/ref. +- `dropped (for now): closure-constant usage counts` - trivial in the collect scan, ClosureConstants.Count==0 already covers the no-closure case. +- `dropped (for now): parameter declaring owner alone` - weak without a parent link; parent index replaces it. +- `deferred: label id table` - conflicts with the "drop info lists" direction. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 2e0e66a85..155dedf97 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -168,7 +168,7 @@ internal bool HasSameShape(ref ExprNode other) => } /// Stores an expression tree as flat nodes plus separate closure constants. -public struct ExprTree : IEquatable +public partial struct ExprTree : IEquatable { private static readonly object ClosureConstantMarker = new(); private const byte ParameterByRefFlag = 1; @@ -1195,9 +1195,9 @@ public SysExpr ToExpression() => ? new SysExprBuilder(this).ReadExpression(RootIdx) : throw new InvalidOperationException("Flat expression tree is empty."); - /// Reconstructs the flat tree as a LightExpression tree. + /// Reconstructs the flat tree as a LightExpression tree via System.Linq expressions; the slower oracle of . [RequiresUnreferencedCode(LightExpression.Trimming.Message)] - public LightExpression.Expression ToLightExpression() => + public LightExpression.Expression ToLightExpressionViaSystem() => LightExpression.FromSysExpressionConverter.ToLightExpression(ToExpression()); /// Structurally compares two flat expression trees. @@ -2023,53 +2023,7 @@ private SysElementInit ReadElementInit(int idx) return SysExpr.ElementInit((MethodInfo)node.Obj, ReadExpressions(GetChildren(idx))); } - private ChildIdxs GetChildren(int idx) - { - ref var node = ref _tree.Nodes.GetSurePresentRef(idx); - if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0 || node.ChildIdx == 0) - return default; - var count = node.ChildCount; - ChildIdxs children = default; - // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this - // owner (last-child.NextIdx is the parent up-link). - var childIdx = node.ChildIdx; - for (var i = 0; i < count && childIdx != 0 && childIdx != idx; ++i) - { - children.Add(childIdx); - childIdx = _tree.Nodes.GetSurePresentRef(childIdx).NextIdx; - } - return children; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object ReadInlineValue(Type type, uint data) - { - if (type.IsEnum) - return Enum.ToObject(type, Type.GetTypeCode(Enum.GetUnderlyingType(type)) switch - { - TypeCode.Byte => (object)(byte)data, - TypeCode.SByte => (object)(sbyte)(byte)data, - TypeCode.Char => (object)(char)(ushort)data, - TypeCode.Int16 => (object)(short)(ushort)data, - TypeCode.UInt16 => (object)(ushort)data, - TypeCode.Int32 => (object)(int)data, - TypeCode.UInt32 => (object)data, - var tc => FlatExpressionThrow.UnsupportedInlineConstantType(type, tc) - }); - return Type.GetTypeCode(type) switch - { - TypeCode.Boolean => (object)(data != 0), - TypeCode.Byte => (object)(byte)data, - TypeCode.SByte => (object)(sbyte)(byte)data, - TypeCode.Char => (object)(char)(ushort)data, - TypeCode.Int16 => (object)(short)(ushort)data, - TypeCode.UInt16 => (object)(ushort)data, - TypeCode.Int32 => (object)(int)data, - TypeCode.UInt32 => (object)data, - TypeCode.Single => (object)FloatBits.ToFloat(data), - _ => FlatExpressionThrow.UnsupportedInlineConstantType(type) - }; - } + private ChildIdxs GetChildren(int idx) => ExprTree.GetChildren(_tree, idx); [RequiresUnreferencedCode(LightExpression.Trimming.Message)] private SysExpr[] ReadExpressions(in ChildIdxs childIdxs) @@ -2085,6 +2039,55 @@ private SysExpr[] ReadExpressions(in ChildIdxs childIdxs) Justification = "Flat expression round-trip stores the runtime type metadata explicitly for reconstruction.")] private static NewExpression CreateValueTypeNewExpression(Type type) => SysExpr.New(type); } + + // Shared by the System and the Light builders. + private static ChildIdxs GetChildren(in ExprTree tree, int idx) + { + ref var node = ref tree.Nodes.GetSurePresentRef(idx); + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0 || node.ChildIdx == 0) + return default; + var count = node.ChildCount; + ChildIdxs children = default; + // Stop after ChildCount. Also stop if the sibling chain hits 0 or loops back to this + // owner (last-child.NextIdx is the parent up-link). + var childIdx = node.ChildIdx; + for (var i = 0; i < count && childIdx != 0 && childIdx != idx; ++i) + { + children.Add(childIdx); + childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; + } + return children; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static object ReadInlineValue(Type type, uint data) + { + if (type.IsEnum) + return Enum.ToObject(type, Type.GetTypeCode(Enum.GetUnderlyingType(type)) switch + { + TypeCode.Byte => (object)(byte)data, + TypeCode.SByte => (object)(sbyte)(byte)data, + TypeCode.Char => (object)(char)(ushort)data, + TypeCode.Int16 => (object)(short)(ushort)data, + TypeCode.UInt16 => (object)(ushort)data, + TypeCode.Int32 => (object)(int)data, + TypeCode.UInt32 => (object)data, + var tc => FlatExpressionThrow.UnsupportedInlineConstantType(type, tc) + }); + return Type.GetTypeCode(type) switch + { + TypeCode.Boolean => (object)(data != 0), + TypeCode.Byte => (object)(byte)data, + TypeCode.SByte => (object)(sbyte)(byte)data, + TypeCode.Char => (object)(char)(ushort)data, + TypeCode.Int16 => (object)(short)(ushort)data, + TypeCode.UInt16 => (object)(ushort)data, + TypeCode.Int32 => (object)(int)data, + TypeCode.UInt32 => (object)data, + TypeCode.Single => (object)FloatBits.ToFloat(data), + _ => FlatExpressionThrow.UnsupportedInlineConstantType(type) + }; + } } /// Union struct for reinterpreting float bits as uint without unsafe code. diff --git a/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs new file mode 100644 index 000000000..260ef32d0 --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs @@ -0,0 +1,345 @@ +#nullable disable +namespace FastExpressionCompiler.FlatExpression; + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using System.Reflection; +using FastExpressionCompiler.LightExpression.ImTools; +using LExpr = FastExpressionCompiler.LightExpression.Expression; +using LParameter = FastExpressionCompiler.LightExpression.ParameterExpression; +using LLabelTarget = FastExpressionCompiler.LightExpression.LabelTarget; +using LSwitchCase = FastExpressionCompiler.LightExpression.SwitchCase; +using LCatchBlock = FastExpressionCompiler.LightExpression.CatchBlock; +using LMemberBinding = FastExpressionCompiler.LightExpression.MemberBinding; +using LElementInit = FastExpressionCompiler.LightExpression.ElementInit; +using ChildIdxs = LightExpression.ImTools.SmallList, LightExpression.ImTools.NoArrayPool>; + +public partial struct ExprTree +{ + /// Reconstructs the flat tree as a LightExpression tree directly, without the System.Linq expressions in between. + /// Mirrors SysExprBuilder: keep the two in sync. + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + public LightExpression.Expression ToLightExpression() => + Nodes.Count != 0 + ? new LightExprBuilder(this).ReadExpression(RootIdx) + : throw new InvalidOperationException("Flat expression tree is empty."); + + private struct LightExprBuilder + { + private readonly ExprTree _tree; + private SmallMap16 _parametersById; + private SmallMap16 _labelsById; + + public LightExprBuilder(ExprTree tree) + { + _tree = tree; + _parametersById = default; + _labelsById = default; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + public LExpr ReadExpression(int idx) + { + ref var node = ref _tree.Nodes[idx]; + if (node.Kind != ExprNodeKind.Expression) + throw new InvalidOperationException($"Node at idx {idx} is not an expression node."); + + switch (node.NodeType) + { + case ExpressionType.Constant: + if (ReferenceEquals(node.Obj, ClosureConstantMarker)) + return LExpr.Constant(_tree.ClosureConstants[node.ChildIdx], node.Type); + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker)) + return LExpr.Constant(ReadInlineValue(node.Type, node.InlineValue), node.Type); + return LExpr.Constant(node.Obj, node.Type); + case ExpressionType.Default: + return LExpr.Default(node.Type); + case ExpressionType.Parameter: + { + ref var parameter = ref _parametersById.Map.AddOrGetValueRef(node.ChildIdx, out var found); + if (found) + return parameter; + + var parameterType = node.HasFlag(ParameterByRefFlag) && !node.Type.IsByRef ? node.Type.MakeByRefType() : node.Type; + return parameter = LExpr.Parameter(parameterType, (string)node.Obj); + } + case ExpressionType.Lambda: + { + // Layout: children[0] = body, children[1..n] = parameter decl nodes. + var children = GetChildren(idx); + var body = ReadExpression(children[0]); + var parameters = new LParameter[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + parameters[i - 1] = (LParameter)ReadExpression(children[i]); + return LExpr.Lambda(node.Type, body, parameters); + } + case ExpressionType.Block: + { + // Layout: children[0] = BlockExprs; children[1..] = variable decls. + var children = GetChildren(idx); + var variables = new LParameter[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + variables[i - 1] = (LParameter)ReadExpression(children[i]); + var expressionIdxs = GetChildren(children[0]); + var expressions = new LExpr[expressionIdxs.Count]; + for (var i = 0; i < expressions.Length; ++i) + expressions[i] = ReadExpression(expressionIdxs[i]); + return LExpr.Block(node.Type, variables, expressions); + } + case ExpressionType.MemberAccess: + { + var children = GetChildren(idx); + return LExpr.MakeMemberAccess(children.Count != 0 ? ReadExpression(children[0]) : null, (MemberInfo)node.Obj); + } + case ExpressionType.Call: + { + var method = (MethodInfo)node.Obj; + var children = GetChildren(idx); + var hasInstance = !method.IsStatic; + var instance = hasInstance ? ReadExpression(children[0]) : null; + var arguments = new LExpr[children.Count - (hasInstance ? 1 : 0)]; + for (var i = hasInstance ? 1 : 0; i < children.Count; ++i) + arguments[i - (hasInstance ? 1 : 0)] = ReadExpression(children[i]); + return LExpr.Call(instance, method, arguments); + } + case ExpressionType.New: + { + var arguments = ReadExpressions(GetChildren(idx)); + return node.Obj is ConstructorInfo ctor ? LExpr.New(ctor, arguments) : LExpr.New(node.Type); + } + case ExpressionType.NewArrayInit: + return LExpr.NewArrayInit(node.Type.GetElementType(), ReadExpressions(GetChildren(idx))); + case ExpressionType.NewArrayBounds: + return LExpr.NewArrayBounds(node.Type.GetElementType(), ReadExpressions(GetChildren(idx))); + case ExpressionType.Invoke: + { + var children = GetChildren(idx); + var arguments = new LExpr[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + arguments[i - 1] = ReadExpression(children[i]); + return LExpr.Invoke(ReadExpression(children[0]), arguments); + } + case ExpressionType.Index: + { + var children = GetChildren(idx); + var property = (PropertyInfo)node.Obj; + var hasInstance = property != null || children.Count > 1; + var instance = hasInstance ? ReadExpression(children[0]) : null; + var arguments = new LExpr[children.Count - (hasInstance ? 1 : 0)]; + for (var i = hasInstance ? 1 : 0; i < children.Count; ++i) + arguments[i - (hasInstance ? 1 : 0)] = ReadExpression(children[i]); + return property != null + ? LExpr.Property(instance, property, arguments) + : LExpr.ArrayAccess(instance, arguments); + } + case ExpressionType.Conditional: + { + var children = GetChildren(idx); + return LExpr.Condition(ReadExpression(children[0]), ReadExpression(children[1]), ReadExpression(children[2]), node.Type); + } + case ExpressionType.Loop: + { + var children = GetChildren(idx); + var childIdx = 1; + var breakLabel = node.HasFlag(LoopHasBreakFlag) ? ReadLabelTarget(children[childIdx++]) : null; + var continueLabel = node.HasFlag(LoopHasContinueFlag) ? ReadLabelTarget(children[childIdx]) : null; + return LExpr.Loop(ReadExpression(children[0]), breakLabel, continueLabel); + } + case ExpressionType.Goto: + { + var children = GetChildren(idx); + var value = children.Count > 1 ? ReadExpression(children[1]) : null; + return LExpr.MakeGoto((GotoExpressionKind)node.Flags, ReadLabelTarget(children[0]), value, node.Type); + } + case ExpressionType.Label: + { + var children = GetChildren(idx); + var defaultValue = children.Count > 1 ? ReadExpression(children[1]) : null; + return LExpr.Label(ReadLabelTarget(children[0]), defaultValue); + } + case ExpressionType.Switch: + { + // Layout: switchValue, optional defaultBody, optional SwitchCases group. + var children = GetChildren(idx); + var hasDefault = node.HasFlag(SwitchHasDefaultFlag); + var defaultBody = hasDefault ? ReadExpression(children[1]) : null; + var casesIdx = hasDefault ? 2 : 1; + var caseIdxs = children.Count > casesIdx ? GetChildren(children[casesIdx]) : default; + var cases = new LSwitchCase[caseIdxs.Count]; + for (var i = 0; i < cases.Length; ++i) + cases[i] = ReadSwitchCase(caseIdxs[i]); + return LExpr.Switch(node.Type, ReadExpression(children[0]), defaultBody, (MethodInfo)node.Obj, cases); + } + case ExpressionType.Try: + { + // Layout: body, optional finally/fault, then CatchBlock handlers as direct children. + var children = GetChildren(idx); + if (node.HasFlag(TryFaultFlag)) + return LExpr.TryFault(ReadExpression(children[0]), ReadExpression(children[1])); + + var hasFinally = node.HasFlag(TryHasFinallyFlag); + var @finally = hasFinally ? ReadExpression(children[1]) : null; + var handlerStart = hasFinally ? 2 : 1; + + var handlers = new LCatchBlock[children.Count - handlerStart]; + for (var i = 0; i < handlers.Length; ++i) + handlers[i] = ReadCatchBlock(children[handlerStart + i]); + return LExpr.TryCatchFinally(ReadExpression(children[0]), @finally, handlers); + } + case ExpressionType.MemberInit: + { + var children = GetChildren(idx); + var bindings = new LMemberBinding[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + bindings[i - 1] = ReadMemberBinding(children[i]); + return LExpr.MemberInit((LightExpression.NewExpression)ReadExpression(children[0]), bindings); + } + case ExpressionType.ListInit: + { + var children = GetChildren(idx); + var initializers = new LElementInit[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + initializers[i - 1] = ReadElementInit(children[i]); + return LExpr.ListInit((LightExpression.NewExpression)ReadExpression(children[0]), initializers); + } + case ExpressionType.TypeIs: + return LExpr.TypeIs(ReadExpression(GetChildren(idx)[0]), (Type)node.Obj); + case ExpressionType.TypeEqual: + return LExpr.TypeEqual(ReadExpression(GetChildren(idx)[0]), (Type)node.Obj); + case ExpressionType.Dynamic: + { + var children = GetChildren(idx); + var delegateType = (Type)ReadObjectReference(children[0]); + var arguments = new LExpr[children.Count - 1]; + for (var i = 1; i < children.Count; ++i) + arguments[i - 1] = ReadExpression(children[i]); + return new LightExpression.DynamicExpression(delegateType, (System.Runtime.CompilerServices.CallSiteBinder)node.Obj, arguments); + } + case ExpressionType.RuntimeVariables: + { + var children = GetChildren(idx); + var variables = new LParameter[children.Count]; + for (var i = 0; i < children.Count; ++i) + variables[i] = (LParameter)ReadExpression(children[i]); + return new LightExpression.RuntimeVariablesExpression(variables); + } + case ExpressionType.DebugInfo: + { + var children = GetChildren(idx); + ReadUInt16Pair(children[0], out var startLine, out var startColumn); + ReadUInt16Pair(children[1], out var endLine, out var endColumn); + return LExpr.DebugInfo(LExpr.SymbolDocument((string)node.Obj), startLine, startColumn, endLine, endColumn); + } + default: + if (node.ChildCount == 1) + return LExpr.MakeUnary(node.NodeType, ReadExpression(GetChildren(idx)[0]), node.Type, node.Obj as MethodInfo); + + if (node.ChildCount >= 2) + { + var children = GetChildren(idx); + var conversion = children.Count > 2 ? (LightExpression.LambdaExpression)ReadExpression(children[2]) : null; + return LExpr.MakeBinary(node.NodeType, ReadExpression(children[0]), ReadExpression(children[1]), + node.HasFlag(BinaryLiftedToNullFlag), (MethodInfo)node.Obj, conversion); + } + + throw new NotSupportedException($"Reconstruction of `ExpressionType.{node.NodeType}` is not supported yet."); + } + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LSwitchCase ReadSwitchCase(int idx) + { + Debug.Assert(_tree.Nodes[idx].Is(ExprNodeKind.SwitchCase)); + var children = GetChildren(idx); + var testValues = new LExpr[children.Count - 1]; + for (var i = 0; i < testValues.Length; ++i) + testValues[i] = ReadExpression(children[i]); + return LExpr.SwitchCase(ReadExpression(children[children.Count - 1]), testValues); + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LCatchBlock ReadCatchBlock(int idx) + { + ref var node = ref _tree.Nodes[idx]; + Debug.Assert(node.Is(ExprNodeKind.CatchBlock)); + var children = GetChildren(idx); + var childIdx = 0; + var variable = node.HasFlag(CatchHasVariableFlag) ? (LParameter)ReadExpression(children[childIdx++]) : null; + var body = ReadExpression(children[childIdx++]); + var filter = node.HasFlag(CatchHasFilterFlag) ? ReadExpression(children[childIdx]) : null; + return LExpr.MakeCatchBlock(node.Type, variable, body, filter); + } + + private LLabelTarget ReadLabelTarget(int idx) + { + ref var node = ref _tree.Nodes[idx]; + Debug.Assert(node.Is(ExprNodeKind.LabelTarget)); + ref var label = ref _labelsById.Map.AddOrGetValueRef(node.ChildIdx, out var found); + return found ? label : label = LExpr.Label(node.Type, (string)node.Obj); + } + + private object ReadObjectReference(int idx) + { + Debug.Assert(_tree.Nodes[idx].Is(ExprNodeKind.ObjectReference)); + return _tree.Nodes[idx].Obj; + } + + private void ReadUInt16Pair(int idx, out int first, out int second) + { + ref var node = ref _tree.Nodes[idx]; + Debug.Assert(node.Is(ExprNodeKind.UInt16Pair)); + first = node.ChildIdx; + second = node.ChildCount; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LMemberBinding ReadMemberBinding(int idx) + { + ref var node = ref _tree.Nodes[idx]; + var member = (MemberInfo)node.Obj; + switch (node.Kind) + { + case ExprNodeKind.MemberAssignment: + return LExpr.Bind(member, ReadExpression(GetChildren(idx)[0])); + case ExprNodeKind.MemberMemberBinding: + { + var childIdxs = GetChildren(idx); + var bindings = new LMemberBinding[childIdxs.Count]; + for (var i = 0; i < childIdxs.Count; ++i) + bindings[i] = ReadMemberBinding(childIdxs[i]); + return LExpr.MemberBind(member, bindings); + } + case ExprNodeKind.MemberListBinding: + { + var childIdxs = GetChildren(idx); + var initializers = new LElementInit[childIdxs.Count]; + for (var i = 0; i < childIdxs.Count; ++i) + initializers[i] = ReadElementInit(childIdxs[i]); + return LExpr.ListBind(member, initializers); + } + default: + throw new InvalidOperationException($"Node at idx {idx} is not a member binding node."); + } + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LElementInit ReadElementInit(int idx) + { + Debug.Assert(_tree.Nodes[idx].Is(ExprNodeKind.ElementInit)); + return LExpr.ElementInit((MethodInfo)_tree.Nodes[idx].Obj, ReadExpressions(GetChildren(idx))); + } + + private ChildIdxs GetChildren(int idx) => ExprTree.GetChildren(_tree, idx); + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private LExpr[] ReadExpressions(in ChildIdxs childIdxs) + { + var expressions = new LExpr[childIdxs.Count]; + for (var i = 0; i < expressions.Length; ++i) + expressions[i] = ReadExpression(childIdxs[i]); + return expressions; + } + } +} diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 40d6466d4..7db7f28e8 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -97,8 +97,10 @@ void Time(string name, int repeat, Action body) }); Time("compile: flat native (collect+emit)", reps, i => flats[i].TryCompileFlat>()); Time("compile: light FEC (collect+emit)", reps, i => lights[i].CompileFast(ifFastFailedReturnNull: true)); - Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpression()).CompileFast>(true)); - Time("convert only: flat->sys->light", reps, i => flats[i].ToLightExpression()); + Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpressionViaSystem()).CompileFast>(true)); + Time("convert only: flat->sys->light", reps, i => flats[i].ToLightExpressionViaSystem()); + Time("convert only: flat->light direct", reps, i => flats[i].ToLightExpression()); + Time("compile: flat->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpression()).CompileFast>(true)); Time("compile: System.Compile", 1, i => lights[i].CompileSys()); } @@ -174,6 +176,7 @@ private static bool CheckOptionalChildren(int bits) var back2 = ((System.Linq.Expressions.Expression>)fe.ToExpression()).ToFlatExpression(); Ensure(fe.Equals(back2), "structural roundtrip: Equals"); Ensure(fe.GetHashCode() == back2.GetHashCode(), "structural roundtrip: GetHashCode"); + Ensure(fe.ToLightExpressionViaSystem().ToFlatExpression().Equals(fe.ToLightExpression().ToFlatExpression()), "direct flat->light equals via-system"); int Count(Func match) { From e7754c61d509c1eaf49301d4e084f66d847a2566 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 22:25:43 +0100 Subject: [PATCH 06/19] Direct Light -> Flat; fix Light loop dropping the continue label without a break label Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 8 +- .../FlatExpression.cs | 22 +- .../FlatFromLightExpression.cs | 279 ++++++++++++++++++ .../LightExpressionPropertyTests.cs | 8 +- 4 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index e459599b3..921ddeda8 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -10,9 +10,11 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. convert only 43.6us -> 15.0us; flat->light->FEC 45us vs 108us via System (same run). Verified: all tests, the generated-tree property test, and the optional-children fuzzer (direct == via-system structurally, compiles and agrees with System). Cost: ~330 duplicated lines (the two builders must be kept in sync). `GetChildren`/`ReadInlineValue` are shared statics on `ExprTree`. -2. `todo` Light -> Flat directly (now Light -> Sys -> Flat) and Sys -> Flat (exists). - Finding: `fe.Equals(fe.ToLightExpression().ToFlatExpression())` is false for the optional-children fuzz tree even via System, - so Light -> Sys -> Flat is not structurally faithful there; investigate when writing the direct Light -> Flat. +2. `tried: done` Light -> Flat directly (`FlatFromLightExpression.cs`, mirrors `AddSysExpression`); Sys -> Flat already existed. + convert only 35.8us (via System) -> 14.6us (direct). `FromLightExprViaSystem` and `ToLightExpressionViaSystem` are kept as oracles. + Found and fixed a real bug on the way: Light `LoopExpression.CreateSysExpression` dropped the continue label when there was no + break label (Expression.cs); the optional-children fuzzer exposed it as a flat->light->flat mismatch in 80 of 300 trees. + The fuzzer now asserts flat->light->flat structural equality and direct == via-system. 3. `todo` Widen the native emitter subset node by node, fuzzer guards each step. ## Construction-time ideas diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 155dedf97..787c65b4d 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -889,15 +889,21 @@ public ushort FromSysExpr(SysExpr expr) return (ushort)RootIdx; } - // @perf remove Light -> System -> Flat round trip => make it Light -> Flat. - /// Flattens a LightExpression into this tree and sets . + /// Flattens a LightExpression into this tree directly, without the System.Linq expressions in between, + /// and sets . Mirrors AddSysExpression: keep the two in sync. [RequiresUnreferencedCode(LightExpression.Trimming.Message)] public ushort FromLightExpr(LightExpression.Expression expr) { - // Prefer Sys round-trip for a single From core; Light→Sys preserves identity via ToExpression. - return FromSysExpr(expr.ToExpression()); + _parameterIds = default; + _labelIds = default; + RootIdx = AddLightExpression(expr); + return (ushort)RootIdx; } + /// The slower oracle of : Light -> System -> Flat. + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + public ushort FromLightExprViaSystem(LightExpression.Expression expr) => FromSysExpr(expr.ToExpression()); + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] private ushort AddSysExpression(SysExpr expr) { @@ -1128,17 +1134,19 @@ private ushort AddSysExpression(SysExpr expr) } } - private ushort AddSysLabelTarget(SysLabelTarget target) + private ushort AddSysLabelTarget(SysLabelTarget target) => AddLabelTarget(target, target.Type, target.Name); + + private ushort AddLabelTarget(object target, Type type, string name) { var id = checked((ushort)GetId(ref _labelIds, target)); - // Reuse existing label-target node with the same id when the same SysLabelTarget is seen again. + // Reuse existing label-target node with the same id when the same label target is seen again. for (var i = 0; i < Nodes.Count; ++i) { ref var n = ref Nodes.GetSurePresentRef(i); if (n.Is(ExprNodeKind.LabelTarget) && n.ChildIdx == id) return checked((ushort)i); } - return LabelTargetWithId(target.Type, target.Name, id); + return LabelTargetWithId(type, name, id); } [RequiresUnreferencedCode(LightExpression.Trimming.Message)] diff --git a/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs new file mode 100644 index 000000000..073ff73c9 --- /dev/null +++ b/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs @@ -0,0 +1,279 @@ +#nullable disable +namespace FastExpressionCompiler.FlatExpression; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using FastExpressionCompiler.LightExpression.ImTools; +using LExpr = FastExpressionCompiler.LightExpression.Expression; +using LParameter = FastExpressionCompiler.LightExpression.ParameterExpression; +using LMemberBinding = FastExpressionCompiler.LightExpression.MemberBinding; +using LElementInit = FastExpressionCompiler.LightExpression.ElementInit; + +public partial struct ExprTree +{ + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort[] AddLightArguments(LightExpression.IArgumentProvider args) + { + var result = new ushort[args.ArgumentCount]; + for (var i = 0; i < result.Length; ++i) + result[i] = AddLightExpression(args.GetArgument(i)); + return result; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort[] AddLightExpressions(System.Collections.Generic.IReadOnlyList exprs) + { + var result = new ushort[exprs.Count]; + for (var i = 0; i < result.Length; ++i) + result[i] = AddLightExpression(exprs[i]); + return result; + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort AddLightExpression(LExpr expr) + { + switch (expr.NodeType) + { + case ExpressionType.Constant: + return Constant(((LightExpression.ConstantExpression)expr).Value, expr.Type); + + case ExpressionType.Default: + return Default(expr.Type); + + case ExpressionType.Parameter: + { + var parameter = (LParameter)expr; + var id = checked((ushort)GetId(ref _parameterIds, parameter)); + return ParameterWithId(parameter.IsByRef ? parameter.Type : expr.Type, parameter.Name, id); + } + case ExpressionType.Lambda: + { + var lambda = (LightExpression.LambdaExpression)expr; + var pars = new ushort[lambda.ParameterCount]; + for (var i = 0; i < pars.Length; ++i) + pars[i] = AddLightExpression(lambda.GetParameter(i)); + return Lambda(expr.Type, AddLightExpression(lambda.Body), pars); + } + case ExpressionType.Block: + { + var block = (LightExpression.BlockExpression)expr; + ushort[] vars = null; + if (block.Variables.Count != 0) + { + vars = new ushort[block.Variables.Count]; + for (var i = 0; i < vars.Length; ++i) + vars[i] = AddLightExpression(block.Variables[i]); + } + var exprs = new ushort[block.Expressions.Count]; + for (var i = 0; i < exprs.Length; ++i) + exprs[i] = AddLightExpression(block.Expressions[i]); + return Block(expr.Type, vars, exprs); + } + case ExpressionType.MemberAccess: + { + var member = (LightExpression.MemberExpression)expr; + return member.Expression != null + ? MakeMemberAccess(AddLightExpression(member.Expression), member.Member) + : MakeMemberAccess(member.Member); + } + case ExpressionType.Call: + { + var call = (LightExpression.MethodCallExpression)expr; + var args = AddLightArguments(call); + return call.Object != null + ? Call(AddLightExpression(call.Object), call.Method, args) + : Call(call.Method, args); + } + case ExpressionType.New: + { + var @new = (LightExpression.NewExpression)expr; + if (@new.Constructor == null) + return New(expr.Type); + if (@new.ArgumentCount == 0) + return New(@new.Constructor); + return New(@new.Constructor, AddLightArguments(@new)); + } + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + { + var items = AddLightArguments((LightExpression.NewArrayExpression)expr); + return expr.NodeType == ExpressionType.NewArrayInit + ? NewArrayInit(expr.Type.GetElementType(), items) + : NewArrayBounds(expr.Type.GetElementType(), items); + } + case ExpressionType.Invoke: + { + var invoke = (LightExpression.InvocationExpression)expr; + var args = AddLightArguments(invoke); + return Invoke(AddLightExpression(invoke.Expression), args); + } + case ExpressionType.Index: + { + var indexExpr = (LightExpression.IndexExpression)expr; + var args = AddLightArguments(indexExpr); + var instance = indexExpr.Object != null ? AddLightExpression(indexExpr.Object) : (ushort)0; + return indexExpr.Indexer != null + ? Property(instance, indexExpr.Indexer, args) + : ArrayAccess(instance, args); + } + case ExpressionType.Conditional: + { + var conditional = (LightExpression.ConditionalExpression)expr; + return Condition( + AddLightExpression(conditional.Test), + AddLightExpression(conditional.IfTrue), + AddLightExpression(conditional.IfFalse), + expr.Type); + } + case ExpressionType.Loop: + { + var loop = (LightExpression.LoopExpression)expr; + return Loop( + AddLightExpression(loop.Body), + loop.BreakLabel != null ? AddLightLabelTarget(loop.BreakLabel) : (ushort)0, + loop.ContinueLabel != null ? AddLightLabelTarget(loop.ContinueLabel) : (ushort)0); + } + case ExpressionType.Goto: + { + var @goto = (LightExpression.GotoExpression)expr; + return MakeGoto(@goto.Kind, AddLightLabelTarget(@goto.Target), + @goto.Value != null ? AddLightExpression(@goto.Value) : (ushort)0, expr.Type); + } + case ExpressionType.Label: + { + var label = (LightExpression.LabelExpression)expr; + return Label(AddLightLabelTarget(label.Target), + label.DefaultValue != null ? AddLightExpression(label.DefaultValue) : (ushort)0); + } + case ExpressionType.Switch: + { + var @switch = (LightExpression.SwitchExpression)expr; + var cases = new ushort[@switch.Cases.Count]; + for (var i = 0; i < cases.Length; ++i) + { + var sc = @switch.Cases[i]; + var tests = AddLightExpressions(sc.TestValues); + cases[i] = SwitchCase(AddLightExpression(sc.Body), tests); + } + return Switch(expr.Type, AddLightExpression(@switch.SwitchValue), + @switch.DefaultBody != null ? AddLightExpression(@switch.DefaultBody) : (ushort)0, + @switch.Comparison, cases); + } + case ExpressionType.Try: + { + var @try = (LightExpression.TryExpression)expr; + if (@try.Fault != null) + return TryFault(AddLightExpression(@try.Body), AddLightExpression(@try.Fault)); + + var handlerCount = @try.Handlers?.Count ?? 0; + var handlers = new ushort[handlerCount]; + for (var i = 0; i < handlers.Length; ++i) + { + var h = @try.Handlers[i]; + var variable = h.Variable != null ? AddLightExpression(h.Variable) : (ushort)0; + var filter = h.Filter != null ? AddLightExpression(h.Filter) : (ushort)0; + handlers[i] = MakeCatchBlock(h.Test, variable, AddLightExpression(h.Body), filter); + } + + if (@try.Finally != null) + return handlers.Length != 0 + ? TryCatchFinally(AddLightExpression(@try.Body), AddLightExpression(@try.Finally), handlers) + : TryFinally(AddLightExpression(@try.Body), AddLightExpression(@try.Finally)); + + return TryCatch(AddLightExpression(@try.Body), handlers); + } + case ExpressionType.MemberInit: + { + var memberInit = (LightExpression.MemberInitExpression)expr; + var bindings = new ushort[memberInit.Bindings.Count]; + for (var i = 0; i < bindings.Length; ++i) + bindings[i] = AddLightMemberBinding(memberInit.Bindings[i]); + return MemberInit(AddLightExpression(memberInit.NewExpression), bindings); + } + case ExpressionType.ListInit: + { + var listInit = (LightExpression.ListInitExpression)expr; + var inits = new ushort[listInit.Initializers.Count]; + for (var i = 0; i < inits.Length; ++i) + inits[i] = AddLightElementInit(listInit.Initializers[i]); + return ListInit(AddLightExpression(listInit.NewExpression), inits); + } + case ExpressionType.TypeIs: + { + var typeBinary = (LightExpression.TypeBinaryExpression)expr; + return TypeIs(AddLightExpression(typeBinary.Expression), typeBinary.TypeOperand); + } + case ExpressionType.TypeEqual: + { + var typeBinary = (LightExpression.TypeBinaryExpression)expr; + return TypeEqual(AddLightExpression(typeBinary.Expression), typeBinary.TypeOperand); + } + case ExpressionType.Dynamic: + { + var dynamic = (LightExpression.DynamicExpression)expr; + return Dynamic(dynamic.DelegateType, dynamic.Binder, AddLightExpressions(dynamic.Arguments)); + } + case ExpressionType.RuntimeVariables: + { + var runtime = (LightExpression.RuntimeVariablesExpression)expr; + var vars = new ushort[runtime.Variables.Count]; + for (var i = 0; i < vars.Length; ++i) + vars[i] = AddLightExpression(runtime.Variables[i]); + return RuntimeVariables(vars); + } + case ExpressionType.DebugInfo: + { + var debug = (LightExpression.DebugInfoExpression)expr; + return DebugInfo(debug.Document.FileName, debug.StartLine, debug.StartColumn, debug.EndLine, debug.EndColumn); + } + default: + if (expr is LightExpression.UnaryExpression unary) + return MakeUnary(expr.NodeType, AddLightExpression(unary.Operand), expr.Type, unary.Method); + + if (expr is LightExpression.BinaryExpression binary) + return MakeBinary(expr.NodeType, + AddLightExpression(binary.Left), + AddLightExpression(binary.Right), + binary.IsLiftedToNull, + binary.Method, + binary.Conversion != null ? AddLightExpression(binary.Conversion) : (ushort)0, + expr.Type); + + throw new NotSupportedException($"Flattening of `ExpressionType.{expr.NodeType}` is not supported yet."); + } + } + + private ushort AddLightLabelTarget(LightExpression.LabelTarget target) => AddLabelTarget(target, target.Type, target.Name); + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort AddLightMemberBinding(LMemberBinding binding) + { + switch (binding.BindingType) + { + case MemberBindingType.Assignment: + return Bind(binding.Member, AddLightExpression(((LightExpression.MemberAssignment)binding).Expression)); + case MemberBindingType.MemberBinding: + { + var memberBinding = (LightExpression.MemberMemberBinding)binding; + var bindings = new ushort[memberBinding.Bindings.Count]; + for (var i = 0; i < bindings.Length; ++i) + bindings[i] = AddLightMemberBinding(memberBinding.Bindings[i]); + return MemberBind(binding.Member, bindings); + } + case MemberBindingType.ListBinding: + { + var listBinding = (LightExpression.MemberListBinding)binding; + var inits = new ushort[listBinding.Initializers.Count]; + for (var i = 0; i < inits.Length; ++i) + inits[i] = AddLightElementInit(listBinding.Initializers[i]); + return ListBind(binding.Member, inits); + } + default: + throw new NotSupportedException($"Flattening of member binding `{binding.BindingType}` is not supported yet."); + } + } + + [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + private ushort AddLightElementInit(LElementInit init) => ElementInit(init.AddMethod, AddLightArguments(init)); +} diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 7db7f28e8..24b17ca24 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -100,6 +100,8 @@ void Time(string name, int repeat, Action body) Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpressionViaSystem()).CompileFast>(true)); Time("convert only: flat->sys->light", reps, i => flats[i].ToLightExpressionViaSystem()); Time("convert only: flat->light direct", reps, i => flats[i].ToLightExpression()); + Time("convert only: light->sys->flat", reps, i => { var t = default(ExprTree); t.FromLightExprViaSystem(lights[i]); }); + Time("convert only: light->flat direct", reps, i => { var t = default(ExprTree); t.FromLightExpr(lights[i]); }); Time("compile: flat->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpression()).CompileFast>(true)); Time("compile: System.Compile", 1, i => lights[i].CompileSys()); } @@ -176,7 +178,11 @@ private static bool CheckOptionalChildren(int bits) var back2 = ((System.Linq.Expressions.Expression>)fe.ToExpression()).ToFlatExpression(); Ensure(fe.Equals(back2), "structural roundtrip: Equals"); Ensure(fe.GetHashCode() == back2.GetHashCode(), "structural roundtrip: GetHashCode"); - Ensure(fe.ToLightExpressionViaSystem().ToFlatExpression().Equals(fe.ToLightExpression().ToFlatExpression()), "direct flat->light equals via-system"); + var light = fe.ToLightExpression(); + Ensure(fe.Equals(light.ToFlatExpression()), "flat->light->flat roundtrip"); + var viaSys = default(ExprTree); + viaSys.FromLightExprViaSystem(light); + Ensure(viaSys.Equals(light.ToFlatExpression()), "direct light->flat equals via-system"); int Count(Func match) { From 38ef227130ddf899cb4a56d01dc175d6c02aaf6c Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 22:36:29 +0100 Subject: [PATCH 07/19] Flat: README expressions test and timing, notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 6 +- .../LightExpressionPropertyTests.cs | 57 +++++++++++++++++++ .../LightExpressionTests.cs | 1 + 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 921ddeda8..a3096846a 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -15,7 +15,11 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. Found and fixed a real bug on the way: Light `LoopExpression.CreateSysExpression` dropped the continue label when there was no break label (Expression.cs); the optional-children fuzzer exposed it as a flat->light->flat mismatch in 80 of 300 trees. The fuzzer now asserts flat->light->flat structural equality and direct == via-system. -3. `todo` Widen the native emitter subset node by node, fuzzer guards each step. +3. `tried: measured` README expressions (`new X(closure a, b)`, `n => () => n + 1`) built flat (`Flat_compiles_the_readme_expressions_and_compares_the_time`). + Both are outside the native subset (closure constant, nested lambda, New), so `CompileFast` = probe (0.1-0.3us) + direct flat->light (~2-4us) + Light FEC (~8us / ~47us). + The conversion is a 25-40% overhead on top of Light for these; single noisy runs, the order of measurements moves numbers by 2x. + Conclusion: the native subset must grow to closures/New/nested lambdas before flat beats Light on the README cases. +4. `todo` Widen the native emitter subset node by node, fuzzer guards each step. Next: New + Call + Member (no closure), then closure constants. ## Construction-time ideas - `todo` Profile node access: the scan is ~45ns/node, far from memory bound. Suspect the Stack32 indexer / Type compares. diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 24b17ca24..990b9e888 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -106,6 +106,62 @@ void Time(string name, int repeat, Action body) Time("compile: System.Compile", 1, i => lights[i].CompileSys()); } + public class ReadmeA { } + public class ReadmeB { } + public class ReadmeX + { + public ReadmeA A; public ReadmeB B; + public ReadmeX(ReadmeA a, ReadmeB b) { A = a; B = b; } + } + + /// The README expressions (new X(closure a, b) and n => () => n + 1) built flat: the results must match Light and the phases are timed. + public void Flat_compiles_the_readme_expressions_and_compares_the_time() + { + var a = new ReadmeA(); + var ctor = typeof(ReadmeX).GetConstructors()[0]; + + var fe1 = default(ExprTree); + var b1 = fe1.ParameterOf("b"); + fe1.RootIdx = fe1.Lambda>(fe1.New(ctor, fe1.Constant(a, typeof(ReadmeA)), b1), b1); + + var fe2 = default(ExprTree); + var n2 = fe2.ParameterOf("n"); + fe2.RootIdx = fe2.Lambda>>( + fe2.Lambda>(fe2.MakeBinary(ExpressionType.Add, n2, fe2.ConstantInt(1))), n2); + + var bp = Parameter(typeof(ReadmeB), "b"); + var l1 = Lambda>(New(ctor, Constant(a, typeof(ReadmeA)), bp), bp); + var np = Parameter(typeof(int), "n"); + var l2 = Lambda>>(Lambda>(Add(np, Constant(1))), np); + + var bb = new ReadmeB(); + var x = fe1.CompileFast>()(bb); + Ensure(x.A == a && x.B == bb, "readme 1 result"); + Ensure(fe2.CompileFast>>()(41)() == 42, "readme 2 result"); + Ensure(((FastExpressionCompiler.LightExpression.LambdaExpression)fe1.ToLightExpression()).CompileFast>(true)(bb).A == a, "readme 1 direct"); + + const int reps = 2000; + void Time(string name, Action body) + { + for (var i = 0; i < 100; ++i) body(); + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (var i = 0; i < reps; ++i) body(); + Console.WriteLine($"//flat-readme {name,-36} {sw.Elapsed.TotalMilliseconds * 1000 / reps,9:F2} us"); + } + + Time("1 light FEC", () => l1.CompileFast(true)); + Time("1 flat->light->FEC (CompileFast)", () => fe1.CompileFast>()); + Time("1 flat->light only", () => fe1.ToLightExpression()); + var l1f = (FastExpressionCompiler.LightExpression.LambdaExpression)fe1.ToLightExpression(); + Time("1 prebuilt flat-made light FEC", () => l1f.CompileFast>(true)); + Time("1 prebuilt light FEC (generic)", () => l1.CompileFast>(true)); + Time("1 flat native probe (falls back)", () => fe1.TryCompileFlat>()); + Time("2 light FEC", () => l2.CompileFast(true)); + Time("2 flat->light->FEC (CompileFast)", () => fe2.CompileFast>>()); + Time("2 flat->light only", () => fe2.ToLightExpression()); + Time("2 flat native probe (falls back)", () => fe2.TryCompileFlat>>()); + } + /// Fuzzes presence/absence of every optional child (switch default/cases, try finally/catch, catch variable/filter, /// loop break/continue, goto value, label default): the bits decide what is present; the flat tree must report exactly that. public void Can_fuzz_optional_children_presence_in_flat_expression() @@ -507,5 +563,6 @@ public void Can_fuzz_optional_children_presence_in_flat_expression() { } public void Can_compile_generated_flat_expression_natively() { } public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() { } public void Experiment_compare_flat_and_light_compile_phases() { } + public void Flat_compiles_the_readme_expressions_and_compares_the_time() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index fbdcb29ca..382860478 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -41,6 +41,7 @@ public int Run() Can_compile_generated_flat_expression_natively(); Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); Experiment_compare_flat_and_light_compile_phases(); + Flat_compiles_the_readme_expressions_and_compares_the_time(); Flat_lambda_parameter_ref_before_decl_preserves_identity(); Flat_lambda_multiple_parameter_refs_all_yield_same_identity(); From a9ac2f1c4efb7960ae8edfc48389bbbc08a2b439 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 22:41:56 +0100 Subject: [PATCH 08/19] Flat native compile: New, Call, members, reference types, closure constants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 7 +- .../FlatExpression.cs | 2 +- .../FlatExpressionCompiler.cs | 165 ++++++++++++++++-- .../LightExpressionPropertyTests.cs | 74 +++++++- .../LightExpressionTests.cs | 1 + 5 files changed, 231 insertions(+), 18 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index a3096846a..60dbfbc38 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -19,7 +19,12 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. Both are outside the native subset (closure constant, nested lambda, New), so `CompileFast` = probe (0.1-0.3us) + direct flat->light (~2-4us) + Light FEC (~8us / ~47us). The conversion is a 25-40% overhead on top of Light for these; single noisy runs, the order of measurements moves numbers by 2x. Conclusion: the native subset must grow to closures/New/nested lambdas before flat beats Light on the README cases. -4. `todo` Widen the native emitter subset node by node, fuzzer guards each step. Next: New + Call + Member (no closure), then closure constants. +4. `tried: done` Widened the native subset: reference-typed params/locals/results, `New`, static+instance `Call`, Field/Property getter, + closure constants (the `ArrayClosure` array is filled from `ClosureConstants`), string and null constants. The collect checks by construction what IL needs + (`FlatFits`: same type or reference upcast, args count, assign target is a parameter, lambda param types == delegate param types). + README 1 (`new X(closure a, b)`): light FEC 5.2us -> flat native 3.2us (Release, one noisy run); README 2 (nested lambda) still falls back. + Cost: ~90 lines in FlatExpressionCompiler.cs; `GetParameters()` allocates per Call/New in the collect (todo @perf). +5. `todo` Nested lambdas (closure with nested-lambda slots), Convert, value types (struct params/Nullable), Invoke, arrays, loops/labels/try. Each with a fuzz or deterministic test. ## Construction-time ideas - `todo` Profile node access: the scan is ~45ns/node, far from memory bound. Suspect the Stack32 indexer / Type compares. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 787c65b4d..fc5bf5060 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -170,7 +170,7 @@ internal bool HasSameShape(ref ExprNode other) => /// Stores an expression tree as flat nodes plus separate closure constants. public partial struct ExprTree : IEquatable { - private static readonly object ClosureConstantMarker = new(); + internal static readonly object ClosureConstantMarker = new(); private const byte ParameterByRefFlag = 1; private const byte BinaryLiftedToNullFlag = 1; private const byte LoopHasBreakFlag = 1; diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index 2d401433e..e8b6bc65a 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -3,6 +3,7 @@ namespace FastExpressionCompiler.LightExpression; using System; +using System.Reflection; using System.Reflection.Emit; using FastExpressionCompiler.FlatExpression; using FastExpressionCompiler.LightExpression.ImTools; @@ -24,6 +25,38 @@ struct FlatVar public LocalBuilder Local; } + private static bool IsFlatType(Type t) => t == typeof(int) || t == typeof(bool) || !t.IsValueType && !t.IsByRef && !t.IsPointer; + + // IL needs no conversion only for the same type or the reference upcast + private static bool FlatFits(Type to, Type from) => to == from || !to.IsValueType && !from.IsValueType && to.IsAssignableFrom(from); + + private static bool FlatArgsFit(ref ExprTree tree, int idx, int count, ParameterInfo[] pars) + { + if (pars.Length != count) return false; // todo: @perf GetParameters allocates, cache or compare lazily + for (var i = 0; i < count; ++i) + { + ref var arg = ref tree.Nodes.GetSurePresentRef(idx); + if (!FlatFits(pars[i].ParameterType, arg.Type)) return false; + idx = arg.NextIdx; + } + return true; + } + + private static bool FlatCallFits(ref ExprTree tree, ref ExprNode n, MethodInfo method) + { + var idx = n.ChildIdx; + var count = (int)n.ChildCount; + if (!method.IsStatic) + { + if (count == 0) return false; + ref var instance = ref tree.Nodes.GetSurePresentRef(idx); + if (!FlatFits(method.DeclaringType, instance.Type)) return false; + idx = instance.NextIdx; + --count; + } + return !method.IsGenericMethodDefinition && FlatArgsFit(ref tree, idx, count, method.GetParameters()); + } + /// The collect phase on the flat nodes: one linear pass over the contiguous array, no recursion, no stack, no per-node casts. /// What the tree construction already guarantees is not checked here: the child counts, the not-null children, the parameter /// and label ids, the closure constants (they are in the separate list, so `IsClosureBoundConstant` is already answered). @@ -31,7 +64,7 @@ struct FlatVar public static Result TryCollectFlatInfo(this ref ExprTree tree) { var count = tree.Nodes.Count; - if (count < 2 || tree.ClosureConstants.Count != 0) + if (count < 2) return Result.NotSupported_UnknownExpression; var lambdas = 0; @@ -49,18 +82,58 @@ public static Result TryCollectFlatInfo(this ref ExprTree tree) case ExprType.Add: case ExprType.Subtract: case ExprType.Multiply: + if (t != typeof(int)) return Result.NotSupported_UnknownExpression; + break; case ExprType.Conditional: - case ExprType.Assign: case ExprType.Block: - if (t != typeof(int)) return Result.NotSupported_UnknownExpression; + if (!IsFlatType(t)) return Result.NotSupported_UnknownExpression; break; + case ExprType.Assign: + { + ref var left = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); + if (!IsFlatType(t) || left.NodeType != ExprType.Parameter || + !FlatFits(left.Type, tree.Nodes.GetSurePresentRef(left.NextIdx).Type)) + return Result.NotSupported_UnknownExpression; + break; + } case ExprType.Parameter: - if (t != typeof(int) || n.Flags != 0) return Result.NotSupported_UnknownExpression; + if (!IsFlatType(t) || n.Flags != 0) return Result.NotSupported_UnknownExpression; break; - case ExprType.Constant: - if (!ReferenceEquals(n.Obj, ExprNode.InlineValueMarker) || t != typeof(int) && t != typeof(bool)) + case ExprType.New: + if (!IsFlatType(t) || n.Obj is not ConstructorInfo ctor || + !FlatArgsFit(ref tree, n.ChildIdx, n.ChildCount, ctor.GetParameters())) return Result.NotSupported_UnknownExpression; break; + case ExprType.Call: + { + if (!IsFlatType(t) || n.Obj is not MethodInfo method) return Result.NotSupported_UnknownExpression; + if (!FlatCallFits(ref tree, ref n, method)) return Result.NotSupported_UnknownExpression; + break; + } + case ExprType.MemberAccess: + { + MethodInfo getter = null; + var isStatic = false; + if (n.Obj is FieldInfo field && !field.IsLiteral) + isStatic = field.IsStatic; + else if (n.Obj is PropertyInfo prop && (getter = prop.GetGetMethod(true)) != null) + isStatic = getter.IsStatic; + else + return Result.NotSupported_UnknownExpression; + var declaringType = ((MemberInfo)n.Obj).DeclaringType; + if (!IsFlatType(t) || isStatic != (n.ChildCount == 0) || + !isStatic && !FlatFits(declaringType, tree.Nodes.GetSurePresentRef(n.ChildIdx).Type)) + return Result.NotSupported_UnknownExpression; + break; + } + case ExprType.Constant: + { + var isInline = ReferenceEquals(n.Obj, ExprNode.InlineValueMarker); + if (isInline ? t != typeof(int) && t != typeof(bool) + : t.IsValueType || !(ReferenceEquals(n.Obj, ExprTree.ClosureConstantMarker) || n.Obj == null || n.Obj is string && t == typeof(string))) + return Result.NotSupported_UnknownExpression; + break; + } case ExprType.Equal: case ExprType.GreaterThan: if (tree.Nodes.GetSurePresentRef(n.ChildIdx).Type != typeof(int)) return Result.NotSupported_UnknownExpression; @@ -93,16 +166,28 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where var bodyIdx = lambda.ChildIdx; var returnType = tree.Nodes.GetSurePresentRef(bodyIdx).Type; - if (returnType != typeof(int) && returnType != typeof(bool)) + var invoke = typeof(TDelegate).GetMethod("Invoke"); + var delegateReturn = invoke?.ReturnType; + if (delegateReturn == null || !IsFlatType(delegateReturn) || !FlatFits(delegateReturn, returnType)) return null; var paramCount = lambda.ChildCount - 1; + var delegatePars = invoke.GetParameters(); + if (delegatePars.Length != paramCount) + return null; var paramTypes = new Type[paramCount + 1]; paramTypes[0] = typeof(ArrayClosure); + var firstParIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; for (var i = 1; i <= paramCount; ++i) - paramTypes[i] = typeof(int); + { + ref var par = ref tree.Nodes.GetSurePresentRef(firstParIdx); + if (par.Type != delegatePars[i - 1].ParameterType) + return null; + paramTypes[i] = par.Type; + firstParIdx = par.NextIdx; + } - var dynMethod = new DynamicMethod(string.Empty, returnType, paramTypes, typeof(ArrayClosure), true); + var dynMethod = new DynamicMethod(string.Empty, delegateReturn, paramTypes, typeof(ArrayClosure), true); var il = dynMethod.GetILGenerator(); var vars = default(SmallList, NoArrayPool>); @@ -116,7 +201,15 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where EmitFlat(ref tree, bodyIdx, il, ref vars, false); il.Emit(OpCodes.Ret); - return (TDelegate)(object)dynMethod.CreateDelegate(typeof(TDelegate), EmptyArrayClosure); + object closure = EmptyArrayClosure; + if (tree.ClosureConstants.Count != 0) + { + var constants = new object[tree.ClosureConstants.Count]; + for (var i = 0; i < constants.Length; ++i) + constants[i] = tree.ClosureConstants[i]; + closure = new ArrayClosure(constants); + } + return (TDelegate)(object)dynMethod.CreateDelegate(typeof(TDelegate), closure); } private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, @@ -127,8 +220,23 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, switch (nodeType) { case ExprType.Constant: - if (!ignoreResult) + if (ignoreResult) + return; + if (ReferenceEquals(n.Obj, ExprNode.InlineValueMarker)) il.Emit(OpCodes.Ldc_I4, (int)n.InlineValue); + else if (ReferenceEquals(n.Obj, ExprTree.ClosureConstantMarker)) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); + il.Emit(OpCodes.Ldc_I4, (int)n.ChildIdx); + il.Emit(OpCodes.Ldelem_Ref); + if (n.Type != typeof(object)) + il.Emit(OpCodes.Castclass, n.Type); + } + else if (n.Obj is string s) + il.Emit(OpCodes.Ldstr, s); + else + il.Emit(OpCodes.Ldnull); return; case ExprType.Parameter: @@ -162,6 +270,39 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, throw new InvalidOperationException("The assigned parameter is not in scope"); } + case ExprType.New: + case ExprType.Call: + { + var argIdx = n.ChildIdx; + for (var i = 0; i < n.ChildCount; ++i) + { + EmitFlat(ref tree, argIdx, il, ref vars, false); + argIdx = tree.Nodes.GetSurePresentRef(argIdx).NextIdx; + } + if (nodeType == ExprType.New) + il.Emit(OpCodes.Newobj, (ConstructorInfo)n.Obj); + else + { + var method = (MethodInfo)n.Obj; + il.Emit(method.IsStatic ? OpCodes.Call : OpCodes.Callvirt, method); + } + break; + } + + case ExprType.MemberAccess: + { + if (n.ChildCount != 0) + EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + if (n.Obj is FieldInfo field) + il.Emit(field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, field); + else + { + var getter = ((PropertyInfo)n.Obj).GetGetMethod(true); + il.Emit(getter.IsStatic ? OpCodes.Call : OpCodes.Callvirt, getter); + } + break; + } + case ExprType.Not: EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); il.Emit(OpCodes.Ldc_I4_0); @@ -212,7 +353,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, for (var i = 1; i < n.ChildCount; ++i) { ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); - vars.Add(new FlatVar { Id = v.ChildIdx, Arg = -1, Local = il.DeclareLocal(typeof(int)) }); + vars.Add(new FlatVar { Id = v.ChildIdx, Arg = -1, Local = il.DeclareLocal(v.Type) }); varIdx = v.NextIdx; } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 990b9e888..f88603a6e 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -58,9 +58,9 @@ public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() var fe2 = default(ExprTree); var p2 = fe2.ParameterOf("p"); - fe2.RootIdx = fe2.Lambda>(fe2.ConstantOf("x"), p2); - Ensure(fe2.TryCompileFlat>() == null, "string is outside the subset"); - Ensure(fe2.CompileFast>()(1) == "x", "fallback result"); + fe2.RootIdx = fe2.Lambda>(fe2.ConstantOf(typeof(string)), p2); + Ensure(fe2.TryCompileFlat>() == null, "the Type constant is outside the subset"); + Ensure(fe2.CompileFast>()(1) == typeof(string), "fallback result"); } /// Prints how long the phases of the compilation take on the flat and on the light trees; asserts nothing but the agreement. @@ -155,13 +155,78 @@ void Time(string name, Action body) var l1f = (FastExpressionCompiler.LightExpression.LambdaExpression)fe1.ToLightExpression(); Time("1 prebuilt flat-made light FEC", () => l1f.CompileFast>(true)); Time("1 prebuilt light FEC (generic)", () => l1.CompileFast>(true)); - Time("1 flat native probe (falls back)", () => fe1.TryCompileFlat>()); + Time("1 flat native (closure const + New)", () => fe1.TryCompileFlat>()); Time("2 light FEC", () => l2.CompileFast(true)); Time("2 flat->light->FEC (CompileFast)", () => fe2.CompileFast>>()); Time("2 flat->light only", () => fe2.ToLightExpression()); Time("2 flat native probe (falls back)", () => fe2.TryCompileFlat>>()); } + public class FlatPoint + { + public int X; + public int Y { get; set; } + public FlatPoint(int x, int y) { X = x; Y = y; } + public FlatPoint Self => this; + public static int Twice(int v) => v * 2; + public int Sum(int k) => X + Y + k; + } + + /// The native subset with the reference types: New, static and instance Call, Field and Property, the reference-typed + /// parameter, local and result; the same tree with a closure constant must fall back. + public void Flat_compile_handles_new_call_and_member_access_natively() + { + var t = typeof(FlatPoint); + var ctor = t.GetConstructor(new[] { typeof(int), typeof(int) }); + var fe = default(ExprTree); + var p = fe.ParameterOf("p"); + var k = fe.ParameterOf("k"); + var q = fe.Variable(t, "q"); + // (p, k) => { var q = new FlatPoint(1, 2); q = p; new FlatPoint(q.Self.X + Twice(k), q.Sum(k)) } + var body = fe.Block(t, new[] { q }, + fe.Assign(q, fe.New(ctor, fe.ConstantInt(1), fe.ConstantInt(2))), + fe.Assign(q, p), + fe.New(ctor, + fe.Add(fe.Field(fe.Property(q, t.GetProperty("Self")), t.GetField("X")), fe.Call(t.GetMethod("Twice"), k)), + fe.Call(q, t.GetMethod("Sum"), k))); + fe.RootIdx = fe.Lambda>(body, p, k); + + var native = fe.TryCompileFlat>(); + Ensure(native != null, "native compile of New/Call/Member"); + var r = native(new FlatPoint(10, 20), 3); + Ensure(r.X == 10 + 6 && r.Y == 10 + 20 + 3, "native result"); + + // closure constant, string and null constants are native too; the nested lambda is outside the subset + var fe2 = default(ExprTree); + var p2 = fe2.ParameterOf("p"); + var held = new FlatPoint(7, 8); + fe2.RootIdx = fe2.Lambda>( + fe2.NewArrayInit(typeof(object), fe2.Constant(held, t), fe2.Constant("s"), fe2.ConstantNull(t), p2), p2); + Ensure(fe2.TryCompileFlat>() == null, "the array init is outside the subset"); + + var fe3 = default(ExprTree); + var p3 = fe3.ParameterOf("p"); + fe3.RootIdx = fe3.Lambda>( + fe3.Add(fe3.Field(fe3.Constant(held, t), t.GetField("X")), + fe3.Call(t.GetMethod("Twice"), fe3.Property(fe3.Constant(held, t), t.GetProperty("Y")))), + p3, fe3.ParameterOf("unused")); + var native3 = fe3.TryCompileFlat>(); + Ensure(native3 != null && native3(null, null) == 7 + 16, "closure constant native"); + + var fe4 = default(ExprTree); + var p4 = fe4.ParameterOf("p"); + fe4.RootIdx = fe4.Lambda>(fe4.Constant(held, t), p4); + Ensure(ReferenceEquals(fe4.TryCompileFlat>()(null), held), "closure constant is the same instance"); + var fe5 = default(ExprTree); + var p5 = fe5.ParameterOf("p"); + fe5.RootIdx = fe5.Lambda>(fe5.Block(fe5.Constant("s"), fe5.Constant("str")), p5); + Ensure(fe5.TryCompileFlat>()(null) == "str", "string constant native"); + var fe6 = default(ExprTree); + var p6 = fe6.ParameterOf("p"); + fe6.RootIdx = fe6.Lambda>(fe6.ConstantNull(t), p6); + Ensure(fe6.TryCompileFlat>()(null) == null, "null constant native"); + } + /// Fuzzes presence/absence of every optional child (switch default/cases, try finally/catch, catch variable/filter, /// loop break/continue, goto value, label default): the bits decide what is present; the flat tree must report exactly that. public void Can_fuzz_optional_children_presence_in_flat_expression() @@ -563,6 +628,7 @@ public void Can_fuzz_optional_children_presence_in_flat_expression() { } public void Can_compile_generated_flat_expression_natively() { } public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() { } public void Experiment_compare_flat_and_light_compile_phases() { } + public void Flat_compile_handles_new_call_and_member_access_natively() { } public void Flat_compiles_the_readme_expressions_and_compares_the_time() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 382860478..4440444e7 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -40,6 +40,7 @@ public int Run() Can_fuzz_optional_children_presence_in_flat_expression(); Can_compile_generated_flat_expression_natively(); Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); + Flat_compile_handles_new_call_and_member_access_natively(); Experiment_compare_flat_and_light_compile_phases(); Flat_compiles_the_readme_expressions_and_compares_the_time(); From 1af09c254cbe1f9d5394002bbe9c532bc512383a Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 1 Nov 2026 22:47:15 +0100 Subject: [PATCH 09/19] Flat native collect: drop per-node argument type checks, match Light Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 5 +- .../FlatExpressionCompiler.cs | 69 +++---------------- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 60dbfbc38..7a5432a5d 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -21,9 +21,10 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. Conclusion: the native subset must grow to closures/New/nested lambdas before flat beats Light on the README cases. 4. `tried: done` Widened the native subset: reference-typed params/locals/results, `New`, static+instance `Call`, Field/Property getter, closure constants (the `ArrayClosure` array is filled from `ClosureConstants`), string and null constants. The collect checks by construction what IL needs - (`FlatFits`: same type or reference upcast, args count, assign target is a parameter, lambda param types == delegate param types). + (`FlatFits` only for the lambda return/params vs the delegate; per-node arg/instance type checks were dropped to match Light: the factories validate, and + `GetParameters()` allocated per Call/New. A tree built wrong now emits bad IL instead of falling back.) README 1 (`new X(closure a, b)`): light FEC 5.2us -> flat native 3.2us (Release, one noisy run); README 2 (nested lambda) still falls back. - Cost: ~90 lines in FlatExpressionCompiler.cs; `GetParameters()` allocates per Call/New in the collect (todo @perf). + Cost: ~60 lines in FlatExpressionCompiler.cs. 5. `todo` Nested lambdas (closure with nested-lambda slots), Convert, value types (struct params/Nullable), Invoke, arrays, loops/labels/try. Each with a fuzz or deterministic test. ## Construction-time ideas diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index e8b6bc65a..edbaa066c 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -27,36 +27,9 @@ struct FlatVar private static bool IsFlatType(Type t) => t == typeof(int) || t == typeof(bool) || !t.IsValueType && !t.IsByRef && !t.IsPointer; - // IL needs no conversion only for the same type or the reference upcast + // the lambda and delegate return/parameter types are checked once per compile, not per node private static bool FlatFits(Type to, Type from) => to == from || !to.IsValueType && !from.IsValueType && to.IsAssignableFrom(from); - private static bool FlatArgsFit(ref ExprTree tree, int idx, int count, ParameterInfo[] pars) - { - if (pars.Length != count) return false; // todo: @perf GetParameters allocates, cache or compare lazily - for (var i = 0; i < count; ++i) - { - ref var arg = ref tree.Nodes.GetSurePresentRef(idx); - if (!FlatFits(pars[i].ParameterType, arg.Type)) return false; - idx = arg.NextIdx; - } - return true; - } - - private static bool FlatCallFits(ref ExprTree tree, ref ExprNode n, MethodInfo method) - { - var idx = n.ChildIdx; - var count = (int)n.ChildCount; - if (!method.IsStatic) - { - if (count == 0) return false; - ref var instance = ref tree.Nodes.GetSurePresentRef(idx); - if (!FlatFits(method.DeclaringType, instance.Type)) return false; - idx = instance.NextIdx; - --count; - } - return !method.IsGenericMethodDefinition && FlatArgsFit(ref tree, idx, count, method.GetParameters()); - } - /// The collect phase on the flat nodes: one linear pass over the contiguous array, no recursion, no stack, no per-node casts. /// What the tree construction already guarantees is not checked here: the child counts, the not-null children, the parameter /// and label ids, the closure constants (they are in the separate list, so `IsClosureBoundConstant` is already answered). @@ -89,43 +62,23 @@ public static Result TryCollectFlatInfo(this ref ExprTree tree) if (!IsFlatType(t)) return Result.NotSupported_UnknownExpression; break; case ExprType.Assign: - { - ref var left = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); - if (!IsFlatType(t) || left.NodeType != ExprType.Parameter || - !FlatFits(left.Type, tree.Nodes.GetSurePresentRef(left.NextIdx).Type)) - return Result.NotSupported_UnknownExpression; - break; - } + // the target must be a parameter, the member assignment is not emitted yet + if (!IsFlatType(t) || tree.Nodes.GetSurePresentRef(n.ChildIdx).NodeType != ExprType.Parameter) return Result.NotSupported_UnknownExpression; + break; case ExprType.Parameter: if (!IsFlatType(t) || n.Flags != 0) return Result.NotSupported_UnknownExpression; break; case ExprType.New: - if (!IsFlatType(t) || n.Obj is not ConstructorInfo ctor || - !FlatArgsFit(ref tree, n.ChildIdx, n.ChildCount, ctor.GetParameters())) - return Result.NotSupported_UnknownExpression; + // the arguments are not type-checked, same as the Light: the factories did that + if (!IsFlatType(t) || n.Obj is not ConstructorInfo) return Result.NotSupported_UnknownExpression; break; case ExprType.Call: - { - if (!IsFlatType(t) || n.Obj is not MethodInfo method) return Result.NotSupported_UnknownExpression; - if (!FlatCallFits(ref tree, ref n, method)) return Result.NotSupported_UnknownExpression; - break; - } + if (!IsFlatType(t) || n.Obj is not MethodInfo m || m.IsGenericMethodDefinition) return Result.NotSupported_UnknownExpression; + break; case ExprType.MemberAccess: - { - MethodInfo getter = null; - var isStatic = false; - if (n.Obj is FieldInfo field && !field.IsLiteral) - isStatic = field.IsStatic; - else if (n.Obj is PropertyInfo prop && (getter = prop.GetGetMethod(true)) != null) - isStatic = getter.IsStatic; - else - return Result.NotSupported_UnknownExpression; - var declaringType = ((MemberInfo)n.Obj).DeclaringType; - if (!IsFlatType(t) || isStatic != (n.ChildCount == 0) || - !isStatic && !FlatFits(declaringType, tree.Nodes.GetSurePresentRef(n.ChildIdx).Type)) - return Result.NotSupported_UnknownExpression; - break; - } + if (!IsFlatType(t) || !(n.Obj is FieldInfo f && !f.IsLiteral || n.Obj is PropertyInfo p && p.CanRead)) + return Result.NotSupported_UnknownExpression; + break; case ExprType.Constant: { var isInline = ReferenceEquals(n.Obj, ExprNode.InlineValueMarker); From 9fa8fc431c47e4476767cb788f8dfaf01d3a3fa7 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 21:30:45 +0100 Subject: [PATCH 10/19] Flat native compile: nested lambdas and captured variables Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 6 + .../FlatExpressionCompiler.cs | 402 +++++++++++++++--- .../LightExpressionPropertyTests.cs | 79 +++- .../LightExpressionTests.cs | 1 + 4 files changed, 422 insertions(+), 66 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 7a5432a5d..6c81e75a5 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -38,3 +38,9 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - `dropped (for now): closure-constant usage counts` - trivial in the collect scan, ClosureConstants.Count==0 already covers the no-closure case. - `dropped (for now): parameter declaring owner alone` - weak without a parent link; parent index replaces it. - `deferred: label id table` - conflicts with the "drop info lists" direction. + +## Step 6: closures / nested lambdas (tried) +- Design: captured vars live in StrongBox (shared mutation), nested lambda = separate DynamicMethod, open delegate stored in the shared constants array, curried at run-time with ArrayClosureWithNonPassedParams (same as FEC). +- Cost: recursive capture pre-pass + per-compile lists, only when lambdas > 1. +- Measured (noisy, Release): README 2 flat native ~30us vs Light FEC ~29us, so no win yet; the DynamicMethod per lambda dominates. README 1 stays ~3x faster natively. +- Todo: Convert, value types, Invoke, arrays, loops/labels/try, native-vs-System fuzzer for new nodes, linear capture scan via construction-time parent links. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index edbaa066c..8fee574fe 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -3,8 +3,10 @@ namespace FastExpressionCompiler.LightExpression; using System; +using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.CompilerServices; using FastExpressionCompiler.FlatExpression; using FastExpressionCompiler.LightExpression.ImTools; using ExprType = System.Linq.Expressions.ExpressionType; @@ -21,8 +23,196 @@ public static TDelegate CompileFast(this ref ExprTree tree, bool ifFa struct FlatVar { public ushort Id; - public short Arg; // the argument index (the closure is 0) or -1 for the local - public LocalBuilder Local; + public Type Type; + public short Arg; // the argument index (the closure is 0) or -1 + public LocalBuilder Local; // the local, or the local StrongBox if Boxed + public short Slot; // for Boxed: the StrongBox is the item of the closure NonPassedParams, or -1 if it is the Local + public bool Boxed; // the variable is used by a nested lambda, so it lives in the StrongBox shared with that lambda + } + + /// The nested lambda and the outer variables it uses, the closure NonPassedParams are in this order. + private sealed class FlatLambda + { + public int Idx; + public List<(ushort Id, Type Type)> Captures = new(); + } + + /// The state of one compilation. The lists are only created when there are nested lambdas. + private sealed class FlatCtx + { + public object[] Constants; // the closure constants, then the open delegates of the nested lambdas + public int NextSlot; + public List<(ushort Id, Type Type)> Captured; // all variables used by the nested lambdas + public List Lambdas; + public List<(ushort Id, int Depth)> Declared; // scratch of the captures search + public List Stack; // scratch of the captures search + + public bool IsCaptured(ushort id) + { + if (Captured != null) + for (var i = 0; i < Captured.Count; ++i) + if (Captured[i].Id == id) return true; + return false; + } + + public FlatLambda FindLambda(int idx) + { + if (Lambdas != null) + for (var i = 0; i < Lambdas.Count; ++i) + if (Lambdas[i].Idx == idx) return Lambdas[i]; + return null; + } + } + + private static readonly ConstructorInfo FlatArrayClosureCtor = typeof(ArrayClosure).GetConstructors()[0]; + + private static void FlatAddCapture(List<(ushort Id, Type Type)> list, ushort id, Type type) + { + for (var i = 0; i < list.Count; ++i) + if (list[i].Id == id) return; + list.Add((id, type)); + } + + /// Finds which variables are used by the lambdas other than the declaring one. Recursive but only run for the nested lambdas. + /// The variable is declared by the lambda parameter or the block variable, the usage is the Parameter node with the same id. + private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx) + { + ref var n = ref tree.Nodes.GetSurePresentRef(idx); + switch (n.NodeType) + { + case ExprType.Constant: + return; + + case ExprType.Parameter: + { + var d = ctx.Declared.Count - 1; + while (d >= 0 && ctx.Declared[d].Id != n.ChildIdx) --d; + if (d < 0) + return; // not in scope, the emit throws + // the lambdas deeper than the declaring one are capturing it + for (var k = ctx.Declared[d].Depth; k < ctx.Stack.Count; ++k) + { + FlatAddCapture(ctx.Stack[k].Captures, n.ChildIdx, n.Type); + FlatAddCapture(ctx.Captured, n.ChildIdx, n.Type); + } + return; + } + + case ExprType.Lambda: + { + var lambda = new FlatLambda { Idx = idx }; + ctx.Lambdas.Add(lambda); + ctx.Stack.Add(lambda); + var scope = ctx.Declared.Count; + var bodyIdx = n.ChildIdx; + var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); + ctx.Declared.Add((par.ChildIdx, ctx.Stack.Count)); + parIdx = par.NextIdx; + } + FlatFindCaptures(ref tree, bodyIdx, ctx); + ctx.Declared.RemoveRange(scope, ctx.Declared.Count - scope); + ctx.Stack.RemoveAt(ctx.Stack.Count - 1); + return; + } + + case ExprType.Block: + { + ref var exprs = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); + var scope = ctx.Declared.Count; + var varIdx = exprs.NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); + ctx.Declared.Add((v.ChildIdx, ctx.Stack.Count)); + varIdx = v.NextIdx; + } + var exprIdx = exprs.ChildIdx; + for (var i = 0; i < exprs.ChildCount; ++i) + { + FlatFindCaptures(ref tree, exprIdx, ctx); + exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; + } + ctx.Declared.RemoveRange(scope, ctx.Declared.Count - scope); + return; + } + + default: + { + var childIdx = n.ChildIdx; + for (var i = 0; i < n.ChildCount; ++i) + { + FlatFindCaptures(ref tree, childIdx, ctx); + childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; + } + return; + } + } + } + + /// The lambda return and parameter types must be flat and be the same as the delegate has. The nested lambda + /// delegate must be the Func (the closure is curried into it) with the supported number of parameters. + private static bool FlatLambdaFits(ref ExprTree tree, ref ExprNode lambda, bool nested) + { + var invoke = lambda.Type.GetMethod("Invoke"); + if (invoke == null) + return false; + ref var body = ref tree.Nodes.GetSurePresentRef(lambda.ChildIdx); + var returnType = invoke.ReturnType; + var pars = invoke.GetParameters(); + if (!IsFlatType(returnType) || !FlatFits(returnType, body.Type) || pars.Length != lambda.ChildCount - 1) + return false; + + var types = nested ? new Type[pars.Length] : null; + var parIdx = body.NextIdx; + for (var i = 0; i < pars.Length; ++i) + { + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); + if (par.Type != pars[i].ParameterType) + return false; + if (nested) + types[i] = par.Type; + parIdx = par.NextIdx; + } + return !nested || pars.Length <= 5 && Tools.GetFuncOrActionType(types, returnType) == lambda.Type; // 5 is the CurryClosureFuncs max + } + + private static Type FlatBoxType(Type t) => typeof(StrongBox<>).MakeGenericType(t); + + private static LocalBuilder FlatNewBox(ILGenerator il, Type t) + { + var boxType = FlatBoxType(t); + var box = il.DeclareLocal(boxType); + il.Emit(OpCodes.Newobj, boxType.GetConstructor(Type.EmptyTypes)); + il.Emit(OpCodes.Stloc, box); + return box; + } + + private static void FlatLoadBox(ILGenerator il, in FlatVar v) + { + if (v.Slot < 0) + { + il.Emit(OpCodes.Ldloc, v.Local); + return; + } + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + il.Emit(OpCodes.Ldc_I4, (int)v.Slot); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, FlatBoxType(v.Type)); + } + + private static FlatVar FlatFindVar(ref SmallList, NoArrayPool> vars, ushort id) + { + for (var i = vars.Count - 1; i >= 0; --i) + { + ref var v = ref vars.GetSurePresentRef(i); + if (v.Id == id) + return v; + } + throw new InvalidOperationException("The parameter is not in scope"); // todo: @wip by construction check } private static bool IsFlatType(Type t) => t == typeof(int) || t == typeof(bool) || !t.IsValueType && !t.IsByRef && !t.IsPointer; @@ -34,13 +224,15 @@ struct FlatVar /// What the tree construction already guarantees is not checked here: the child counts, the not-null children, the parameter /// and label ids, the closure constants (they are in the separate list, so `IsClosureBoundConstant` is already answered). /// Returns `OK` if the tree is the int/bool subset which can emit. - public static Result TryCollectFlatInfo(this ref ExprTree tree) + public static Result TryCollectFlatInfo(this ref ExprTree tree) => tree.CollectFlat(out _); + + private static Result CollectFlat(this ref ExprTree tree, out int lambdas) { + lambdas = 0; var count = tree.Nodes.Count; if (count < 2) return Result.NotSupported_UnknownExpression; - var lambdas = 0; for (var i = 1; i < count; ++i) { ref var n = ref tree.Nodes.GetSurePresentRef(i); @@ -97,76 +289,102 @@ public static Result TryCollectFlatInfo(this ref ExprTree tree) if (t != typeof(bool)) return Result.NotSupported_UnknownExpression; break; case ExprType.Lambda: - if (++lambdas != 1) return Result.NotSupported_UnknownExpression; // the nested lambdas need the closure + ++lambdas; + if (!FlatLambdaFits(ref tree, ref n, i != tree.RootIdx)) return Result.NotSupported_UnknownExpression; break; default: return Result.NotSupported_UnknownExpression; } } - return lambdas == 1 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; + return lambdas != 0 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; } /// Returns null if the tree is not the supported subset or the delegate type is not the lambda type. public static TDelegate TryCompileFlat(this ref ExprTree tree) where TDelegate : class { - if (tree.TryCollectFlatInfo() != Result.OK) + if (tree.CollectFlat(out var lambdas) != Result.OK) return null; - ref var lambda = ref tree.Nodes.GetSurePresentRef(tree.RootIdx); - if (lambda.Type != typeof(TDelegate)) + if (tree.Nodes.GetSurePresentRef(tree.RootIdx).Type != typeof(TDelegate)) return null; - var bodyIdx = lambda.ChildIdx; - var returnType = tree.Nodes.GetSurePresentRef(bodyIdx).Type; - var invoke = typeof(TDelegate).GetMethod("Invoke"); - var delegateReturn = invoke?.ReturnType; - if (delegateReturn == null || !IsFlatType(delegateReturn) || !FlatFits(delegateReturn, returnType)) - return null; + var constantCount = tree.ClosureConstants.Count; + var ctx = new FlatCtx { NextSlot = constantCount }; + var slotCount = constantCount + lambdas - 1; // the slot per nested lambda for its open delegate + if (slotCount != 0) + { + ctx.Constants = new object[slotCount]; + for (var i = 0; i < constantCount; ++i) + ctx.Constants[i] = tree.ClosureConstants[i]; + } + + if (lambdas > 1) + { + ctx.Captured = new(); + ctx.Lambdas = new(); + ctx.Declared = new(); + ctx.Stack = new(); + FlatFindCaptures(ref tree, tree.RootIdx, ctx); // todo: @perf the recursive pass, the parent links and the flags at construction would make it a linear scan + } + + var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); + return (TDelegate)(object)EmitFlatLambda(ref tree, tree.RootIdx, ctx, typeof(TDelegate), closure); + } + /// Emits the lambda into its own DynamicMethod. The root is closed over the closure; the nested one + /// (the delegate type is null) becomes the open delegate with the closure as the first parameter, to be curried at run-time. + private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx ctx, Type delegateType, object closure) + { + ref var lambda = ref tree.Nodes.GetSurePresentRef(lambdaIdx); + var bodyIdx = lambda.ChildIdx; var paramCount = lambda.ChildCount - 1; - var delegatePars = invoke.GetParameters(); - if (delegatePars.Length != paramCount) - return null; var paramTypes = new Type[paramCount + 1]; paramTypes[0] = typeof(ArrayClosure); - var firstParIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; for (var i = 1; i <= paramCount; ++i) { - ref var par = ref tree.Nodes.GetSurePresentRef(firstParIdx); - if (par.Type != delegatePars[i - 1].ParameterType) - return null; + ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); paramTypes[i] = par.Type; - firstParIdx = par.NextIdx; + parIdx = par.NextIdx; } - var dynMethod = new DynamicMethod(string.Empty, delegateReturn, paramTypes, typeof(ArrayClosure), true); + var returnType = lambda.Type.GetMethod("Invoke").ReturnType; + var dynMethod = new DynamicMethod(string.Empty, returnType, paramTypes, typeof(ArrayClosure), true); var il = dynMethod.GetILGenerator(); var vars = default(SmallList, NoArrayPool>); - var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; for (var i = 1; i <= paramCount; ++i) { ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); - vars.Add(new FlatVar { Id = par.ChildIdx, Arg = (short)i }); + if (ctx.IsCaptured(par.ChildIdx)) + { + var box = FlatNewBox(il, par.Type); + il.Emit(OpCodes.Ldloc, box); + il.Emit(OpCodes.Ldarg, (short)i); + il.Emit(OpCodes.Stfld, FlatBoxType(par.Type).GetField("Value")); + vars.Add(new FlatVar { Id = par.ChildIdx, Type = par.Type, Arg = -1, Local = box, Slot = -1, Boxed = true }); + } + else + vars.Add(new FlatVar { Id = par.ChildIdx, Type = par.Type, Arg = (short)i, Slot = -1 }); parIdx = par.NextIdx; } - EmitFlat(ref tree, bodyIdx, il, ref vars, false); + var captures = ctx.FindLambda(lambdaIdx)?.Captures; + for (var j = 0; captures != null && j < captures.Count; ++j) + vars.Add(new FlatVar { Id = captures[j].Id, Type = captures[j].Type, Arg = -1, Slot = (short)j, Boxed = true }); + + EmitFlat(ref tree, bodyIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Ret); - object closure = EmptyArrayClosure; - if (tree.ClosureConstants.Count != 0) - { - var constants = new object[tree.ClosureConstants.Count]; - for (var i = 0; i < constants.Length; ++i) - constants[i] = tree.ClosureConstants[i]; - closure = new ArrayClosure(constants); - } - return (TDelegate)(object)dynMethod.CreateDelegate(typeof(TDelegate), closure); + + return delegateType != null + ? dynMethod.CreateDelegate(delegateType, closure) + : dynMethod.CreateDelegate(Tools.GetFuncOrActionType(paramTypes, returnType), null); } private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, - ref SmallList, NoArrayPool> vars, bool ignoreResult) + ref SmallList, NoArrayPool> vars, FlatCtx ctx, bool ignoreResult) { ref var n = ref tree.Nodes.GetSurePresentRef(idx); var nodeType = n.NodeType; @@ -193,34 +411,85 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, return; case ExprType.Parameter: - if (ignoreResult) - return; - for (var i = vars.Count - 1; i >= 0; --i) { - ref var v = ref vars.GetSurePresentRef(i); - if (v.Id != n.ChildIdx) - continue; - if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); else il.Emit(OpCodes.Ldloc, v.Local); + if (ignoreResult) + return; + var v = FlatFindVar(ref vars, n.ChildIdx); + if (v.Boxed) + { + FlatLoadBox(il, in v); + il.Emit(OpCodes.Ldfld, FlatBoxType(v.Type).GetField("Value")); + } + else if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); else il.Emit(OpCodes.Ldloc, v.Local); return; } - throw new InvalidOperationException("The parameter is not in scope"); // todo: @wip by construction check case ExprType.Assign: { var leftIdx = n.ChildIdx; ref var left = ref tree.Nodes.GetSurePresentRef(leftIdx); - EmitFlat(ref tree, left.NextIdx, il, ref vars, false); + var rightIdx = left.NextIdx; + var v = FlatFindVar(ref vars, left.ChildIdx); + if (v.Boxed) + { + FlatLoadBox(il, in v); + EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); + LocalBuilder result = null; + if (!ignoreResult) + { + il.Emit(OpCodes.Dup); + result = il.DeclareLocal(v.Type); + il.Emit(OpCodes.Stloc, result); + } + il.Emit(OpCodes.Stfld, FlatBoxType(v.Type).GetField("Value")); + if (result != null) + il.Emit(OpCodes.Ldloc, result); + return; + } + EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); if (!ignoreResult) il.Emit(OpCodes.Dup); - for (var i = vars.Count - 1; i >= 0; --i) - { - ref var v = ref vars.GetSurePresentRef(i); - if (v.Id != left.ChildIdx) - continue; - if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else il.Emit(OpCodes.Stloc, v.Local); + if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else il.Emit(OpCodes.Stloc, v.Local); + return; + } + + case ExprType.Lambda: + { + if (ignoreResult) return; + // the nested lambda is the separate method; at run-time the open delegate is curried with the new closure + // made of the shared constants and the StrongBoxes of the used outer variables + var captures = ctx.FindLambda(idx).Captures; + var open = EmitFlatLambda(ref tree, idx, ctx, null, null); + var openType = open.GetType(); + var slot = ctx.NextSlot++; + ctx.Constants[slot] = open; + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); + il.Emit(OpCodes.Ldc_I4, slot); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Castclass, openType); + + if (captures.Count != 0) + { + il.Emit(OpCodes.Ldc_I4, captures.Count); + il.Emit(OpCodes.Newarr, typeof(object)); + for (var j = 0; j < captures.Count; ++j) + { + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Ldc_I4, j); + FlatLoadBox(il, FlatFindVar(ref vars, captures[j].Id)); + il.Emit(OpCodes.Stelem_Ref); + } } - throw new InvalidOperationException("The assigned parameter is not in scope"); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); + il.Emit(OpCodes.Newobj, captures.Count != 0 ? ArrayClosureWithNonPassedParamsAndConstantsCtor : FlatArrayClosureCtor); + + var typeArgs = openType.GetGenericArguments(); + il.Emit(OpCodes.Call, CurryClosureFuncs.Methods[typeArgs.Length - 2].MakeGenericMethod(typeArgs)); + return; } case ExprType.New: @@ -229,7 +498,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var argIdx = n.ChildIdx; for (var i = 0; i < n.ChildCount; ++i) { - EmitFlat(ref tree, argIdx, il, ref vars, false); + EmitFlat(ref tree, argIdx, il, ref vars, ctx, false); argIdx = tree.Nodes.GetSurePresentRef(argIdx).NextIdx; } if (nodeType == ExprType.New) @@ -245,7 +514,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, case ExprType.MemberAccess: { if (n.ChildCount != 0) - EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); if (n.Obj is FieldInfo field) il.Emit(field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, field); else @@ -257,7 +526,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, } case ExprType.Not: - EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ceq); break; @@ -269,12 +538,12 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var ifFalseIdx = tree.Nodes.GetSurePresentRef(ifTrueIdx).NextIdx; var elseLabel = il.DefineLabel(); var endLabel = il.DefineLabel(); - EmitFlat(ref tree, testIdx, il, ref vars, false); + EmitFlat(ref tree, testIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Brfalse, elseLabel); - EmitFlat(ref tree, ifTrueIdx, il, ref vars, ignoreResult); + EmitFlat(ref tree, ifTrueIdx, il, ref vars, ctx, ignoreResult); il.Emit(OpCodes.Br, endLabel); il.MarkLabel(elseLabel); - EmitFlat(ref tree, ifFalseIdx, il, ref vars, ignoreResult); + EmitFlat(ref tree, ifFalseIdx, il, ref vars, ctx, ignoreResult); il.MarkLabel(endLabel); return; } @@ -286,9 +555,9 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var rightIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; var shortCircuit = il.DefineLabel(); var endLabel = il.DefineLabel(); - EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); il.Emit(isAnd ? OpCodes.Brfalse : OpCodes.Brtrue, shortCircuit); - EmitFlat(ref tree, rightIdx, il, ref vars, false); + EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Br, endLabel); il.MarkLabel(shortCircuit); il.Emit(isAnd ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1); @@ -306,7 +575,10 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, for (var i = 1; i < n.ChildCount; ++i) { ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); - vars.Add(new FlatVar { Id = v.ChildIdx, Arg = -1, Local = il.DeclareLocal(v.Type) }); + if (ctx.IsCaptured(v.ChildIdx)) + vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = FlatNewBox(il, v.Type), Slot = -1, Boxed = true }); + else + vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = il.DeclareLocal(v.Type), Slot = -1 }); varIdx = v.NextIdx; } @@ -314,7 +586,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var exprIdx = exprs.ChildIdx; for (var i = 0; i <= last; ++i) { - EmitFlat(ref tree, exprIdx, il, ref vars, i != last || ignoreResult); + EmitFlat(ref tree, exprIdx, il, ref vars, ctx, i != last || ignoreResult); exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; } vars.Count = scopeStart; // the block variables go out of the scope @@ -323,8 +595,8 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, default: // the binary int/bool operations { - EmitFlat(ref tree, n.ChildIdx, il, ref vars, false); - EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ctx, false); switch (nodeType) { case ExprType.Add: il.Emit(OpCodes.Add); break; diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index f88603a6e..bf547ca1e 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -159,7 +159,7 @@ void Time(string name, Action body) Time("2 light FEC", () => l2.CompileFast(true)); Time("2 flat->light->FEC (CompileFast)", () => fe2.CompileFast>>()); Time("2 flat->light only", () => fe2.ToLightExpression()); - Time("2 flat native probe (falls back)", () => fe2.TryCompileFlat>>()); + Time("2 flat native (nested lambda)", () => fe2.TryCompileFlat>>()); } public class FlatPoint @@ -227,6 +227,82 @@ public void Flat_compile_handles_new_call_and_member_access_natively() Ensure(fe6.TryCompileFlat>()(null) == null, "null constant native"); } + /// The nested lambdas: no capture, the captured parameter and block variable (the shared StrongBox, so the mutation is seen + /// both ways), the capture through the middle lambda, and the closure constants inside the nested lambda. + public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively() + { + // n => () => n + 1, the README expression + var fe1 = default(ExprTree); + var n1 = fe1.ParameterOf("n"); + fe1.RootIdx = fe1.Lambda>>(fe1.Lambda>(fe1.Add(n1, fe1.ConstantInt(1))), n1); + var f1 = fe1.TryCompileFlat>>(); + Ensure(f1 != null && f1(41)() == 42, "readme: n => () => n + 1"); + + // x => y => x + y + var fe2 = default(ExprTree); + var x2 = fe2.ParameterOf("x"); + var y2 = fe2.ParameterOf("y"); + fe2.RootIdx = fe2.Lambda>>(fe2.Lambda>(fe2.Add(x2, y2), y2), x2); + var f2 = fe2.TryCompileFlat>>(); + var add3 = f2(3); + var add10 = f2(10); + Ensure(add3(4) == 7 && add10(4) == 14 && add3(1) == 4, "x => y => x + y"); + + // n => () => n = n + 1: every outer call has its own n, the nested calls share it + var fe3 = default(ExprTree); + var n3 = fe3.ParameterOf("n"); + fe3.RootIdx = fe3.Lambda>>( + fe3.Lambda>(fe3.Assign(n3, fe3.Add(n3, fe3.ConstantInt(1)))), n3); + var f3 = fe3.TryCompileFlat>>(); + var inc = f3(10); + var inc0 = f3(0); + Ensure(inc() == 11 && inc() == 12 && inc0() == 1 && inc() == 13, "mutated captured parameter"); + + // n => { var f = () => n; n = 5; f }: the lambda sees the later assignment + var fe4 = default(ExprTree); + var n4 = fe4.ParameterOf("n"); + var f4v = fe4.Variable(typeof(Func), "f"); + fe4.RootIdx = fe4.Lambda>>( + fe4.Block(typeof(Func), new[] { f4v }, + fe4.Assign(f4v, fe4.Lambda>(n4)), + fe4.Assign(n4, fe4.ConstantInt(5)), + f4v), + n4); + var f4 = fe4.TryCompileFlat>>(); + Ensure(f4 != null && f4(1)() == 5, "the lambda sees the assignment after its creation"); + + // a => () => () => a + 1: the middle lambda passes the capture through + var fe5 = default(ExprTree); + var a5 = fe5.ParameterOf("a"); + fe5.RootIdx = fe5.Lambda>>>( + fe5.Lambda>>(fe5.Lambda>(fe5.Add(a5, fe5.ConstantInt(1)))), a5); + var f5 = fe5.TryCompileFlat>>>(); + Ensure(f5 != null && f5(1)()() == 2, "the capture through the middle lambda"); + + // p => { var v = p * 2; () => v + p + Constant(held).X } + var held = new FlatPoint(7, 8); + var fe6 = default(ExprTree); + var p6 = fe6.ParameterOf("p"); + var v6 = fe6.Variable(typeof(int), "v"); + fe6.RootIdx = fe6.Lambda>>( + fe6.Block(typeof(Func), new[] { v6 }, + fe6.Assign(v6, fe6.MakeBinary(ExpressionType.Multiply, p6, fe6.ConstantInt(2))), + fe6.Lambda>( + fe6.Add(fe6.Add(v6, p6), fe6.Field(fe6.Constant(held, typeof(FlatPoint)), typeof(FlatPoint).GetField("X"))))), + p6); + var f6 = fe6.TryCompileFlat>>(); + Ensure(f6 != null && f6(5)() == 10 + 5 + 7, "the captured block variable and the closure constant in the nested lambda"); + + // the custom delegate type of the nested lambda is outside the subset and falls back + var fe7 = default(ExprTree); + var n7 = fe7.ParameterOf("n"); + fe7.RootIdx = fe7.Lambda>(fe7.Lambda(n7), n7); + Ensure(fe7.TryCompileFlat>() == null, "custom delegate is outside the subset"); + Ensure(fe7.CompileFast>()(3)() == 3, "custom delegate falls back"); + } + + public delegate int FlatCustomDelegate(); + /// Fuzzes presence/absence of every optional child (switch default/cases, try finally/catch, catch variable/filter, /// loop break/continue, goto value, label default): the bits decide what is present; the flat tree must report exactly that. public void Can_fuzz_optional_children_presence_in_flat_expression() @@ -629,6 +705,7 @@ public void Can_compile_generated_flat_expression_natively() { } public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() { } public void Experiment_compare_flat_and_light_compile_phases() { } public void Flat_compile_handles_new_call_and_member_access_natively() { } + public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively() { } public void Flat_compiles_the_readme_expressions_and_compares_the_time() { } #endif } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 4440444e7..6d968c0d2 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -41,6 +41,7 @@ public int Run() Can_compile_generated_flat_expression_natively(); Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); Flat_compile_handles_new_call_and_member_access_natively(); + Flat_compile_handles_nested_lambdas_and_captured_variables_natively(); Experiment_compare_flat_and_light_compile_phases(); Flat_compiles_the_readme_expressions_and_compares_the_time(); From 94e9a50919e39514c778d1459cf0168252b245ce Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 21:44:36 +0100 Subject: [PATCH 11/19] Flat native compile: construction-time lambda count and assigned params, by-value captures, delegate info cache Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 9 + .../FlatExpression.cs | 32 +- .../FlatExpressionCompiler.cs | 295 ++++++++++-------- .../LightExpressionPropertyTests.cs | 42 ++- 4 files changed, 245 insertions(+), 133 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 6c81e75a5..c3f92a52e 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -44,3 +44,12 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - Cost: recursive capture pre-pass + per-compile lists, only when lambdas > 1. - Measured (noisy, Release): README 2 flat native ~30us vs Light FEC ~29us, so no win yet; the DynamicMethod per lambda dominates. README 1 stays ~3x faster natively. - Todo: Convert, value types, Invoke, arrays, loops/labels/try, native-vs-System fuzzer for new nodes, linear capture scan via construction-time parent links. + +## Step 7: construction helps for the nested lambdas (tried, measured) +- 1 LambdaCount at construction: DONE. Collect no longer counts, the constants array is sized upfront. +- 2 linear capture scan by index range: REJECTED, unsound. The nodes of a subtree are not contiguous (shared nodes are built earlier, clones are appended later). Instead: the same top-down pass but with no allocations (SmallList on the stack, one FlatCtx object). +- 3 AssignedParams recorded by Assign: DONE. The never-assigned captured variable goes by value (boxed in NonPassedParams, no StrongBox, plain local in the declaring lambda). Separate list, not a node flag, because the comparer/hash read the flags. +- 4 counts per tree: DONE with 1 (closure constants count was already known). +- 5 SOA of the nodes: NOT TRIED. The collect scan of README 2 is 0.23 us of ~7 us, the floor of 2 bare DynamicMethods is 3 us; the layout is not the bottleneck. +- Biggest win was not structural: the per-delegate-type cache (Invoke, param types, open Func type, Curry method). Collect 0.71 -> 0.23 us, README 2 native 9.5 -> ~6.6 us (Light FEC ~10-11, min of 9 rounds). +- Todo: the remaining ~3 us over the DynamicMethod floor (IL token resolution, ctx), Convert, value types, Invoke, arrays, loops/try, native-vs-System fuzzer. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index fc5bf5060..4b4f1c084 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -190,6 +190,23 @@ public partial struct ExprTree : IEquatable /// Gets or sets closure constants that are referenced from constant nodes. public SmallList, NoArrayPool> ClosureConstants; + /// The number of the lambdas built so far, so the compile does not count them and knows the nested lambdas slots upfront. + public ushort LambdaCount; + + /// The ids of the parameters and variables that are assigned, recorded by . The compile passes + /// the never-assigned captured variable to the nested lambda by value, without the StrongBox. Any other node that mutates + /// the variable (increment, compound assign) must record it here too, once the compile supports it. + /// It is the separate list, not the node flag, so the structural equality and hash are untouched. + internal SmallList, NoArrayPool> AssignedParams; + + internal bool IsAssigned(ushort id) + { + for (var i = 0; i < AssignedParams.Count; ++i) + if (AssignedParams.GetSurePresentRef(i) == id) + return true; + return false; + } + // Import-only identity maps (valid during FromSysExpr / FromLightExpr). private SmallMap16> _parameterIds; private SmallMap16> _labelIds; @@ -558,11 +575,21 @@ public ushort Property(ushort instance, PropertyInfo prop, params ushort[] args) /// Adds a binary node of the specified kind. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort MakeBinary(ExpressionType nodeType, ushort left, ushort right, bool isLiftedToNull = false, - MethodInfo method = null, ushort conversion = 0, Type type = null) => - WithThreeChildren( + MethodInfo method = null, ushort conversion = 0, Type type = null) + { + var idx = WithThreeChildren( nodeType, type ?? GetBinaryResultType(nodeType, Nodes[left].Type, method), method, isLiftedToNull ? BinaryLiftedToNullFlag : (byte)0, default, left, right, conversion); + if (nodeType == ExpressionType.Assign) + { + ref var target = ref Nodes.GetSurePresentRef(left); + if (target.NodeType == ExpressionType.Parameter && !IsAssigned(target.ChildIdx)) + AssignedParams.Add(target.ChildIdx); + } + return idx; + } + /// Adds a one-dimensional array index node. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort ArrayIndex(ushort array, ushort idx) => MakeBinary(ExpressionType.ArrayIndex, array, idx); @@ -644,6 +671,7 @@ public ushort Lambda(Type delegateType, ushort bodyIdx, params ushort[] pars) pars ??= Array.Empty(); #endif EnsureIndexZeroSentinel(); + ++LambdaCount; ref var lambdaRef = ref Nodes.AddDefaultAndGetRef(out var idx); ExprNode.Set(ref lambdaRef, ExpressionType.Lambda, delegateType); var lambdaIdx = (ushort)idx; diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index 8fee574fe..d9253937e 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -3,7 +3,7 @@ namespace FastExpressionCompiler.LightExpression; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; @@ -26,56 +26,45 @@ struct FlatVar public Type Type; public short Arg; // the argument index (the closure is 0) or -1 public LocalBuilder Local; // the local, or the local StrongBox if Boxed - public short Slot; // for Boxed: the StrongBox is the item of the closure NonPassedParams, or -1 if it is the Local - public bool Boxed; // the variable is used by a nested lambda, so it lives in the StrongBox shared with that lambda + public short Slot; // the item of the closure NonPassedParams if the variable is captured from the outer lambda, or -1 + public bool Boxed; // the variable is assigned and used by a nested lambda, so it lives in the StrongBox shared with that lambda } - /// The nested lambda and the outer variables it uses, the closure NonPassedParams are in this order. - private sealed class FlatLambda + /// The outer variable used by the nested lambda. The closure NonPassedParams of the lambda are its captures in the list order. + /// ByValue: the variable is never assigned, so the NonPassedParams item is the (boxed) value, otherwise it is the StrongBox. + private struct FlatCapture { - public int Idx; - public List<(ushort Id, Type Type)> Captures = new(); + public int LambdaIdx; + public ushort Id; + public Type Type; + public bool ByValue; } - /// The state of one compilation. The lists are only created when there are nested lambdas. + /// The state of one compilation. The only allocation is this object, and the constants array if any. private sealed class FlatCtx { public object[] Constants; // the closure constants, then the open delegates of the nested lambdas public int NextSlot; - public List<(ushort Id, Type Type)> Captured; // all variables used by the nested lambdas - public List Lambdas; - public List<(ushort Id, int Depth)> Declared; // scratch of the captures search - public List Stack; // scratch of the captures search + public SmallList, NoArrayPool> Captures; - public bool IsCaptured(ushort id) + public bool IsBoxed(ushort id) { - if (Captured != null) - for (var i = 0; i < Captured.Count; ++i) - if (Captured[i].Id == id) return true; + for (var i = 0; i < Captures.Count; ++i) + { + ref var c = ref Captures.GetSurePresentRef(i); + if (c.Id == id && !c.ByValue) return true; + } return false; } - - public FlatLambda FindLambda(int idx) - { - if (Lambdas != null) - for (var i = 0; i < Lambdas.Count; ++i) - if (Lambdas[i].Idx == idx) return Lambdas[i]; - return null; - } } private static readonly ConstructorInfo FlatArrayClosureCtor = typeof(ArrayClosure).GetConstructors()[0]; - private static void FlatAddCapture(List<(ushort Id, Type Type)> list, ushort id, Type type) - { - for (var i = 0; i < list.Count; ++i) - if (list[i].Id == id) return; - list.Add((id, type)); - } - /// Finds which variables are used by the lambdas other than the declaring one. Recursive but only run for the nested lambdas. /// The variable is declared by the lambda parameter or the block variable, the usage is the Parameter node with the same id. - private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx) + private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx, + ref SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>> declared, + ref SmallList, NoArrayPool> lambdas) { ref var n = ref tree.Nodes.GetSurePresentRef(idx); switch (n.NodeType) @@ -85,57 +74,62 @@ private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx) case ExprType.Parameter: { - var d = ctx.Declared.Count - 1; - while (d >= 0 && ctx.Declared[d].Id != n.ChildIdx) --d; + var d = declared.Count - 1; + while (d >= 0 && declared.GetSurePresentRef(d).Id != n.ChildIdx) --d; if (d < 0) return; // not in scope, the emit throws // the lambdas deeper than the declaring one are capturing it - for (var k = ctx.Declared[d].Depth; k < ctx.Stack.Count; ++k) + for (var k = declared.GetSurePresentRef(d).Depth; k < lambdas.Count; ++k) { - FlatAddCapture(ctx.Stack[k].Captures, n.ChildIdx, n.Type); - FlatAddCapture(ctx.Captured, n.ChildIdx, n.Type); + var lambdaIdx = lambdas.GetSurePresentRef(k); + var found = false; + for (var c = 0; c < ctx.Captures.Count && !found; ++c) + { + ref var cap = ref ctx.Captures.GetSurePresentRef(c); + found = cap.LambdaIdx == lambdaIdx && cap.Id == n.ChildIdx; + } + if (!found) + ctx.Captures.Add(new FlatCapture { LambdaIdx = lambdaIdx, Id = n.ChildIdx, Type = n.Type, ByValue = !tree.IsAssigned(n.ChildIdx) }); } return; } case ExprType.Lambda: { - var lambda = new FlatLambda { Idx = idx }; - ctx.Lambdas.Add(lambda); - ctx.Stack.Add(lambda); - var scope = ctx.Declared.Count; + lambdas.Add(idx); + var scope = declared.Count; var bodyIdx = n.ChildIdx; var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; for (var i = 1; i < n.ChildCount; ++i) { ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); - ctx.Declared.Add((par.ChildIdx, ctx.Stack.Count)); + declared.Add((par.ChildIdx, lambdas.Count)); parIdx = par.NextIdx; } - FlatFindCaptures(ref tree, bodyIdx, ctx); - ctx.Declared.RemoveRange(scope, ctx.Declared.Count - scope); - ctx.Stack.RemoveAt(ctx.Stack.Count - 1); + FlatFindCaptures(ref tree, bodyIdx, ctx, ref declared, ref lambdas); + declared.Count = scope; + --lambdas.Count; return; } case ExprType.Block: { ref var exprs = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); - var scope = ctx.Declared.Count; + var scope = declared.Count; var varIdx = exprs.NextIdx; for (var i = 1; i < n.ChildCount; ++i) { ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); - ctx.Declared.Add((v.ChildIdx, ctx.Stack.Count)); + declared.Add((v.ChildIdx, lambdas.Count)); varIdx = v.NextIdx; } var exprIdx = exprs.ChildIdx; for (var i = 0; i < exprs.ChildCount; ++i) { - FlatFindCaptures(ref tree, exprIdx, ctx); + FlatFindCaptures(ref tree, exprIdx, ctx, ref declared, ref lambdas); exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; } - ctx.Declared.RemoveRange(scope, ctx.Declared.Count - scope); + declared.Count = scope; return; } @@ -144,7 +138,7 @@ private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx) var childIdx = n.ChildIdx; for (var i = 0; i < n.ChildCount; ++i) { - FlatFindCaptures(ref tree, childIdx, ctx); + FlatFindCaptures(ref tree, childIdx, ctx, ref declared, ref lambdas); childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; } return; @@ -152,31 +146,63 @@ private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx) } } - /// The lambda return and parameter types must be flat and be the same as the delegate has. The nested lambda - /// delegate must be the Func (the closure is curried into it) with the supported number of parameters. + /// The delegate type facts, found once per type: the reflection (Invoke, the Func types, the Curry method) is the biggest + /// part of the compile of the small lambda. The open delegate and the Curry are only for the nested lambda: the Func (the closure + /// is curried into it) with the supported number of parameters, otherwise they are null. + private sealed class FlatDelegateInfo + { + public Type ReturnType; + public Type[] ParamTypes; + public Type[] ClosureParamTypes; // the closure then the parameters, for the DynamicMethod + public Type OpenType; // Func + public MethodInfo CurryMethod; + } + + // the cache keeps the delegate types alive, same as the other FEC type caches + private static readonly ConcurrentDictionary FlatDelegates = new(); + + private static FlatDelegateInfo FlatGetDelegateInfo(Type delegateType) + { + if (FlatDelegates.TryGetValue(delegateType, out var info)) + return info; + + var invoke = delegateType.GetMethod("Invoke"); + if (invoke != null) + { + var pars = invoke.GetParameters(); + info = new FlatDelegateInfo { ReturnType = invoke.ReturnType, ParamTypes = new Type[pars.Length], ClosureParamTypes = new Type[pars.Length + 1] }; + info.ClosureParamTypes[0] = typeof(ArrayClosure); + for (var i = 0; i < pars.Length; ++i) + info.ParamTypes[i] = info.ClosureParamTypes[i + 1] = pars[i].ParameterType; + + if (pars.Length <= 5 && info.ReturnType != typeof(void) && Tools.GetFuncOrActionType(info.ParamTypes, info.ReturnType) == delegateType) // 5 is the CurryClosureFuncs max + { + info.OpenType = Tools.GetFuncOrActionType(info.ClosureParamTypes, info.ReturnType); + info.CurryMethod = CurryClosureFuncs.Methods[pars.Length].MakeGenericMethod(info.OpenType.GetGenericArguments()); + } + } + return FlatDelegates.GetOrAdd(delegateType, info); + } + + /// The lambda return and parameter types must be flat and be the same as the delegate has. private static bool FlatLambdaFits(ref ExprTree tree, ref ExprNode lambda, bool nested) { - var invoke = lambda.Type.GetMethod("Invoke"); - if (invoke == null) + var info = FlatGetDelegateInfo(lambda.Type); + if (info == null) return false; ref var body = ref tree.Nodes.GetSurePresentRef(lambda.ChildIdx); - var returnType = invoke.ReturnType; - var pars = invoke.GetParameters(); - if (!IsFlatType(returnType) || !FlatFits(returnType, body.Type) || pars.Length != lambda.ChildCount - 1) + if (!IsFlatType(info.ReturnType) || !FlatFits(info.ReturnType, body.Type) || info.ParamTypes.Length != lambda.ChildCount - 1) return false; - var types = nested ? new Type[pars.Length] : null; var parIdx = body.NextIdx; - for (var i = 0; i < pars.Length; ++i) + for (var i = 0; i < info.ParamTypes.Length; ++i) { ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); - if (par.Type != pars[i].ParameterType) + if (par.Type != info.ParamTypes[i]) return false; - if (nested) - types[i] = par.Type; parIdx = par.NextIdx; } - return !nested || pars.Length <= 5 && Tools.GetFuncOrActionType(types, returnType) == lambda.Type; // 5 is the CurryClosureFuncs max + return !nested || info.OpenType != null; } private static Type FlatBoxType(Type t) => typeof(StrongBox<>).MakeGenericType(t); @@ -190,6 +216,15 @@ private static LocalBuilder FlatNewBox(ILGenerator il, Type t) return box; } + /// Loads the item of the closure NonPassedParams: the StrongBox or the boxed value of the captured variable. + private static void FlatLoadNonPassed(ILGenerator il, int slot) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); + EmittingVisitor.EmitLoadConstantInt(il, slot); + il.Emit(OpCodes.Ldelem_Ref); + } + private static void FlatLoadBox(ILGenerator il, in FlatVar v) { if (v.Slot < 0) @@ -197,13 +232,26 @@ private static void FlatLoadBox(ILGenerator il, in FlatVar v) il.Emit(OpCodes.Ldloc, v.Local); return; } - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldfld, ArrayClosureWithNonPassedParamsField); - il.Emit(OpCodes.Ldc_I4, (int)v.Slot); - il.Emit(OpCodes.Ldelem_Ref); + FlatLoadNonPassed(il, v.Slot); il.Emit(OpCodes.Castclass, FlatBoxType(v.Type)); } + private static void FlatLoadVar(ILGenerator il, in FlatVar v) + { + if (v.Boxed) + { + FlatLoadBox(il, in v); + il.Emit(OpCodes.Ldfld, FlatBoxType(v.Type).GetField("Value")); + } + else if (v.Slot >= 0) + { + FlatLoadNonPassed(il, v.Slot); + il.Emit(OpCodes.Unbox_Any, v.Type); + } + else if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); + else il.Emit(OpCodes.Ldloc, v.Local); + } + private static FlatVar FlatFindVar(ref SmallList, NoArrayPool> vars, ushort id) { for (var i = vars.Count - 1; i >= 0; --i) @@ -224,11 +272,10 @@ private static FlatVar FlatFindVar(ref SmallList, NoArr /// What the tree construction already guarantees is not checked here: the child counts, the not-null children, the parameter /// and label ids, the closure constants (they are in the separate list, so `IsClosureBoundConstant` is already answered). /// Returns `OK` if the tree is the int/bool subset which can emit. - public static Result TryCollectFlatInfo(this ref ExprTree tree) => tree.CollectFlat(out _); + public static Result TryCollectFlatInfo(this ref ExprTree tree) => tree.CollectFlat(); - private static Result CollectFlat(this ref ExprTree tree, out int lambdas) + private static Result CollectFlat(this ref ExprTree tree) { - lambdas = 0; var count = tree.Nodes.Count; if (count < 2) return Result.NotSupported_UnknownExpression; @@ -289,7 +336,6 @@ private static Result CollectFlat(this ref ExprTree tree, out int lambdas) if (t != typeof(bool)) return Result.NotSupported_UnknownExpression; break; case ExprType.Lambda: - ++lambdas; if (!FlatLambdaFits(ref tree, ref n, i != tree.RootIdx)) return Result.NotSupported_UnknownExpression; break; default: @@ -297,13 +343,13 @@ private static Result CollectFlat(this ref ExprTree tree, out int lambdas) } } - return lambdas != 0 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; + return tree.LambdaCount != 0 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; } /// Returns null if the tree is not the supported subset or the delegate type is not the lambda type. public static TDelegate TryCompileFlat(this ref ExprTree tree) where TDelegate : class { - if (tree.CollectFlat(out var lambdas) != Result.OK) + if (tree.CollectFlat() != Result.OK) return null; if (tree.Nodes.GetSurePresentRef(tree.RootIdx).Type != typeof(TDelegate)) @@ -311,7 +357,7 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where var constantCount = tree.ClosureConstants.Count; var ctx = new FlatCtx { NextSlot = constantCount }; - var slotCount = constantCount + lambdas - 1; // the slot per nested lambda for its open delegate + var slotCount = constantCount + tree.LambdaCount - 1; // the slot per nested lambda for its open delegate if (slotCount != 0) { ctx.Constants = new object[slotCount]; @@ -319,13 +365,11 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where ctx.Constants[i] = tree.ClosureConstants[i]; } - if (lambdas > 1) + if (tree.LambdaCount > 1) { - ctx.Captured = new(); - ctx.Lambdas = new(); - ctx.Declared = new(); - ctx.Stack = new(); - FlatFindCaptures(ref tree, tree.RootIdx, ctx); // todo: @perf the recursive pass, the parent links and the flags at construction would make it a linear scan + var declared = default(SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>>); + var lambdas = default(SmallList, NoArrayPool>); + FlatFindCaptures(ref tree, tree.RootIdx, ctx, ref declared, ref lambdas); } var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); @@ -338,27 +382,17 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx { ref var lambda = ref tree.Nodes.GetSurePresentRef(lambdaIdx); var bodyIdx = lambda.ChildIdx; - var paramCount = lambda.ChildCount - 1; - var paramTypes = new Type[paramCount + 1]; - paramTypes[0] = typeof(ArrayClosure); - var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; - for (var i = 1; i <= paramCount; ++i) - { - ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); - paramTypes[i] = par.Type; - parIdx = par.NextIdx; - } - - var returnType = lambda.Type.GetMethod("Invoke").ReturnType; - var dynMethod = new DynamicMethod(string.Empty, returnType, paramTypes, typeof(ArrayClosure), true); + var info = FlatGetDelegateInfo(lambda.Type); + var paramCount = info.ParamTypes.Length; + var dynMethod = new DynamicMethod(string.Empty, info.ReturnType, info.ClosureParamTypes, typeof(ArrayClosure), true); var il = dynMethod.GetILGenerator(); var vars = default(SmallList, NoArrayPool>); - parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; + var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; for (var i = 1; i <= paramCount; ++i) { ref var par = ref tree.Nodes.GetSurePresentRef(parIdx); - if (ctx.IsCaptured(par.ChildIdx)) + if (ctx.IsBoxed(par.ChildIdx)) { var box = FlatNewBox(il, par.Type); il.Emit(OpCodes.Ldloc, box); @@ -371,16 +405,16 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx parIdx = par.NextIdx; } - var captures = ctx.FindLambda(lambdaIdx)?.Captures; - for (var j = 0; captures != null && j < captures.Count; ++j) - vars.Add(new FlatVar { Id = captures[j].Id, Type = captures[j].Type, Arg = -1, Slot = (short)j, Boxed = true }); - + for (int c = 0, slot = 0; c < ctx.Captures.Count; ++c) + { + ref var cap = ref ctx.Captures.GetSurePresentRef(c); + if (cap.LambdaIdx == lambdaIdx) + vars.Add(new FlatVar { Id = cap.Id, Type = cap.Type, Arg = -1, Slot = (short)slot++, Boxed = !cap.ByValue }); + } EmitFlat(ref tree, bodyIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Ret); - return delegateType != null - ? dynMethod.CreateDelegate(delegateType, closure) - : dynMethod.CreateDelegate(Tools.GetFuncOrActionType(paramTypes, returnType), null); + return dynMethod.CreateDelegate(delegateType ?? info.OpenType, closure); } private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, @@ -411,18 +445,10 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, return; case ExprType.Parameter: - { - if (ignoreResult) - return; - var v = FlatFindVar(ref vars, n.ChildIdx); - if (v.Boxed) - { - FlatLoadBox(il, in v); - il.Emit(OpCodes.Ldfld, FlatBoxType(v.Type).GetField("Value")); - } - else if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); else il.Emit(OpCodes.Ldloc, v.Local); + if (ignoreResult) return; - } + FlatLoadVar(il, FlatFindVar(ref vars, n.ChildIdx)); + return; case ExprType.Assign: { @@ -458,37 +484,52 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, if (ignoreResult) return; // the nested lambda is the separate method; at run-time the open delegate is curried with the new closure - // made of the shared constants and the StrongBoxes of the used outer variables - var captures = ctx.FindLambda(idx).Captures; + // made of the shared constants and the captured variables: the StrongBox, or the boxed value if never assigned + var info = FlatGetDelegateInfo(n.Type); var open = EmitFlatLambda(ref tree, idx, ctx, null, null); - var openType = open.GetType(); var slot = ctx.NextSlot++; ctx.Constants[slot] = open; il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); - il.Emit(OpCodes.Ldc_I4, slot); + EmittingVisitor.EmitLoadConstantInt(il, slot); il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Castclass, openType); + il.Emit(OpCodes.Castclass, info.OpenType); + + var captureCount = 0; + for (var c = 0; c < ctx.Captures.Count; ++c) + if (ctx.Captures.GetSurePresentRef(c).LambdaIdx == idx) ++captureCount; - if (captures.Count != 0) + if (captureCount != 0) { - il.Emit(OpCodes.Ldc_I4, captures.Count); + EmittingVisitor.EmitLoadConstantInt(il, captureCount); il.Emit(OpCodes.Newarr, typeof(object)); - for (var j = 0; j < captures.Count; ++j) + for (int c = 0, j = 0; c < ctx.Captures.Count; ++c) { + ref var cap = ref ctx.Captures.GetSurePresentRef(c); + if (cap.LambdaIdx != idx) + continue; il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Ldc_I4, j); - FlatLoadBox(il, FlatFindVar(ref vars, captures[j].Id)); + EmittingVisitor.EmitLoadConstantInt(il, j++); + var v = FlatFindVar(ref vars, cap.Id); + if (v.Slot >= 0) // captured by the outer lambda too, pass the item through + FlatLoadNonPassed(il, v.Slot); + else if (v.Boxed) + FlatLoadBox(il, in v); + else + { + FlatLoadVar(il, in v); + if (v.Type.IsValueType) + il.Emit(OpCodes.Box, v.Type); + } il.Emit(OpCodes.Stelem_Ref); } } il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); - il.Emit(OpCodes.Newobj, captures.Count != 0 ? ArrayClosureWithNonPassedParamsAndConstantsCtor : FlatArrayClosureCtor); + il.Emit(OpCodes.Newobj, captureCount != 0 ? ArrayClosureWithNonPassedParamsAndConstantsCtor : FlatArrayClosureCtor); - var typeArgs = openType.GetGenericArguments(); - il.Emit(OpCodes.Call, CurryClosureFuncs.Methods[typeArgs.Length - 2].MakeGenericMethod(typeArgs)); + il.Emit(OpCodes.Call, info.CurryMethod); return; } @@ -575,7 +616,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, for (var i = 1; i < n.ChildCount; ++i) { ref var v = ref tree.Nodes.GetSurePresentRef(varIdx); - if (ctx.IsCaptured(v.ChildIdx)) + if (ctx.IsBoxed(v.ChildIdx)) vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = FlatNewBox(il, v.Type), Slot = -1, Boxed = true }); else vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = il.DeclareLocal(v.Type), Slot = -1 }); diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index bf547ca1e..daa084699 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -140,13 +140,18 @@ public void Flat_compiles_the_readme_expressions_and_compares_the_time() Ensure(fe2.CompileFast>>()(41)() == 42, "readme 2 result"); Ensure(((FastExpressionCompiler.LightExpression.LambdaExpression)fe1.ToLightExpression()).CompileFast>(true)(bb).A == a, "readme 1 direct"); - const int reps = 2000; + const int reps = 300; void Time(string name, Action body) { for (var i = 0; i < 100; ++i) body(); - var sw = System.Diagnostics.Stopwatch.StartNew(); - for (var i = 0; i < reps; ++i) body(); - Console.WriteLine($"//flat-readme {name,-36} {sw.Elapsed.TotalMilliseconds * 1000 / reps,9:F2} us"); + var best = double.MaxValue; // the min of the rounds, the DynamicMethod garbage makes the single run noisy + for (var round = 0; round < 9; ++round) + { + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (var i = 0; i < reps; ++i) body(); + best = Math.Min(best, sw.Elapsed.TotalMilliseconds * 1000 / reps); + } + Console.WriteLine($"//flat-readme {name,-36} {best,9:F2} us"); } Time("1 light FEC", () => l1.CompileFast(true)); @@ -160,6 +165,18 @@ void Time(string name, Action body) Time("2 flat->light->FEC (CompileFast)", () => fe2.CompileFast>>()); Time("2 flat->light only", () => fe2.ToLightExpression()); Time("2 flat native (nested lambda)", () => fe2.TryCompileFlat>>()); + Time("floor: collect only", () => fe2.TryCollectFlatInfo()); + Time("floor: 2 bare DynamicMethods", () => + { + for (var k = 0; k < 2; ++k) + { + var dm = new System.Reflection.Emit.DynamicMethod(string.Empty, typeof(int), new[] { typeof(FastExpressionCompiler.LightExpression.ExpressionCompiler.ArrayClosure), typeof(int) }, typeof(FastExpressionCompiler.LightExpression.ExpressionCompiler.ArrayClosure), true); + var il = dm.GetILGenerator(); + il.Emit(System.Reflection.Emit.OpCodes.Ldc_I4_1); + il.Emit(System.Reflection.Emit.OpCodes.Ret); + dm.CreateDelegate(typeof(Func), null); + } + }); } public class FlatPoint @@ -293,6 +310,23 @@ public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively( var f6 = fe6.TryCompileFlat>>(); Ensure(f6 != null && f6(5)() == 10 + 5 + 7, "the captured block variable and the closure constant in the nested lambda"); + // the never-assigned reference-type parameter is captured by value + var fe8 = default(ExprTree); + var s8 = fe8.ParameterOf("s"); + fe8.RootIdx = fe8.Lambda>>(fe8.Lambda>(s8), s8); + var f8 = fe8.TryCompileFlat>>(); + Ensure(f8 != null && f8("abc")() == "abc", "captured string by value"); + + // x => (y, z) => x + y + z: the nested lambda with two parameters and a by-value capture + var fe9 = default(ExprTree); + var x9 = fe9.ParameterOf("x"); + var y9 = fe9.ParameterOf("y"); + var z9 = fe9.ParameterOf("z"); + fe9.RootIdx = fe9.Lambda>>( + fe9.Lambda>(fe9.Add(fe9.Add(x9, y9), z9), y9, z9), x9); + var f9 = fe9.TryCompileFlat>>(); + Ensure(f9 != null && f9(1)(10, 100) == 111, "nested lambda with two parameters"); + // the custom delegate type of the nested lambda is outside the subset and falls back var fe7 = default(ExprTree); var n7 = fe7.ParameterOf("n"); From 78a982c1ebc5ff9bf3b898a49743699f3677c683 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:01:41 +0100 Subject: [PATCH 12/19] Flat native: MemberInit, NewArrayInit, ArrayIndex, Convert; drop via-System from prod; README and Complex tests with alloc numbers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 7 ++ .../FlatExpression.cs | 9 -- .../FlatExpressionCompiler.cs | 78 +++++++++++++++ .../LightExpressionPropertyTests.cs | 98 +++++++++++++++++-- .../LightExpressionTests.cs | 1 + 5 files changed, 178 insertions(+), 15 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index c3f92a52e..002e0a2e0 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -53,3 +53,10 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - 5 SOA of the nodes: NOT TRIED. The collect scan of README 2 is 0.23 us of ~7 us, the floor of 2 bare DynamicMethods is 3 us; the layout is not the bottleneck. - Biggest win was not structural: the per-delegate-type cache (Invoke, param types, open Func type, Curry method). Collect 0.71 -> 0.23 us, README 2 native 9.5 -> ~6.6 us (Light FEC ~10-11, min of 9 rounds). - Todo: the remaining ~3 us over the DynamicMethod floor (IL token resolution, ctx), Convert, value types, Invoke, arrays, loops/try, native-vs-System fuzzer. + +## Step 8: via-System dropped from prod, README + ComplexExpression native (done) +- ToLightExpressionViaSystem / FromLightExprViaSystem removed from src; kept as test-only helpers for the round-trip fuzz. +- Native now: MemberInit (field/property bindings), NewArrayInit, ArrayIndex (rank 1), reference Convert (castclass, no user-defined/numeric/boxing). Hoisted `() => new X(a, b)`, manual lambda, `n => () => n + 1` and Complex all compile without fallback. +- Release (noisy), us / bytes per op: README 1 flat 4.4 / 1544 vs Light 7.7 / 1080 (prebuilt); Complex flat native 9.7 / 2016 vs Light FEC 13.8 / 1656; hoisted flat 5.1 / 1832 vs Light 7.6 / 1232. Two bare DynamicMethods floor: 2304 B. +- Allocation: flat allocates more than Light on one-lambda trees (FlatCtx, constants copy, delegate closure), but nothing here is above the DynamicMethod floor. TRIED nothing more; caching StrongBox Value/ctor lookups NOT DONE (only on assigned-captured path, not in README cases). +- Todo: alloc of FlatCtx (struct on stack), skip constants copy when the tree constants array can be reused, value-type members, Invoke, loops/try, native-vs-System fuzzer. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 4b4f1c084..a4922df21 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -928,10 +928,6 @@ public ushort FromLightExpr(LightExpression.Expression expr) return (ushort)RootIdx; } - /// The slower oracle of : Light -> System -> Flat. - [RequiresUnreferencedCode(LightExpression.Trimming.Message)] - public ushort FromLightExprViaSystem(LightExpression.Expression expr) => FromSysExpr(expr.ToExpression()); - [RequiresUnreferencedCode(LightExpression.Trimming.Message)] private ushort AddSysExpression(SysExpr expr) { @@ -1231,11 +1227,6 @@ public SysExpr ToExpression() => ? new SysExprBuilder(this).ReadExpression(RootIdx) : throw new InvalidOperationException("Flat expression tree is empty."); - /// Reconstructs the flat tree as a LightExpression tree via System.Linq expressions; the slower oracle of . - [RequiresUnreferencedCode(LightExpression.Trimming.Message)] - public LightExpression.Expression ToLightExpressionViaSystem() => - LightExpression.FromSysExpressionConverter.ToLightExpression(ToExpression()); - /// Structurally compares two flat expression trees. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(ExprTree other) => diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index d9253937e..c213763b0 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -285,12 +285,39 @@ private static Result CollectFlat(this ref ExprTree tree) ref var n = ref tree.Nodes.GetSurePresentRef(i); if (n.Kind == ExprNodeKind.BlockExprs) continue; + if (n.Kind == ExprNodeKind.MemberAssignment) + { + // the instance field or the property with the setter, the binding is emitted by the MemberInit + if (!IsFlatType(n.Type) || !(n.Obj is FieldInfo bf && !bf.IsInitOnly && !bf.IsLiteral && !bf.IsStatic || + n.Obj is PropertyInfo bp && bp.GetSetMethod(true) is { IsStatic: false })) + return Result.NotSupported_UnknownExpression; + continue; + } if (n.Kind != ExprNodeKind.Expression) return Result.NotSupported_UnknownExpression; var t = n.Type; switch (n.NodeType) { + case ExprType.MemberInit: + if (!IsFlatType(t)) return Result.NotSupported_UnknownExpression; + break; + case ExprType.NewArrayInit: + if (!t.IsArray || !IsFlatType(t.GetElementType())) return Result.NotSupported_UnknownExpression; + break; + case ExprType.ArrayIndex: + { + ref var arr = ref tree.Nodes.GetSurePresentRef(n.ChildIdx); + if (!arr.Type.IsArray || arr.Type.GetArrayRank() != 1 || !IsFlatType(t) || + tree.Nodes.GetSurePresentRef(arr.NextIdx).Type != typeof(int)) + return Result.NotSupported_UnknownExpression; + break; + } + case ExprType.Convert: + // the reference cast only, no user-defined conversion method, no boxing and no numeric conversions + if (n.Obj != null || t.IsValueType || !IsFlatType(t) || tree.Nodes.GetSurePresentRef(n.ChildIdx).Type.IsValueType) + return Result.NotSupported_UnknownExpression; + break; case ExprType.Add: case ExprType.Subtract: case ExprType.Multiply: @@ -566,6 +593,57 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, break; } + case ExprType.MemberInit: + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + var bindingIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + for (var i = 1; i < n.ChildCount; ++i) + { + ref var binding = ref tree.Nodes.GetSurePresentRef(bindingIdx); + il.Emit(OpCodes.Dup); + EmitFlat(ref tree, binding.ChildIdx, il, ref vars, ctx, false); + if (binding.Obj is FieldInfo field) + il.Emit(OpCodes.Stfld, field); + else + il.Emit(OpCodes.Callvirt, ((PropertyInfo)binding.Obj).GetSetMethod(true)); + bindingIdx = binding.NextIdx; + } + break; + } + + case ExprType.NewArrayInit: + { + var elemType = n.Type.GetElementType(); + EmittingVisitor.EmitLoadConstantInt(il, n.ChildCount); + il.Emit(OpCodes.Newarr, elemType); + var elemIdx = n.ChildIdx; + for (var i = 0; i < n.ChildCount; ++i) + { + il.Emit(OpCodes.Dup); + EmittingVisitor.EmitLoadConstantInt(il, i); + EmitFlat(ref tree, elemIdx, il, ref vars, ctx, false); + il.Emit(elemType == typeof(int) ? OpCodes.Stelem_I4 : elemType == typeof(bool) ? OpCodes.Stelem_I1 : OpCodes.Stelem_Ref); + elemIdx = tree.Nodes.GetSurePresentRef(elemIdx).NextIdx; + } + break; + } + + case ExprType.ArrayIndex: + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ctx, false); + il.Emit(n.Type == typeof(int) ? OpCodes.Ldelem_I4 : n.Type == typeof(bool) ? OpCodes.Ldelem_U1 : OpCodes.Ldelem_Ref); + break; + } + + case ExprType.Convert: + { + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + if (!n.Type.IsAssignableFrom(tree.Nodes.GetSurePresentRef(n.ChildIdx).Type)) + il.Emit(OpCodes.Castclass, n.Type); + break; + } + case ExprType.Not: EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Ldc_I4_0); diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index daa084699..3bfd7f47f 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -63,6 +63,13 @@ public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() Ensure(fe2.CompileFast>()(1) == typeof(string), "fallback result"); } + // the slower oracles of the direct conversions: through System.Linq expressions + static FastExpressionCompiler.LightExpression.Expression FlatToLightViaSystem(ref ExprTree tree) => + FromSysExpressionConverter.ToLightExpression(tree.ToExpression()); + + static void LightToFlatViaSystem(ref ExprTree tree, FastExpressionCompiler.LightExpression.Expression light) => + tree.FromSysExpr(light.ToExpression()); + /// Prints how long the phases of the compilation take on the flat and on the light trees; asserts nothing but the agreement. public void Experiment_compare_flat_and_light_compile_phases() { @@ -97,10 +104,10 @@ void Time(string name, int repeat, Action body) }); Time("compile: flat native (collect+emit)", reps, i => flats[i].TryCompileFlat>()); Time("compile: light FEC (collect+emit)", reps, i => lights[i].CompileFast(ifFastFailedReturnNull: true)); - Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpressionViaSystem()).CompileFast>(true)); - Time("convert only: flat->sys->light", reps, i => flats[i].ToLightExpressionViaSystem()); + Time("compile: flat->sys->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)FlatToLightViaSystem(ref flats[i])).CompileFast>(true)); + Time("convert only: flat->sys->light", reps, i => FlatToLightViaSystem(ref flats[i])); Time("convert only: flat->light direct", reps, i => flats[i].ToLightExpression()); - Time("convert only: light->sys->flat", reps, i => { var t = default(ExprTree); t.FromLightExprViaSystem(lights[i]); }); + Time("convert only: light->sys->flat", reps, i => { var t = default(ExprTree); LightToFlatViaSystem(ref t, lights[i]); }); Time("convert only: light->flat direct", reps, i => { var t = default(ExprTree); t.FromLightExpr(lights[i]); }); Time("compile: flat->light->FEC", reps, i => ((FastExpressionCompiler.LightExpression.LambdaExpression)flats[i].ToLightExpression()).CompileFast>(true)); Time("compile: System.Compile", 1, i => lights[i].CompileSys()); @@ -151,7 +158,10 @@ void Time(string name, Action body) for (var i = 0; i < reps; ++i) body(); best = Math.Min(best, sw.Elapsed.TotalMilliseconds * 1000 / reps); } - Console.WriteLine($"//flat-readme {name,-36} {best,9:F2} us"); + var allocated = GC.GetAllocatedBytesForCurrentThread(); + for (var i = 0; i < reps; ++i) body(); + allocated = (GC.GetAllocatedBytesForCurrentThread() - allocated) / reps; + Console.WriteLine($"//flat-readme {name,-36} {best,9:F2} us {allocated,7} B"); } Time("1 light FEC", () => l1.CompileFast(true)); @@ -166,6 +176,20 @@ void Time(string name, Action body) Time("2 flat->light only", () => fe2.ToLightExpression()); Time("2 flat native (nested lambda)", () => fe2.TryCompileFlat>>()); Time("floor: collect only", () => fe2.TryCollectFlatInfo()); + + var cLight = CreateComplexLightExpression(); + var cFlat = CreateComplexFlatExpression(); + Time("C light FEC", () => cLight.CompileFast(true)); + Time("C flat->light->FEC (CompileFast)", () => cFlat.CompileFast>()); + Time("C flat->light only", () => cFlat.ToLightExpression()); + Time("C flat native", () => cFlat.TryCompileFlat>()); + + var hoistedB = new ReadmeB(); + System.Linq.Expressions.Expression> hoisted = () => new ReadmeX(a, hoistedB); + var hFlat = hoisted.ToFlatExpression(); + var hLight = (FastExpressionCompiler.LightExpression.LambdaExpression)hFlat.ToLightExpression(); + Time("H light FEC", () => hLight.CompileFast>(true)); + Time("H flat native", () => hFlat.TryCompileFlat>()); Time("floor: 2 bare DynamicMethods", () => { for (var k = 0; k < 2; ++k) @@ -179,6 +203,64 @@ void Time(string name, Action body) }); } + /// All README examples and the complex benchmark expression (MemberInit, Convert, ArrayIndex, NewArrayInit) compile natively, no fallback. + public void Flat_compiles_all_the_readme_examples_natively() + { + var a = new ReadmeA(); + var b = new ReadmeB(); + + // hoisted: () => new X(a, b), the closure fields of the C# compiler display class + System.Linq.Expressions.Expression> hoisted = () => new ReadmeX(a, b); + var hFlat = hoisted.ToFlatExpression(); + var hf = hFlat.TryCompileFlat>(); + Ensure(hf != null, "hoisted: native"); + var hx = hf(); + Ensure(hx.A == a && hx.B == b, "hoisted: result"); + + // manually composed, System expression converted to flat + var bp = SysExpr.Parameter(typeof(ReadmeB), "b"); + var manual = SysExpr.Lambda>( + SysExpr.New(typeof(ReadmeX).GetConstructors()[0], SysExpr.Constant(a, typeof(ReadmeA)), bp), bp); + var mFlat = manual.ToFlatExpression(); + var mf = mFlat.TryCompileFlat>(); + Ensure(mf != null && mf(b).A == a && mf(b).B == b, "manual: native"); + + // the complex expression + var cFlat = CreateComplexFlatExpression(); + var cf = cFlat.TryCompileFlat>(); + Ensure(cf != null, "complex: native"); + var input = new object[12]; + for (var i = 0; i < input.Length; ++i) + input[i] = i + ""; + var cx = (A)cf(input); + Ensure(cx.Sop == "11" && cx.Prop != null && cx.Bop != null, "complex: result"); + Ensure(cf(input) != cx, "complex: a new object on each call"); + + // the same complex expression made of the System expression + var cSys = CreateComplexExpression().ToFlatExpression(); + var cf2 = cSys.TryCompileFlat>(); + Ensure(cf2 != null && ((A)cf2(input)).Sop == "11", "complex from System: native"); + + // the bad cast throws the same as the System does + input[11] = 42; + var threw = false; + try { cf(input); } catch (InvalidCastException) { threw = true; } + Ensure(threw, "complex: the bad cast throws"); + + // n => () => n + 1 + var fe = default(ExprTree); + var n = fe.ParameterOf("n"); + fe.RootIdx = fe.Lambda>>(fe.Lambda>(fe.Add(n, fe.ConstantInt(1))), n); + Ensure(fe.TryCompileFlat>>()?.Invoke(41)() == 42, "n => () => n + 1: native"); + + // the array of ints and bools, the index, the element type fits + var fa = default(ExprTree); + var arr = fa.ParameterOf("arr"); + fa.RootIdx = fa.Lambda>( + fa.Add(fa.ArrayIndex(arr, fa.ConstantInt(1)), fa.ArrayIndex(fa.NewArrayInit(typeof(int), fa.ConstantInt(5), fa.ConstantInt(6)), fa.ConstantInt(0))), arr); + Ensure(fa.TryCompileFlat>()?.Invoke(new[] { 1, 2 }) == 7, "int array index and init"); + } + public class FlatPoint { public int X; @@ -219,7 +301,10 @@ public void Flat_compile_handles_new_call_and_member_access_natively() var held = new FlatPoint(7, 8); fe2.RootIdx = fe2.Lambda>( fe2.NewArrayInit(typeof(object), fe2.Constant(held, t), fe2.Constant("s"), fe2.ConstantNull(t), p2), p2); - Ensure(fe2.TryCompileFlat>() == null, "the array init is outside the subset"); + var arrFn = fe2.TryCompileFlat>(); + Ensure(arrFn != null, "the array init is native"); + var arrRes = arrFn(held); + Ensure(arrRes.Length == 4 && arrRes[0] == held && (string)arrRes[1] == "s" && arrRes[2] == null && arrRes[3] == held, "the array init result"); var fe3 = default(ExprTree); var p3 = fe3.ParameterOf("p"); @@ -412,7 +497,7 @@ private static bool CheckOptionalChildren(int bits) var light = fe.ToLightExpression(); Ensure(fe.Equals(light.ToFlatExpression()), "flat->light->flat roundtrip"); var viaSys = default(ExprTree); - viaSys.FromLightExprViaSystem(light); + LightToFlatViaSystem(ref viaSys, light); Ensure(viaSys.Equals(light.ToFlatExpression()), "direct light->flat equals via-system"); int Count(Func match) @@ -739,6 +824,7 @@ public void Can_compile_generated_flat_expression_natively() { } public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() { } public void Experiment_compare_flat_and_light_compile_phases() { } public void Flat_compile_handles_new_call_and_member_access_natively() { } + public void Flat_compiles_all_the_readme_examples_natively() { } public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively() { } public void Flat_compiles_the_readme_expressions_and_compares_the_time() { } #endif diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 6d968c0d2..ee33a2e0e 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -41,6 +41,7 @@ public int Run() Can_compile_generated_flat_expression_natively(); Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); Flat_compile_handles_new_call_and_member_access_natively(); + Flat_compiles_all_the_readme_examples_natively(); Flat_compile_handles_nested_lambdas_and_captured_variables_natively(); Experiment_compare_flat_and_light_compile_phases(); Flat_compiles_the_readme_expressions_and_compares_the_time(); From fb965447555faee3704b1260e18714b922f99548 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:07:07 +0100 Subject: [PATCH 13/19] Flat native: per-thread compile context, cached StrongBox members (-176 B per compile) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 7 ++ .../FlatExpressionCompiler.cs | 68 ++++++++++++------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 002e0a2e0..95f35bbef 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -60,3 +60,10 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - Release (noisy), us / bytes per op: README 1 flat 4.4 / 1544 vs Light 7.7 / 1080 (prebuilt); Complex flat native 9.7 / 2016 vs Light FEC 13.8 / 1656; hoisted flat 5.1 / 1832 vs Light 7.6 / 1232. Two bare DynamicMethods floor: 2304 B. - Allocation: flat allocates more than Light on one-lambda trees (FlatCtx, constants copy, delegate closure), but nothing here is above the DynamicMethod floor. TRIED nothing more; caching StrongBox Value/ctor lookups NOT DONE (only on assigned-captured path, not in README cases). - Todo: alloc of FlatCtx (struct on stack), skip constants copy when the tree constants array can be reused, value-type members, Invoke, loops/try, native-vs-System fuzzer. + +## Step 9: allocations (done, measured) +- FlatCtx is one [ThreadStatic] instance (it embeds the 8-slot capture buffer, was allocated per compile); Constants is cleared after the compile. DONE. +- StrongBox type/ctor/Value field cached per variable type (FlatBox). DONE. +- Release B/op before -> after: README 1 flat 1544 -> 1368, README 2 3024 -> 2848, Complex 2016 -> 1840, hoisted 1832 -> 1656. Light FEC: 1080 / 3000 / 1656 / 1232. Floor of one DynamicMethod is ~1150 B. +- Left: object[] constants copy (could reuse the tree array if ImTools SmallList exposed its heap array), the ArrayClosure + delegate (needed), ILGenerator/LocalBuilder (BCL). Flat is now ~290 B over Light on one-lambda trees, below Light on README 2. +- Todo: ImTools SmallList hand-over of its backing array to skip the copy; check what else allocates in the 290 B with an allocation profiler. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index c213763b0..c21ae4828 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -40,7 +40,9 @@ private struct FlatCapture public bool ByValue; } - /// The state of one compilation. The only allocation is this object, and the constants array if any. + [ThreadStatic] private static FlatCtx _flatCtx; + + /// The state of one compilation, one instance per thread. private sealed class FlatCtx { public object[] Constants; // the closure constants, then the open delegates of the nested lambdas @@ -205,13 +207,23 @@ private static bool FlatLambdaFits(ref ExprTree tree, ref ExprNode lambda, bool return !nested || info.OpenType != null; } - private static Type FlatBoxType(Type t) => typeof(StrongBox<>).MakeGenericType(t); + private sealed class FlatBoxInfo { public Type Type; public ConstructorInfo Ctor; public FieldInfo Value; } + + private static readonly ConcurrentDictionary FlatBoxes = new(); + + private static FlatBoxInfo FlatBox(Type t) + { + if (FlatBoxes.TryGetValue(t, out var info)) + return info; + var boxType = typeof(StrongBox<>).MakeGenericType(t); + return FlatBoxes.GetOrAdd(t, new FlatBoxInfo { Type = boxType, Ctor = boxType.GetConstructor(Type.EmptyTypes), Value = boxType.GetField("Value") }); + } private static LocalBuilder FlatNewBox(ILGenerator il, Type t) { - var boxType = FlatBoxType(t); - var box = il.DeclareLocal(boxType); - il.Emit(OpCodes.Newobj, boxType.GetConstructor(Type.EmptyTypes)); + var info = FlatBox(t); + var box = il.DeclareLocal(info.Type); + il.Emit(OpCodes.Newobj, info.Ctor); il.Emit(OpCodes.Stloc, box); return box; } @@ -233,7 +245,7 @@ private static void FlatLoadBox(ILGenerator il, in FlatVar v) return; } FlatLoadNonPassed(il, v.Slot); - il.Emit(OpCodes.Castclass, FlatBoxType(v.Type)); + il.Emit(OpCodes.Castclass, FlatBox(v.Type).Type); } private static void FlatLoadVar(ILGenerator il, in FlatVar v) @@ -241,7 +253,7 @@ private static void FlatLoadVar(ILGenerator il, in FlatVar v) if (v.Boxed) { FlatLoadBox(il, in v); - il.Emit(OpCodes.Ldfld, FlatBoxType(v.Type).GetField("Value")); + il.Emit(OpCodes.Ldfld, FlatBox(v.Type).Value); } else if (v.Slot >= 0) { @@ -383,24 +395,34 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where return null; var constantCount = tree.ClosureConstants.Count; - var ctx = new FlatCtx { NextSlot = constantCount }; - var slotCount = constantCount + tree.LambdaCount - 1; // the slot per nested lambda for its open delegate - if (slotCount != 0) + // the context is reused per thread to not allocate it (the capture buffer is big); the compile is not re-entrant + var ctx = _flatCtx ??= new FlatCtx(); + ctx.NextSlot = constantCount; + ctx.Captures.Count = 0; + try { - ctx.Constants = new object[slotCount]; - for (var i = 0; i < constantCount; ++i) - ctx.Constants[i] = tree.ClosureConstants[i]; - } + var slotCount = constantCount + tree.LambdaCount - 1; // the slot per nested lambda for its open delegate + if (slotCount != 0) + { + ctx.Constants = new object[slotCount]; + for (var i = 0; i < constantCount; ++i) + ctx.Constants[i] = tree.ClosureConstants[i]; + } - if (tree.LambdaCount > 1) + if (tree.LambdaCount > 1) + { + var declared = default(SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>>); + var lambdas = default(SmallList, NoArrayPool>); + FlatFindCaptures(ref tree, tree.RootIdx, ctx, ref declared, ref lambdas); + } + + var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); + return (TDelegate)(object)EmitFlatLambda(ref tree, tree.RootIdx, ctx, typeof(TDelegate), closure); + } + finally { - var declared = default(SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>>); - var lambdas = default(SmallList, NoArrayPool>); - FlatFindCaptures(ref tree, tree.RootIdx, ctx, ref declared, ref lambdas); + ctx.Constants = null; // do not keep the constants alive } - - var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); - return (TDelegate)(object)EmitFlatLambda(ref tree, tree.RootIdx, ctx, typeof(TDelegate), closure); } /// Emits the lambda into its own DynamicMethod. The root is closed over the closure; the nested one @@ -424,7 +446,7 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx var box = FlatNewBox(il, par.Type); il.Emit(OpCodes.Ldloc, box); il.Emit(OpCodes.Ldarg, (short)i); - il.Emit(OpCodes.Stfld, FlatBoxType(par.Type).GetField("Value")); + il.Emit(OpCodes.Stfld, FlatBox(par.Type).Value); vars.Add(new FlatVar { Id = par.ChildIdx, Type = par.Type, Arg = -1, Local = box, Slot = -1, Boxed = true }); } else @@ -494,7 +516,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, result = il.DeclareLocal(v.Type); il.Emit(OpCodes.Stloc, result); } - il.Emit(OpCodes.Stfld, FlatBoxType(v.Type).GetField("Value")); + il.Emit(OpCodes.Stfld, FlatBox(v.Type).Value); if (result != null) il.Emit(OpCodes.Ldloc, result); return; From f1fbf6e6c9b1e5723e68ce7984d9b7847fd640b3 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:21:01 +0100 Subject: [PATCH 14/19] Flat native: reuse FEC pooled ILGenerator (-240 B per compile) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 5 +++++ .../FlatExpressionCompiler.cs | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 95f35bbef..a9e0df6a1 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -67,3 +67,8 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - Release B/op before -> after: README 1 flat 1544 -> 1368, README 2 3024 -> 2848, Complex 2016 -> 1840, hoisted 1832 -> 1656. Light FEC: 1080 / 3000 / 1656 / 1232. Floor of one DynamicMethod is ~1150 B. - Left: object[] constants copy (could reuse the tree array if ImTools SmallList exposed its heap array), the ArrayClosure + delegate (needed), ILGenerator/LocalBuilder (BCL). Flat is now ~290 B over Light on one-lambda trees, below Light on README 2. - Todo: ImTools SmallList hand-over of its backing array to skip the copy; check what else allocates in the 290 B with an allocation profiler. + +## Step 10: reuse the FEC pooling (done, measured) +- Learned from FEC TryCompileBoundToFirstClosureParam: it rents/frees the per-thread ILGenerator (DynamicMethodHacks.RentPooledOrNewILGenerator/FreePooledILGenerator), pools the closure+param types arrays, interprets Func constants, and sizes one object[] for constants+nested lambdas. Flat now uses the ILGenerator pool. DONE: -240 B per compile. +- Release B/op now: README 1 flat 1128 (Light 1080), README 2 2608 (3000), Complex 1448 (1656), hoisted 1416 (1232). Time: flat 3.1 / 6.6 / 6.6 / 3.3 us vs Light 4.1 / 10.9 / 9.2 / 4.8. +- Todo: Func interpreter shortcut (Interpreter is FEC's), the pooled local-var path (GetNextLocalVarLocation), create+compile rows (tree building cost is not measured yet), the nested ILGenerator is not pooled while the parent holds it (same as FEC). diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index c21ae4828..3c82f8005 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -434,7 +434,7 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx var info = FlatGetDelegateInfo(lambda.Type); var paramCount = info.ParamTypes.Length; var dynMethod = new DynamicMethod(string.Empty, info.ReturnType, info.ClosureParamTypes, typeof(ArrayClosure), true); - var il = dynMethod.GetILGenerator(); + var il = DynamicMethodHacks.RentPooledOrNewILGenerator(dynMethod, info.ReturnType, info.ClosureParamTypes); var vars = default(SmallList, NoArrayPool>); var parIdx = tree.Nodes.GetSurePresentRef(bodyIdx).NextIdx; @@ -463,7 +463,9 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx EmitFlat(ref tree, bodyIdx, il, ref vars, ctx, false); il.Emit(OpCodes.Ret); - return dynMethod.CreateDelegate(delegateType ?? info.OpenType, closure); + var result = dynMethod.CreateDelegate(delegateType ?? info.OpenType, closure); + DynamicMethodHacks.FreePooledILGenerator(dynMethod, il); // only after the CreateDelegate, as FEC does + return result; } private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, From b0f4d148a9110f1230ef25f06aa3fcd295067c5f Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:32:36 +0100 Subject: [PATCH 15/19] Flat native: fold constant Func, FEC local var indexes, create+compile benchmark rows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 7 ++ .../FlatExpressionCompiler.cs | 86 +++++++++++++++---- .../LightExpressionPropertyTests.cs | 36 ++++++++ 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index a9e0df6a1..c4de2cb43 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -72,3 +72,10 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - Learned from FEC TryCompileBoundToFirstClosureParam: it rents/frees the per-thread ILGenerator (DynamicMethodHacks.RentPooledOrNewILGenerator/FreePooledILGenerator), pools the closure+param types arrays, interprets Func constants, and sizes one object[] for constants+nested lambdas. Flat now uses the ILGenerator pool. DONE: -240 B per compile. - Release B/op now: README 1 flat 1128 (Light 1080), README 2 2608 (3000), Complex 1448 (1656), hoisted 1416 (1232). Time: flat 3.1 / 6.6 / 6.6 / 3.3 us vs Light 4.1 / 10.9 / 9.2 / 4.8. - Todo: Func interpreter shortcut (Interpreter is FEC's), the pooled local-var path (GetNextLocalVarLocation), create+compile rows (tree building cost is not measured yet), the nested ILGenerator is not pooled while the parent holds it (same as FEC). + +## Step 11: Func fold, FEC local indexes, create+compile rows (done, measured) +- Func over inline int/bool constants is folded (FlatFold) to the shared Interpreter.TrueFunc/FalseFunc, no DynamicMethod. DONE (FEC does the same with its Expression-bound Interpreter, which cannot read flat nodes, so the folder is a small flat one). +- Locals use FEC il.GetNextLocalVarIndex + EmitLoad/StoreLocalVariable: no LocalBuilder object, short opcodes. DONE (no change on the README cases, they have no locals; saves 1 LocalBuilder per block variable / box / assign result). +- FEC closure info: nothing more to take, FEC allocates the same ArrayClosure + object[] and the NestedLambdaInfo objects that flat does not need. +- Create+compile Release (us / B): README 1 flat 3.3 / 1128 vs Light 4.6 / 1210; README 2 flat 7.1 / 2608 vs Light 11.8 / 3144; Complex flat 7.1 / 1448 vs Light 9.9 / 2176. The flat tree build itself allocates 0 B. +- Todo: README 1 / hoisted compile is still 48-184 B above Light prebuilt; profile the rest (ArrayClosure + constants copy + delegate + DynamicMethod). diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index 3c82f8005..375ec1062 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -25,7 +25,7 @@ struct FlatVar public ushort Id; public Type Type; public short Arg; // the argument index (the closure is 0) or -1 - public LocalBuilder Local; // the local, or the local StrongBox if Boxed + public int Local; // the local index (no LocalBuilder allocated), or the local StrongBox if Boxed public short Slot; // the item of the closure NonPassedParams if the variable is captured from the outer lambda, or -1 public bool Boxed; // the variable is assigned and used by a nested lambda, so it lives in the StrongBox shared with that lambda } @@ -219,13 +219,12 @@ private static FlatBoxInfo FlatBox(Type t) return FlatBoxes.GetOrAdd(t, new FlatBoxInfo { Type = boxType, Ctor = boxType.GetConstructor(Type.EmptyTypes), Value = boxType.GetField("Value") }); } - private static LocalBuilder FlatNewBox(ILGenerator il, Type t) + /// Creates the StrongBox and stores it into the new local, returns the local index. + private static int FlatNewBox(ILGenerator il, Type t) { var info = FlatBox(t); - var box = il.DeclareLocal(info.Type); il.Emit(OpCodes.Newobj, info.Ctor); - il.Emit(OpCodes.Stloc, box); - return box; + return EmittingVisitor.EmitStoreLocalVariable(il, info.Type); } /// Loads the item of the closure NonPassedParams: the StrongBox or the boxed value of the captured variable. @@ -241,7 +240,7 @@ private static void FlatLoadBox(ILGenerator il, in FlatVar v) { if (v.Slot < 0) { - il.Emit(OpCodes.Ldloc, v.Local); + EmittingVisitor.EmitLoadLocalVariable(il, v.Local); return; } FlatLoadNonPassed(il, v.Slot); @@ -261,7 +260,7 @@ private static void FlatLoadVar(ILGenerator il, in FlatVar v) il.Emit(OpCodes.Unbox_Any, v.Type); } else if (v.Arg >= 0) il.Emit(OpCodes.Ldarg, v.Arg); - else il.Emit(OpCodes.Ldloc, v.Local); + else EmittingVisitor.EmitLoadLocalVariable(il, v.Local); } private static FlatVar FlatFindVar(ref SmallList, NoArrayPool> vars, ushort id) @@ -385,6 +384,60 @@ private static Result CollectFlat(this ref ExprTree tree) return tree.LambdaCount != 0 && tree.Nodes.GetSurePresentRef(tree.RootIdx).NodeType == ExprType.Lambda ? Result.OK : Result.NotSupported_UnknownExpression; } + /// Folds the tree of the inline int/bool constants and the operations over them (the bool as 0/1). False if anything else is inside. + private static bool FlatFold(ref ExprTree tree, int idx, out int result) + { + ref var n = ref tree.Nodes.GetSurePresentRef(idx); + result = 0; + if (n.Kind != ExprNodeKind.Expression) + return false; + if (n.NodeType == ExprType.Constant) + { + if (!ReferenceEquals(n.Obj, ExprNode.InlineValueMarker)) + return false; + result = (int)n.InlineValue; + return true; + } + if (n.NodeType == ExprType.Not) + { + if (!FlatFold(ref tree, n.ChildIdx, out var v)) + return false; + result = v == 0 ? 1 : 0; + return true; + } + if (n.NodeType == ExprType.Conditional) + { + var ifTrueIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + var ifFalseIdx = tree.Nodes.GetSurePresentRef(ifTrueIdx).NextIdx; + return FlatFold(ref tree, n.ChildIdx, out var test) && FlatFold(ref tree, test != 0 ? ifTrueIdx : ifFalseIdx, out result); + } + + var nodeType = n.NodeType; + if (nodeType != ExprType.AndAlso && nodeType != ExprType.OrElse && nodeType != ExprType.Add && nodeType != ExprType.Subtract && + nodeType != ExprType.Multiply && nodeType != ExprType.Equal && nodeType != ExprType.GreaterThan) + return false; + if (!FlatFold(ref tree, n.ChildIdx, out var l)) + return false; + var rightIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; + if (nodeType == ExprType.AndAlso && l == 0 || nodeType == ExprType.OrElse && l != 0) + { + result = l; // short-circuit, the right is not even looked at, same as the emit + return true; + } + if (!FlatFold(ref tree, rightIdx, out var r)) + return false; + result = nodeType switch + { + ExprType.Add => l + r, + ExprType.Subtract => l - r, + ExprType.Multiply => l * r, + ExprType.Equal => l == r ? 1 : 0, + ExprType.GreaterThan => l > r ? 1 : 0, + _ => r // AndAlso / OrElse with the left not short-circuiting + }; + return true; + } + /// Returns null if the tree is not the supported subset or the delegate type is not the lambda type. public static TDelegate TryCompileFlat(this ref ExprTree tree) where TDelegate : class { @@ -394,6 +447,10 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where if (tree.Nodes.GetSurePresentRef(tree.RootIdx).Type != typeof(TDelegate)) return null; + // the constant bool lambda needs no DynamicMethod at all, same as FEC does with its Interpreter + if (typeof(TDelegate) == typeof(Func) && FlatFold(ref tree, tree.Nodes.GetSurePresentRef(tree.RootIdx).ChildIdx, out var folded)) + return (TDelegate)(object)(folded != 0 ? ExpressionCompiler.Interpreter.TrueFunc : ExpressionCompiler.Interpreter.FalseFunc); + var constantCount = tree.ClosureConstants.Count; // the context is reused per thread to not allocate it (the capture buffer is big); the compile is not re-entrant var ctx = _flatCtx ??= new FlatCtx(); @@ -444,7 +501,7 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx if (ctx.IsBoxed(par.ChildIdx)) { var box = FlatNewBox(il, par.Type); - il.Emit(OpCodes.Ldloc, box); + EmittingVisitor.EmitLoadLocalVariable(il, box); il.Emit(OpCodes.Ldarg, (short)i); il.Emit(OpCodes.Stfld, FlatBox(par.Type).Value); vars.Add(new FlatVar { Id = par.ChildIdx, Type = par.Type, Arg = -1, Local = box, Slot = -1, Boxed = true }); @@ -511,22 +568,21 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, { FlatLoadBox(il, in v); EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); - LocalBuilder result = null; + var result = -1; if (!ignoreResult) { il.Emit(OpCodes.Dup); - result = il.DeclareLocal(v.Type); - il.Emit(OpCodes.Stloc, result); + result = EmittingVisitor.EmitStoreLocalVariable(il, v.Type); } il.Emit(OpCodes.Stfld, FlatBox(v.Type).Value); - if (result != null) - il.Emit(OpCodes.Ldloc, result); + if (result != -1) + EmittingVisitor.EmitLoadLocalVariable(il, result); return; } EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); if (!ignoreResult) il.Emit(OpCodes.Dup); - if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else il.Emit(OpCodes.Stloc, v.Local); + if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else EmittingVisitor.EmitStoreLocalVariable(il, v.Local); return; } @@ -721,7 +777,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, if (ctx.IsBoxed(v.ChildIdx)) vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = FlatNewBox(il, v.Type), Slot = -1, Boxed = true }); else - vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = il.DeclareLocal(v.Type), Slot = -1 }); + vars.Add(new FlatVar { Id = v.ChildIdx, Type = v.Type, Arg = -1, Local = il.GetNextLocalVarIndex(v.Type), Slot = -1 }); varIdx = v.NextIdx; } diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 3bfd7f47f..c241e5b99 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -177,6 +177,34 @@ void Time(string name, Action body) Time("2 flat native (nested lambda)", () => fe2.TryCompileFlat>>()); Time("floor: collect only", () => fe2.TryCollectFlatInfo()); + // the whole cost: create the tree and compile it + Time("1 create+compile light FEC", () => + { + var p = Parameter(typeof(ReadmeB), "b"); + Lambda>(New(ctor, Constant(a, typeof(ReadmeA)), p), p).CompileFast(true); + }); + Time("1 create+compile flat native", () => + { + var f = default(ExprTree); + var p = f.ParameterOf("b"); + f.RootIdx = f.Lambda>(f.New(ctor, f.Constant(a, typeof(ReadmeA)), p), p); + f.TryCompileFlat>(); + }); + Time("2 create+compile light FEC", () => + { + var p = Parameter(typeof(int), "n"); + Lambda>>(Lambda>(Add(p, Constant(1))), p).CompileFast(true); + }); + Time("2 create+compile flat native", () => + { + var f = default(ExprTree); + var p = f.ParameterOf("n"); + f.RootIdx = f.Lambda>>(f.Lambda>(f.MakeBinary(ExpressionType.Add, p, f.ConstantInt(1))), p); + f.TryCompileFlat>>(); + }); + Time("C create+compile light FEC", () => CreateComplexLightExpression().CompileFast(true)); + Time("C create+compile flat native", () => { var f = CreateComplexFlatExpression(); f.TryCompileFlat>(); }); + var cLight = CreateComplexLightExpression(); var cFlat = CreateComplexFlatExpression(); Time("C light FEC", () => cLight.CompileFast(true)); @@ -259,6 +287,14 @@ public void Flat_compiles_all_the_readme_examples_natively() fa.RootIdx = fa.Lambda>( fa.Add(fa.ArrayIndex(arr, fa.ConstantInt(1)), fa.ArrayIndex(fa.NewArrayInit(typeof(int), fa.ConstantInt(5), fa.ConstantInt(6)), fa.ConstantInt(0))), arr); Ensure(fa.TryCompileFlat>()?.Invoke(new[] { 1, 2 }) == 7, "int array index and init"); + + // the constant bool lambda is folded without the compile, FEC does the same with its Interpreter + var ff = default(ExprTree); + ff.RootIdx = ff.Lambda>(ff.MakeBinary(ExpressionType.AndAlso, + ff.MakeBinary(ExpressionType.GreaterThan, ff.MakeBinary(ExpressionType.Add, ff.ConstantInt(2), ff.ConstantInt(3)), ff.ConstantInt(4)), + ff.Not(ff.MakeBinary(ExpressionType.Equal, ff.ConstantInt(1), ff.ConstantInt(2))))); + var folded = ff.TryCompileFlat>(); + Ensure(folded != null && folded() && ReferenceEquals(folded, FastExpressionCompiler.LightExpression.ExpressionCompiler.Interpreter.TrueFunc), "the constant bool is folded"); } public class FlatPoint From 03276f6e9ff4010cc6c28a58c9064a44deab027a Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:37:06 +0100 Subject: [PATCH 16/19] Fix the test for the TryCollectInfo scopeIdx API change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../LightExpressionPropertyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index c241e5b99..7617d0cc1 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -100,7 +100,7 @@ void Time(string name, int repeat, Action body) Time("collect: light TryCollectInfo", reps * 10, i => { var ctx = new ExpressionCompiler.CompilerContext(ExpressionCompiler.ClosureStatus.ToBeCollected, (FastExpressionCompiler.LightExpression.IParameterProvider)lights[i], CompilerFlags.Default); - ExpressionCompiler.TryCollectInfo(ref ctx, lights[i].Body, null, ref ctx.NestedLambdas); + ExpressionCompiler.TryCollectInfo(ref ctx, lights[i].Body, null, ref ctx.NestedLambdas, 0, true); }); Time("compile: flat native (collect+emit)", reps, i => flats[i].TryCompileFlat>()); Time("compile: light FEC (collect+emit)", reps, i => lights[i].CompileFast(ifFastFailedReturnNull: true)); From ac5873d0f45ea6124fbfabcb12135cc8eed2fc83 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:40:57 +0100 Subject: [PATCH 17/19] Flat review: reject value-type instances natively, short constant IL, perf todos Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 5 +++++ .../FlatExpressionCompiler.cs | 13 +++++++++---- .../FlatFromLightExpression.cs | 1 + .../FlatToLightExpression.cs | 1 + .../LightExpressionPropertyTests.cs | 7 +++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index c4de2cb43..72a4a93a3 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -79,3 +79,8 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - FEC closure info: nothing more to take, FEC allocates the same ArrayClosure + object[] and the NestedLambdaInfo objects that flat does not need. - Create+compile Release (us / B): README 1 flat 3.3 / 1128 vs Light 4.6 / 1210; README 2 flat 7.1 / 2608 vs Light 11.8 / 3144; Complex flat 7.1 / 1448 vs Light 9.9 / 2176. The flat tree build itself allocates 0 B. - Todo: README 1 / hoisted compile is still 48-184 B above Light prebuilt; profile the rest (ArrayClosure + constants copy + delegate + DynamicMethod). + +## Step 12: 3 ponytails review rounds over all the Flat code +- R1 (compiler): Call/MemberAccess on a value-type instance (int.ToString) emitted callvirt on the value = invalid IL. FIXED: collect rejects it, falls back to Light (test added). Constants now use EmitLoadConstantInt (short IL forms). Rejected: the delegate-type check before the collect (RootIdx may be unset). +- R2 (converters): ToLightExpression copies the whole ExprTree into its builder; FromLight allocates ushort[] per call. NOT FIXED, @perf todos (needs ref fields on all targets / SmallList ids). Both are fallback/test paths only. +- R3 (invariants): by-value capture is sound only because by-ref params are rejected and Assign is the only emitted mutation; now written next to the check. Debug and Release: 1724 pass. diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index 375ec1062..12f644167 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -343,6 +343,7 @@ private static Result CollectFlat(this ref ExprTree tree) if (!IsFlatType(t) || tree.Nodes.GetSurePresentRef(n.ChildIdx).NodeType != ExprType.Parameter) return Result.NotSupported_UnknownExpression; break; case ExprType.Parameter: + // by-ref parameters are rejected here and Assign is the only emitted mutation, so `AssignedParams` is the complete list for the by-value captures if (!IsFlatType(t) || n.Flags != 0) return Result.NotSupported_UnknownExpression; break; case ExprType.New: @@ -350,10 +351,14 @@ private static Result CollectFlat(this ref ExprTree tree) if (!IsFlatType(t) || n.Obj is not ConstructorInfo) return Result.NotSupported_UnknownExpression; break; case ExprType.Call: - if (!IsFlatType(t) || n.Obj is not MethodInfo m || m.IsGenericMethodDefinition) return Result.NotSupported_UnknownExpression; + // the instance of the value type needs the address and the constrained call, so the Light handles it + if (!IsFlatType(t) || n.Obj is not MethodInfo m || m.IsGenericMethodDefinition || + !m.IsStatic && tree.Nodes.GetSurePresentRef(n.ChildIdx).Type.IsValueType) + return Result.NotSupported_UnknownExpression; break; case ExprType.MemberAccess: - if (!IsFlatType(t) || !(n.Obj is FieldInfo f && !f.IsLiteral || n.Obj is PropertyInfo p && p.CanRead)) + if (!IsFlatType(t) || !(n.Obj is FieldInfo f && !f.IsLiteral || n.Obj is PropertyInfo p && p.CanRead) || + n.ChildCount != 0 && tree.Nodes.GetSurePresentRef(n.ChildIdx).Type.IsValueType) return Result.NotSupported_UnknownExpression; break; case ExprType.Constant: @@ -536,12 +541,12 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, if (ignoreResult) return; if (ReferenceEquals(n.Obj, ExprNode.InlineValueMarker)) - il.Emit(OpCodes.Ldc_I4, (int)n.InlineValue); + EmittingVisitor.EmitLoadConstantInt(il, (int)n.InlineValue); else if (ReferenceEquals(n.Obj, ExprTree.ClosureConstantMarker)) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, ArrayClosureArrayField); - il.Emit(OpCodes.Ldc_I4, (int)n.ChildIdx); + EmittingVisitor.EmitLoadConstantInt(il, n.ChildIdx); il.Emit(OpCodes.Ldelem_Ref); if (n.Type != typeof(object)) il.Emit(OpCodes.Castclass, n.Type); diff --git a/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs index 073ff73c9..519aae9b7 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatFromLightExpression.cs @@ -12,6 +12,7 @@ namespace FastExpressionCompiler.FlatExpression; public partial struct ExprTree { + // todo: @perf the ushort[] per call, use the SmallList of the child ids as the ToLightExpression does [RequiresUnreferencedCode(LightExpression.Trimming.Message)] private ushort[] AddLightArguments(LightExpression.IArgumentProvider args) { diff --git a/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs index 260ef32d0..afcd41c28 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatToLightExpression.cs @@ -21,6 +21,7 @@ public partial struct ExprTree /// Reconstructs the flat tree as a LightExpression tree directly, without the System.Linq expressions in between. /// Mirrors SysExprBuilder: keep the two in sync. [RequiresUnreferencedCode(LightExpression.Trimming.Message)] + // todo: @perf the builder copies the whole ExprTree (the big Stack32 inside); take it by ref when the ref fields are available on all the targets public LightExpression.Expression ToLightExpression() => Nodes.Count != 0 ? new LightExprBuilder(this).ReadExpression(RootIdx) diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index 7617d0cc1..ed416b8f2 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -295,6 +295,13 @@ public void Flat_compiles_all_the_readme_examples_natively() ff.Not(ff.MakeBinary(ExpressionType.Equal, ff.ConstantInt(1), ff.ConstantInt(2))))); var folded = ff.TryCompileFlat>(); Ensure(folded != null && folded() && ReferenceEquals(folded, FastExpressionCompiler.LightExpression.ExpressionCompiler.Interpreter.TrueFunc), "the constant bool is folded"); + + // the instance method of the int needs the address: not emitted natively, the Light fallback does it + var fs = default(ExprTree); + var ns = fs.ParameterOf("n"); + fs.RootIdx = fs.Lambda>(fs.Call(ns, typeof(int).GetMethod("ToString", Type.EmptyTypes)), ns); + Ensure(fs.TryCompileFlat>() == null, "the value type instance is not native"); + Ensure(fs.CompileFast>()(5) == "5", "the value type instance via the fallback"); } public class FlatPoint From c11856d3b6ad82bee454e6d357d920950d0e306c Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 2 Nov 2026 22:43:38 +0100 Subject: [PATCH 18/19] Fuzz the native flat compile: closures and objects/members Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 3 + .../LightExpressionPropertyTests.cs | 136 ++++++++++++++++++ .../LightExpressionTests.cs | 2 + 3 files changed, 141 insertions(+) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index 72a4a93a3..b404bd8da 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -84,3 +84,6 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. - R1 (compiler): Call/MemberAccess on a value-type instance (int.ToString) emitted callvirt on the value = invalid IL. FIXED: collect rejects it, falls back to Light (test added). Constants now use EmitLoadConstantInt (short IL forms). Rejected: the delegate-type check before the collect (RootIdx may be unset). - R2 (converters): ToLightExpression copies the whole ExprTree into its builder; FromLight allocates ushort[] per call. NOT FIXED, @perf todos (needs ref fields on all targets / SmallList ids). Both are fallback/test paths only. - R3 (invariants): by-value capture is sound only because by-ref params are rejected and Assign is the only emitted mutation; now written next to the check. Debug and Release: 1724 pass. + +## Step 13: native fuzzers +CsCheck fuzzers compare native flat vs System: closures (shared StrongBox, by-value capture, 2 instances) and objects (New/Call/Field/Property/MemberInit/ref Conditional). Both assert no fallback. diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs index ed416b8f2..1ba6d5e27 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionPropertyTests.cs @@ -304,6 +304,140 @@ public void Flat_compiles_all_the_readme_examples_natively() Ensure(fs.CompileFast>()(5) == "5", "the value type instance via the fallback"); } + /// The generated closures (outer variables read and assigned by the nested lambda and by the outer one afterwards) + /// must compile natively and agree with System.Linq on the repeated calls, so the shared StrongBox and the by-value capture are checked. + public void Can_fuzz_nested_lambdas_with_captured_variables_natively() + { + Gen.Int[0, int.MaxValue / 2].Sample(seed => + { + var lambda = GenerateClosureLambda(seed); + var fe = lambda.ToFlatExpression(); + var native = fe.TryCompileFlat>>(); + Ensure(native != null, "closure: native compile"); + var expected = lambda.Compile(); + for (var arg = -2; arg <= 2; ++arg) + { + var n = native(arg); + var e = expected(arg); + var n2 = native(arg); // the second instance has its own variables + var e2 = expected(arg); + for (var y = -1; y <= 3; ++y) + { + Ensure(n(y) == e(y), $"closure: result for {arg},{y}"); + Ensure(n2(y + 1) == e2(y + 1), $"closure: the second instance for {arg},{y}"); + } + } + return true; + }, + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GenerateClosureLambda(seed)}"); + } + + // x => { int a = 0, b = 0; f = y => ; a = ; f } + private static System.Linq.Expressions.Expression>> GenerateClosureLambda(int seed) + { + var rnd = new Random(seed); + var x = SysExpr.Parameter(typeof(int), "x"); + var y = SysExpr.Parameter(typeof(int), "y"); + var a = SysExpr.Variable(typeof(int), "a"); + var b = SysExpr.Variable(typeof(int), "b"); + var f = SysExpr.Variable(typeof(Func), "f"); + + SysExpr Gen(int depth, SysExpr[] leaves) + { + if (depth == 0 || rnd.Next(4) == 0) + return rnd.Next(5) == 0 ? SysExpr.Constant(rnd.Next(-3, 4)) : leaves[rnd.Next(leaves.Length)]; + switch (rnd.Next(5)) + { + case 0: return SysExpr.Add(Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + case 1: return SysExpr.Subtract(Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + case 2: return SysExpr.Multiply(Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + case 3: return SysExpr.Condition(SysExpr.GreaterThan(Gen(depth - 1, leaves), Gen(depth - 1, leaves)), Gen(depth - 1, leaves), Gen(depth - 1, leaves)); + default: // assign the captured variable, the value is the result + return SysExpr.Assign(rnd.Next(2) == 0 ? a : b, Gen(depth - 1, leaves)); + } + } + + var inner = SysExpr.Lambda>(Gen(3, new SysExpr[] { x, y, a, b }), y); + var body = SysExpr.Block(typeof(Func), new[] { a, b, f }, + SysExpr.Assign(f, inner), + SysExpr.Assign(a, Gen(2, new SysExpr[] { x, a, b })), + f); + return SysExpr.Lambda>>(body, x); + } + + /// The generated New, Call, Field and Property, MemberInit, and the reference Conditional must compile natively and agree with System.Linq. + public void Can_fuzz_objects_and_members_natively() + { + Gen.Int[0, int.MaxValue / 2].Sample(seed => + { + var lambda = GenerateObjectLambda(seed); + var fe = lambda.ToFlatExpression(); + var native = fe.TryCompileFlat>(); + Ensure(native != null, "objects: native compile"); + var expected = lambda.Compile(); + for (var k = -2; k <= 2; ++k) + { + var p = new FlatPoint(k * 3, k + 7); + var nr = native(p, k); + var er = expected(p, k); + Ensure(nr.X == er.X && nr.Y == er.Y, $"objects: result for {k}"); + } + return true; + }, + iter: 300, threads: 1, seed: "0N0XIzNsQ0O2", print: seed => $"{seed}: {GenerateObjectLambda(seed)}"); + } + + private static System.Linq.Expressions.Expression> GenerateObjectLambda(int seed) + { + var rnd = new Random(seed); + var t = typeof(FlatPoint); + var ctor = t.GetConstructor(new[] { typeof(int), typeof(int) }); + var p = SysExpr.Parameter(t, "p"); + var k = SysExpr.Parameter(typeof(int), "k"); + var q = SysExpr.Variable(t, "q"); + var v = SysExpr.Variable(typeof(int), "v"); + + SysExpr Ref(int depth) + { + if (depth == 0 || rnd.Next(3) == 0) + return rnd.Next(2) == 0 ? p : q; + switch (rnd.Next(3)) + { + case 0: return SysExpr.Property(Ref(depth - 1), "Self"); + case 1: return SysExpr.New(ctor, Int(depth - 1), Int(depth - 1)); + default: return SysExpr.Condition(SysExpr.GreaterThan(Int(depth - 1), Int(depth - 1)), Ref(depth - 1), Ref(depth - 1)); + } + } + + SysExpr Int(int depth) + { + if (depth == 0 || rnd.Next(4) == 0) + return rnd.Next(3) == 0 ? SysExpr.Constant(rnd.Next(-3, 4)) : rnd.Next(2) == 0 ? k : v; + switch (rnd.Next(8)) + { + case 0: return SysExpr.Add(Int(depth - 1), Int(depth - 1)); + case 1: return SysExpr.Multiply(Int(depth - 1), Int(depth - 1)); + case 2: return SysExpr.Field(Ref(depth - 1), "X"); + case 3: return SysExpr.Property(Ref(depth - 1), "Y"); + case 4: return SysExpr.Call(t.GetMethod("Twice"), Int(depth - 1)); + case 5: return SysExpr.Call(Ref(depth - 1), t.GetMethod("Sum"), Int(depth - 1)); + case 6: return SysExpr.Assign(v, Int(depth - 1)); + default: return SysExpr.Condition(SysExpr.Equal(Int(depth - 1), Int(depth - 1)), Int(depth - 1), Int(depth - 1)); + } + } + + var result = rnd.Next(2) == 0 + ? SysExpr.MemberInit(SysExpr.New(ctor, Int(2), Int(2)), SysExpr.Bind(t.GetProperty("Y"), Int(2)), SysExpr.Bind(t.GetField("X"), Int(2))) + : (SysExpr)SysExpr.New(ctor, Int(3), Int(3)); + var body = SysExpr.Block(t, new[] { q, v }, + SysExpr.Assign(q, p), // q must not be null when Ref reads it + SysExpr.Assign(v, k), + SysExpr.Assign(q, Ref(2)), + SysExpr.Assign(v, Int(2)), + result); + return SysExpr.Lambda>(body, p, k); + } + public class FlatPoint { public int X; @@ -868,6 +1002,8 @@ public void Flat_compile_handles_bool_result_and_falls_back_outside_the_subset() public void Experiment_compare_flat_and_light_compile_phases() { } public void Flat_compile_handles_new_call_and_member_access_natively() { } public void Flat_compiles_all_the_readme_examples_natively() { } + public void Can_fuzz_nested_lambdas_with_captured_variables_natively() { } + public void Can_fuzz_objects_and_members_natively() { } public void Flat_compile_handles_nested_lambdas_and_captured_variables_natively() { } public void Flat_compiles_the_readme_expressions_and_compares_the_time() { } #endif diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index ee33a2e0e..64692bc26 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -42,6 +42,8 @@ public int Run() Flat_compile_handles_bool_result_and_falls_back_outside_the_subset(); Flat_compile_handles_new_call_and_member_access_natively(); Flat_compiles_all_the_readme_examples_natively(); + Can_fuzz_nested_lambdas_with_captured_variables_natively(); + Can_fuzz_objects_and_members_natively(); Flat_compile_handles_nested_lambdas_and_captured_variables_natively(); Experiment_compare_flat_and_light_compile_phases(); Flat_compiles_the_readme_expressions_and_compares_the_time(); From 39648cc3749901b18eecc9e21df25f6755b4ed10 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 3 Nov 2026 22:34:42 +0100 Subject: [PATCH 19/19] FlatCtx: stack struct passed by ref instead of the thread-static class Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .scratch/flat-ideas.md | 3 + .../FlatExpressionCompiler.cs | 98 ++++++++----------- 2 files changed, 46 insertions(+), 55 deletions(-) diff --git a/.scratch/flat-ideas.md b/.scratch/flat-ideas.md index b404bd8da..f8e897865 100644 --- a/.scratch/flat-ideas.md +++ b/.scratch/flat-ideas.md @@ -87,3 +87,6 @@ flat->sys->light->FEC 64-108us, System.Compile 169-198us. ## Step 13: native fuzzers CsCheck fuzzers compare native flat vs System: closures (shared StrongBox, by-value capture, 2 instances) and objects (New/Call/Field/Property/MemberInit/ref Conditional). Both assert no fallback. + +## Step 14: FlatCtx is a stack struct +FlatCtx is a local struct passed by ref (as FEC does with its ClosureInfo); no ThreadStatic, no cleanup. Allocation is unchanged (1128/2608/1448 B). diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs index 12f644167..2e68c1ff9 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpressionCompiler.cs @@ -40,10 +40,8 @@ private struct FlatCapture public bool ByValue; } - [ThreadStatic] private static FlatCtx _flatCtx; - - /// The state of one compilation, one instance per thread. - private sealed class FlatCtx + /// The state of one compilation, a local of the TryCompileFlat passed by ref. + private struct FlatCtx { public object[] Constants; // the closure constants, then the open delegates of the nested lambdas public int NextSlot; @@ -64,7 +62,7 @@ public bool IsBoxed(ushort id) /// Finds which variables are used by the lambdas other than the declaring one. Recursive but only run for the nested lambdas. /// The variable is declared by the lambda parameter or the block variable, the usage is the Parameter node with the same id. - private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx, + private static void FlatFindCaptures(ref ExprTree tree, int idx, ref FlatCtx ctx, ref SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>> declared, ref SmallList, NoArrayPool> lambdas) { @@ -108,7 +106,7 @@ private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx, declared.Add((par.ChildIdx, lambdas.Count)); parIdx = par.NextIdx; } - FlatFindCaptures(ref tree, bodyIdx, ctx, ref declared, ref lambdas); + FlatFindCaptures(ref tree, bodyIdx, ref ctx, ref declared, ref lambdas); declared.Count = scope; --lambdas.Count; return; @@ -128,7 +126,7 @@ private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx, var exprIdx = exprs.ChildIdx; for (var i = 0; i < exprs.ChildCount; ++i) { - FlatFindCaptures(ref tree, exprIdx, ctx, ref declared, ref lambdas); + FlatFindCaptures(ref tree, exprIdx, ref ctx, ref declared, ref lambdas); exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; } declared.Count = scope; @@ -140,7 +138,7 @@ private static void FlatFindCaptures(ref ExprTree tree, int idx, FlatCtx ctx, var childIdx = n.ChildIdx; for (var i = 0; i < n.ChildCount; ++i) { - FlatFindCaptures(ref tree, childIdx, ctx, ref declared, ref lambdas); + FlatFindCaptures(ref tree, childIdx, ref ctx, ref declared, ref lambdas); childIdx = tree.Nodes.GetSurePresentRef(childIdx).NextIdx; } return; @@ -457,39 +455,29 @@ public static TDelegate TryCompileFlat(this ref ExprTree tree) where return (TDelegate)(object)(folded != 0 ? ExpressionCompiler.Interpreter.TrueFunc : ExpressionCompiler.Interpreter.FalseFunc); var constantCount = tree.ClosureConstants.Count; - // the context is reused per thread to not allocate it (the capture buffer is big); the compile is not re-entrant - var ctx = _flatCtx ??= new FlatCtx(); - ctx.NextSlot = constantCount; - ctx.Captures.Count = 0; - try + var ctx = new FlatCtx { NextSlot = constantCount }; + var slotCount = constantCount + tree.LambdaCount - 1; // the slot per nested lambda for its open delegate + if (slotCount != 0) { - var slotCount = constantCount + tree.LambdaCount - 1; // the slot per nested lambda for its open delegate - if (slotCount != 0) - { - ctx.Constants = new object[slotCount]; - for (var i = 0; i < constantCount; ++i) - ctx.Constants[i] = tree.ClosureConstants[i]; - } - - if (tree.LambdaCount > 1) - { - var declared = default(SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>>); - var lambdas = default(SmallList, NoArrayPool>); - FlatFindCaptures(ref tree, tree.RootIdx, ctx, ref declared, ref lambdas); - } - - var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); - return (TDelegate)(object)EmitFlatLambda(ref tree, tree.RootIdx, ctx, typeof(TDelegate), closure); + ctx.Constants = new object[slotCount]; + for (var i = 0; i < constantCount; ++i) + ctx.Constants[i] = tree.ClosureConstants[i]; } - finally + + if (tree.LambdaCount > 1) { - ctx.Constants = null; // do not keep the constants alive + var declared = default(SmallList<(ushort Id, int Depth), Stack8<(ushort Id, int Depth)>, NoArrayPool<(ushort Id, int Depth)>>); + var lambdas = default(SmallList, NoArrayPool>); + FlatFindCaptures(ref tree, tree.RootIdx, ref ctx, ref declared, ref lambdas); } + + var closure = ctx.Constants == null ? EmptyArrayClosure : new ArrayClosure(ctx.Constants); + return (TDelegate)(object)EmitFlatLambda(ref tree, tree.RootIdx, ref ctx, typeof(TDelegate), closure); } /// Emits the lambda into its own DynamicMethod. The root is closed over the closure; the nested one /// (the delegate type is null) becomes the open delegate with the closure as the first parameter, to be curried at run-time. - private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx ctx, Type delegateType, object closure) + private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, ref FlatCtx ctx, Type delegateType, object closure) { ref var lambda = ref tree.Nodes.GetSurePresentRef(lambdaIdx); var bodyIdx = lambda.ChildIdx; @@ -522,7 +510,7 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx if (cap.LambdaIdx == lambdaIdx) vars.Add(new FlatVar { Id = cap.Id, Type = cap.Type, Arg = -1, Slot = (short)slot++, Boxed = !cap.ByValue }); } - EmitFlat(ref tree, bodyIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, bodyIdx, il, ref vars, ref ctx, false); il.Emit(OpCodes.Ret); var result = dynMethod.CreateDelegate(delegateType ?? info.OpenType, closure); @@ -531,7 +519,7 @@ private static Delegate EmitFlatLambda(ref ExprTree tree, int lambdaIdx, FlatCtx } private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, - ref SmallList, NoArrayPool> vars, FlatCtx ctx, bool ignoreResult) + ref SmallList, NoArrayPool> vars, ref FlatCtx ctx, bool ignoreResult) { ref var n = ref tree.Nodes.GetSurePresentRef(idx); var nodeType = n.NodeType; @@ -572,7 +560,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, if (v.Boxed) { FlatLoadBox(il, in v); - EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, rightIdx, il, ref vars, ref ctx, false); var result = -1; if (!ignoreResult) { @@ -584,7 +572,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, EmittingVisitor.EmitLoadLocalVariable(il, result); return; } - EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, rightIdx, il, ref vars, ref ctx, false); if (!ignoreResult) il.Emit(OpCodes.Dup); if (v.Arg >= 0) il.Emit(OpCodes.Starg, v.Arg); else EmittingVisitor.EmitStoreLocalVariable(il, v.Local); @@ -598,7 +586,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, // the nested lambda is the separate method; at run-time the open delegate is curried with the new closure // made of the shared constants and the captured variables: the StrongBox, or the boxed value if never assigned var info = FlatGetDelegateInfo(n.Type); - var open = EmitFlatLambda(ref tree, idx, ctx, null, null); + var open = EmitFlatLambda(ref tree, idx, ref ctx, null, null); var slot = ctx.NextSlot++; ctx.Constants[slot] = open; @@ -651,7 +639,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var argIdx = n.ChildIdx; for (var i = 0; i < n.ChildCount; ++i) { - EmitFlat(ref tree, argIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, argIdx, il, ref vars, ref ctx, false); argIdx = tree.Nodes.GetSurePresentRef(argIdx).NextIdx; } if (nodeType == ExprType.New) @@ -667,7 +655,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, case ExprType.MemberAccess: { if (n.ChildCount != 0) - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); if (n.Obj is FieldInfo field) il.Emit(field.IsStatic ? OpCodes.Ldsfld : OpCodes.Ldfld, field); else @@ -680,13 +668,13 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, case ExprType.MemberInit: { - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); var bindingIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; for (var i = 1; i < n.ChildCount; ++i) { ref var binding = ref tree.Nodes.GetSurePresentRef(bindingIdx); il.Emit(OpCodes.Dup); - EmitFlat(ref tree, binding.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, binding.ChildIdx, il, ref vars, ref ctx, false); if (binding.Obj is FieldInfo field) il.Emit(OpCodes.Stfld, field); else @@ -706,7 +694,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, { il.Emit(OpCodes.Dup); EmittingVisitor.EmitLoadConstantInt(il, i); - EmitFlat(ref tree, elemIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, elemIdx, il, ref vars, ref ctx, false); il.Emit(elemType == typeof(int) ? OpCodes.Stelem_I4 : elemType == typeof(bool) ? OpCodes.Stelem_I1 : OpCodes.Stelem_Ref); elemIdx = tree.Nodes.GetSurePresentRef(elemIdx).NextIdx; } @@ -715,22 +703,22 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, case ExprType.ArrayIndex: { - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); - EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ref ctx, false); il.Emit(n.Type == typeof(int) ? OpCodes.Ldelem_I4 : n.Type == typeof(bool) ? OpCodes.Ldelem_U1 : OpCodes.Ldelem_Ref); break; } case ExprType.Convert: { - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); if (!n.Type.IsAssignableFrom(tree.Nodes.GetSurePresentRef(n.ChildIdx).Type)) il.Emit(OpCodes.Castclass, n.Type); break; } case ExprType.Not: - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ceq); break; @@ -742,12 +730,12 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var ifFalseIdx = tree.Nodes.GetSurePresentRef(ifTrueIdx).NextIdx; var elseLabel = il.DefineLabel(); var endLabel = il.DefineLabel(); - EmitFlat(ref tree, testIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, testIdx, il, ref vars, ref ctx, false); il.Emit(OpCodes.Brfalse, elseLabel); - EmitFlat(ref tree, ifTrueIdx, il, ref vars, ctx, ignoreResult); + EmitFlat(ref tree, ifTrueIdx, il, ref vars, ref ctx, ignoreResult); il.Emit(OpCodes.Br, endLabel); il.MarkLabel(elseLabel); - EmitFlat(ref tree, ifFalseIdx, il, ref vars, ctx, ignoreResult); + EmitFlat(ref tree, ifFalseIdx, il, ref vars, ref ctx, ignoreResult); il.MarkLabel(endLabel); return; } @@ -759,9 +747,9 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var rightIdx = tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx; var shortCircuit = il.DefineLabel(); var endLabel = il.DefineLabel(); - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); il.Emit(isAnd ? OpCodes.Brfalse : OpCodes.Brtrue, shortCircuit); - EmitFlat(ref tree, rightIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, rightIdx, il, ref vars, ref ctx, false); il.Emit(OpCodes.Br, endLabel); il.MarkLabel(shortCircuit); il.Emit(isAnd ? OpCodes.Ldc_I4_0 : OpCodes.Ldc_I4_1); @@ -790,7 +778,7 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, var exprIdx = exprs.ChildIdx; for (var i = 0; i <= last; ++i) { - EmitFlat(ref tree, exprIdx, il, ref vars, ctx, i != last || ignoreResult); + EmitFlat(ref tree, exprIdx, il, ref vars, ref ctx, i != last || ignoreResult); exprIdx = tree.Nodes.GetSurePresentRef(exprIdx).NextIdx; } vars.Count = scopeStart; // the block variables go out of the scope @@ -799,8 +787,8 @@ private static void EmitFlat(ref ExprTree tree, int idx, ILGenerator il, default: // the binary int/bool operations { - EmitFlat(ref tree, n.ChildIdx, il, ref vars, ctx, false); - EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ctx, false); + EmitFlat(ref tree, n.ChildIdx, il, ref vars, ref ctx, false); + EmitFlat(ref tree, tree.Nodes.GetSurePresentRef(n.ChildIdx).NextIdx, il, ref vars, ref ctx, false); switch (nodeType) { case ExprType.Add: il.Emit(OpCodes.Add); break;