From a2337b9fc4b62e59dc87a0b93280fff83e929f0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 12:07:09 +0000 Subject: [PATCH 1/6] Initial plan From ae807adf99ee8d1e23586619495f8ceaeaf87358 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 12:16:33 +0000 Subject: [PATCH 2/6] Add flat expression lambda closure usage tracking Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/534dad56-d0d5-41e9-ae8c-bef331240b75 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FlatExpression.cs | 189 ++++++++++++++++++ .../LightExpressionTests.cs | 74 +++++++ 2 files changed, 263 insertions(+) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 11c12e74..376a86be 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -146,6 +146,21 @@ internal bool ShouldCloneWhenLinked() => Kind == ExprNodeKind.ObjectReference || ChildCount == 0; } +/// Maps a lambda node to a captured outer parameter/variable identity used for closure creation. +public struct LambdaClosureParameterUsage +{ + public int LambdaIndex; + public int ParameterIndex; + public int ParameterId; + + public LambdaClosureParameterUsage(int lambdaIndex, int parameterIndex, int parameterId) + { + LambdaIndex = lambdaIndex; + ParameterIndex = parameterIndex; + ParameterId = parameterId; + } +} + /// Stores an expression tree as a flat node array plus out-of-line closure constants. public struct ExprTree { @@ -197,6 +212,12 @@ public struct ExprTree /// enabling callers to locate all try regions without a full tree traversal. public SmallList, NoArrayPool> TryCatchNodes; + /// Gets or sets the captured outer parameter/variable usages for lambdas. + /// Populated automatically by and , + /// mirroring the nested-lambda non-passed-parameter information collected by TryCollectInfo + /// so closure preparation data is available directly on the flat tree. + public SmallList, NoArrayPool> LambdaClosureParameterUsages; + /// Adds a parameter node and returns its index. public int Parameter(Type type, string name = null) { @@ -463,6 +484,7 @@ public int Lambda(Type delegateType, int body, params int[] parameters) ? AddFactoryExpressionNode(delegateType, null, ExpressionType.Lambda, 0, body) : AddFactoryExpressionNode(delegateType, null, ExpressionType.Lambda, PrependToChildList(body, parameters)); LambdaNodes.Add(index); + CollectLambdaClosureParameterUsages(index); return index; } @@ -900,6 +922,7 @@ private int AddExpression(SysExpr expression) children.Add(AddExpression(lambda.Parameters[i])); var lambdaIndex = _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, children); _tree.LambdaNodes.Add(lambdaIndex); + _tree.CollectLambdaClosureParameterUsages(lambdaIndex); return lambdaIndex; } case ExpressionType.Block: @@ -1437,6 +1460,172 @@ private ChildList CloneChildren(in ChildList children) return cloned; } + private void CollectLambdaClosureParameterUsages(int lambdaIndex) + { + var children = GetChildren(lambdaIndex); + if (children.Count == 0) + return; + + SmallList, NoArrayPool> lambdaParameterIds = default; + for (var i = 1; i < children.Count; ++i) + lambdaParameterIds.Add(Nodes[children[i]].ChildIdx); + + SmallList, NoArrayPool> localParameterIds = default; + SmallList, NoArrayPool> captures = default; + CollectClosureParameterUsages(children[0], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + + for (var i = 0; i < captures.Count; ++i) + LambdaClosureParameterUsages.Add(captures[i]); + } + + private void CollectClosureParameterUsages( + int index, + int lambdaIndex, + ref SmallList, NoArrayPool> lambdaParameterIds, + ref SmallList, NoArrayPool> localParameterIds, + ref SmallList, NoArrayPool> captures) + { + ref var node = ref Nodes.GetSurePresentRef(index); + switch (node.NodeType) + { + case ExpressionType.Parameter: + { + var parameterId = node.ChildIdx; + if (!Contains(ref lambdaParameterIds, parameterId) && + !Contains(ref localParameterIds, parameterId)) + AddClosureParameterUsage(lambdaIndex, index, parameterId, ref captures); + return; + } + case ExpressionType.Lambda: + PropagateNestedLambdaClosureParameterUsages(index, lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + return; + case ExpressionType.Block: + { + var children = GetChildren(index); + var localCount = localParameterIds.Count; + var hasVariables = children.Count == 2; + if (hasVariables) + { + var variableIndexes = GetChildren(children[0]); + for (var i = 0; i < variableIndexes.Count; ++i) + localParameterIds.Add(Nodes[variableIndexes[i]].ChildIdx); + } + + var expressionIndexes = GetChildren(children[children.Count - 1]); + for (var i = 0; i < expressionIndexes.Count; ++i) + CollectClosureParameterUsages(expressionIndexes[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + + localParameterIds.Count = localCount; + return; + } + case ExpressionType.Try: + { + var children = GetChildren(index); + CollectClosureParameterUsages(children[0], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + + var lastChildIndex = children.Count - 1; + if (lastChildIndex > 0 && Nodes[children[lastChildIndex]].Is(ExprNodeKind.ChildList)) + { + var handlerIndexes = GetChildren(children[lastChildIndex]); + for (var i = 0; i < handlerIndexes.Count; ++i) + CollectCatchBlockClosureParameterUsages(handlerIndexes[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + lastChildIndex--; + } + + for (var i = 1; i <= lastChildIndex; ++i) + CollectClosureParameterUsages(children[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + return; + } + } + + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0) + return; + + var childIndexes = GetChildren(index); + for (var i = 0; i < childIndexes.Count; ++i) + CollectClosureParameterUsages(childIndexes[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + } + + private void CollectCatchBlockClosureParameterUsages( + int index, + int lambdaIndex, + ref SmallList, NoArrayPool> lambdaParameterIds, + ref SmallList, NoArrayPool> localParameterIds, + ref SmallList, NoArrayPool> captures) + { + ref var node = ref Nodes.GetSurePresentRef(index); + Debug.Assert(node.Is(ExprNodeKind.CatchBlock)); + + var children = GetChildren(index); + var localCount = localParameterIds.Count; + var childIndex = 0; + if (node.HasFlag(CatchHasVariableFlag)) + localParameterIds.Add(Nodes[children[childIndex++]].ChildIdx); + + var bodyIndex = children[childIndex++]; + if (node.HasFlag(CatchHasFilterFlag)) + CollectClosureParameterUsages(children[childIndex], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(bodyIndex, lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + localParameterIds.Count = localCount; + } + + private void PropagateNestedLambdaClosureParameterUsages( + int nestedLambdaIndex, + int lambdaIndex, + ref SmallList, NoArrayPool> 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.LambdaIndex != nestedLambdaIndex) + continue; + if (Contains(ref lambdaParameterIds, usage.ParameterId) || + Contains(ref localParameterIds, usage.ParameterId)) + continue; + AddClosureParameterUsage(lambdaIndex, usage.ParameterIndex, usage.ParameterId, ref captures); + } + } + + private static void AddClosureParameterUsage( + int lambdaIndex, + int parameterIndex, + int parameterId, + ref SmallList, NoArrayPool> captures) + { + for (var i = 0; i < captures.Count; ++i) + if (captures[i].ParameterId == parameterId) + return; + captures.Add(new LambdaClosureParameterUsage(lambdaIndex, parameterIndex, parameterId)); + } + + private ChildList GetChildren(int index) + { + ref var node = ref Nodes.GetSurePresentRef(index); + if (ReferenceEquals(node.Obj, ExprNode.InlineValueMarker) || node.ChildCount == 0) + return default; + var count = node.ChildCount; + ChildList children = default; + var childIndex = node.ChildIdx; + for (var i = 0; i < count; ++i) + { + children.Add(childIndex); + childIndex = Nodes.GetSurePresentRef(childIndex).NextIdx; + } + return children; + } + + private static bool Contains(ref SmallList ids, int 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; + } + /// Reconstructs System.Linq nodes from the flat representation while reusing parameter and label identities. private struct Reader { diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index edf89600..ec8ea607 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -635,6 +635,80 @@ 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.RootIndex = fe.Lambda>>(inner, x); + + Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); + Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIndex); + 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.RootIndex = 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.LambdaIndex == inner) foundInner = true; + if (usage.LambdaIndex == 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.RootIndex = 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].LambdaIndex); + 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].ParameterIndex].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); + + var nestedLambdaCount = 0; + for (var i = 0; i < fe.LambdaNodes.Count; ++i) + if (fe.LambdaNodes[i] != fe.RootIndex) + { + ++nestedLambdaCount; + Asserts.AreEqual(fe.LambdaNodes[i], fe.LambdaClosureParameterUsages[0].LambdaIndex); + } + + 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 From f455e2b6c2de07053e1d4509472bc82180f42b45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 12:19:42 +0000 Subject: [PATCH 3/6] Wire new flat closure usage tests into runner Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/534dad56-d0d5-41e9-ae8c-bef331240b75 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FlatExpression.cs | 1 + .../LightExpressionTests.cs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 376a86be..11f4a201 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -151,6 +151,7 @@ public struct LambdaClosureParameterUsage { public int LambdaIndex; public int ParameterIndex; + /// The shared parameter/variable identity stored in . public int ParameterId; public LambdaClosureParameterUsage(int lambdaIndex, int parameterIndex, int parameterId) diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index ec8ea607..9e000cf9 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -38,6 +38,10 @@ 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_lambda_nodes_tracks_all_lambdas_during_direct_construction(); @@ -50,7 +54,7 @@ public int Run() 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(); - return 33; + return 37; } From 7e612414f06e9d88712f965b155e967deeb6cc21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 08:23:33 +0000 Subject: [PATCH 4/6] Use ushort flat closure capture indexes Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/c6cac0b3-185b-4595-91a8-f2d3176e31e5 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FlatExpression.cs | 91 ++++++++++--------- .../LightExpressionTests.cs | 12 +-- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 11f4a201..0e27412d 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -147,17 +147,26 @@ internal bool ShouldCloneWhenLinked() => } /// Maps a lambda node to a captured outer parameter/variable identity used for closure creation. +[StructLayout(LayoutKind.Explicit, Size = 6)] public struct LambdaClosureParameterUsage { - public int LambdaIndex; - public int ParameterIndex; + /// The lambda node index in the flat-expression node array. + [FieldOffset(0)] + public ushort LambdaIdx; + + /// The parameter-usage expression node index in the flat-expression node array. + [FieldOffset(2)] + public ushort ParameterIdx; + /// The shared parameter/variable identity stored in . - public int ParameterId; + [FieldOffset(4)] + public ushort ParameterId; - public LambdaClosureParameterUsage(int lambdaIndex, int parameterIndex, int parameterId) + /// Creates the lambda capture mapping. + public LambdaClosureParameterUsage(ushort lambdaIdx, ushort parameterIdx, ushort parameterId) { - LambdaIndex = lambdaIndex; - ParameterIndex = parameterIndex; + LambdaIdx = lambdaIdx; + ParameterIdx = parameterIdx; ParameterId = parameterId; } } @@ -1467,13 +1476,13 @@ private void CollectLambdaClosureParameterUsages(int lambdaIndex) if (children.Count == 0) return; - SmallList, NoArrayPool> lambdaParameterIds = default; + SmallList, NoArrayPool> lambdaParameterIds = default; for (var i = 1; i < children.Count; ++i) - lambdaParameterIds.Add(Nodes[children[i]].ChildIdx); + lambdaParameterIds.Add(checked((ushort)Nodes[children[i]].ChildIdx)); - SmallList, NoArrayPool> localParameterIds = default; + SmallList, NoArrayPool> localParameterIds = default; SmallList, NoArrayPool> captures = default; - CollectClosureParameterUsages(children[0], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(children[0], checked((ushort)lambdaIndex), ref lambdaParameterIds, ref localParameterIds, ref captures); for (var i = 0; i < captures.Count; ++i) LambdaClosureParameterUsages.Add(captures[i]); @@ -1481,9 +1490,9 @@ private void CollectLambdaClosureParameterUsages(int lambdaIndex) private void CollectClosureParameterUsages( int index, - int lambdaIndex, - ref SmallList, NoArrayPool> lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, + ushort lambdaIdx, + ref SmallList, NoArrayPool> lambdaParameterIds, + ref SmallList, NoArrayPool> localParameterIds, ref SmallList, NoArrayPool> captures) { ref var node = ref Nodes.GetSurePresentRef(index); @@ -1491,14 +1500,14 @@ private void CollectClosureParameterUsages( { case ExpressionType.Parameter: { - var parameterId = node.ChildIdx; + var parameterId = checked((ushort)node.ChildIdx); if (!Contains(ref lambdaParameterIds, parameterId) && !Contains(ref localParameterIds, parameterId)) - AddClosureParameterUsage(lambdaIndex, index, parameterId, ref captures); + AddClosureParameterUsage(lambdaIdx, checked((ushort)index), parameterId, ref captures); return; } case ExpressionType.Lambda: - PropagateNestedLambdaClosureParameterUsages(index, lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + PropagateNestedLambdaClosureParameterUsages(checked((ushort)index), lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); return; case ExpressionType.Block: { @@ -1509,12 +1518,12 @@ private void CollectClosureParameterUsages( { var variableIndexes = GetChildren(children[0]); for (var i = 0; i < variableIndexes.Count; ++i) - localParameterIds.Add(Nodes[variableIndexes[i]].ChildIdx); + localParameterIds.Add(checked((ushort)Nodes[variableIndexes[i]].ChildIdx)); } var expressionIndexes = GetChildren(children[children.Count - 1]); for (var i = 0; i < expressionIndexes.Count; ++i) - CollectClosureParameterUsages(expressionIndexes[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(expressionIndexes[i], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); localParameterIds.Count = localCount; return; @@ -1522,19 +1531,19 @@ private void CollectClosureParameterUsages( case ExpressionType.Try: { var children = GetChildren(index); - CollectClosureParameterUsages(children[0], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(children[0], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); var lastChildIndex = children.Count - 1; if (lastChildIndex > 0 && Nodes[children[lastChildIndex]].Is(ExprNodeKind.ChildList)) { var handlerIndexes = GetChildren(children[lastChildIndex]); for (var i = 0; i < handlerIndexes.Count; ++i) - CollectCatchBlockClosureParameterUsages(handlerIndexes[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectCatchBlockClosureParameterUsages(handlerIndexes[i], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); lastChildIndex--; } for (var i = 1; i <= lastChildIndex; ++i) - CollectClosureParameterUsages(children[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(children[i], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); return; } } @@ -1544,14 +1553,14 @@ private void CollectClosureParameterUsages( var childIndexes = GetChildren(index); for (var i = 0; i < childIndexes.Count; ++i) - CollectClosureParameterUsages(childIndexes[i], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(childIndexes[i], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); } private void CollectCatchBlockClosureParameterUsages( int index, - int lambdaIndex, - ref SmallList, NoArrayPool> lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, + ushort lambdaIdx, + ref SmallList, NoArrayPool> lambdaParameterIds, + ref SmallList, NoArrayPool> localParameterIds, ref SmallList, NoArrayPool> captures) { ref var node = ref Nodes.GetSurePresentRef(index); @@ -1561,44 +1570,44 @@ private void CollectCatchBlockClosureParameterUsages( var localCount = localParameterIds.Count; var childIndex = 0; if (node.HasFlag(CatchHasVariableFlag)) - localParameterIds.Add(Nodes[children[childIndex++]].ChildIdx); + localParameterIds.Add(checked((ushort)Nodes[children[childIndex++]].ChildIdx)); var bodyIndex = children[childIndex++]; if (node.HasFlag(CatchHasFilterFlag)) - CollectClosureParameterUsages(children[childIndex], lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); - CollectClosureParameterUsages(bodyIndex, lambdaIndex, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(children[childIndex], lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(bodyIndex, lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); localParameterIds.Count = localCount; } private void PropagateNestedLambdaClosureParameterUsages( - int nestedLambdaIndex, - int lambdaIndex, - ref SmallList, NoArrayPool> lambdaParameterIds, - ref SmallList, NoArrayPool> localParameterIds, + ushort nestedLambdaIdx, + ushort lambdaIdx, + ref SmallList, NoArrayPool> 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.LambdaIndex != nestedLambdaIndex) + if (usage.LambdaIdx != nestedLambdaIdx) continue; if (Contains(ref lambdaParameterIds, usage.ParameterId) || Contains(ref localParameterIds, usage.ParameterId)) continue; - AddClosureParameterUsage(lambdaIndex, usage.ParameterIndex, usage.ParameterId, ref captures); + AddClosureParameterUsage(lambdaIdx, usage.ParameterIdx, usage.ParameterId, ref captures); } } private static void AddClosureParameterUsage( - int lambdaIndex, - int parameterIndex, - int parameterId, + 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(lambdaIndex, parameterIndex, parameterId)); + captures.Add(new LambdaClosureParameterUsage(lambdaIdx, parameterIdx, parameterId)); } private ChildList GetChildren(int index) @@ -1617,9 +1626,9 @@ private ChildList GetChildren(int index) return children; } - private static bool Contains(ref SmallList ids, int value) - where TStack : struct, IStack - where TPool : struct, ISmallArrayPool + 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) diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs index 9e000cf9..e7471a6e 100644 --- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs +++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs @@ -647,7 +647,7 @@ public void Flat_lambda_closure_parameter_usages_track_captured_outer_parameter_ fe.RootIndex = fe.Lambda>>(inner, x); Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIndex); + Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIdx); Asserts.AreEqual(fe.Nodes[x].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); } @@ -667,8 +667,8 @@ public void Flat_lambda_closure_parameter_usages_propagate_across_nested_lambdas { ref var usage = ref fe.LambdaClosureParameterUsages[i]; Asserts.AreEqual(fe.Nodes[x].ChildIdx, usage.ParameterId); - if (usage.LambdaIndex == inner) foundInner = true; - if (usage.LambdaIndex == middle) foundMiddle = true; + if (usage.LambdaIdx == inner) foundInner = true; + if (usage.LambdaIdx == middle) foundMiddle = true; } Asserts.IsTrue(foundInner); @@ -686,7 +686,7 @@ public void Flat_lambda_closure_parameter_usages_track_captured_block_variable_d inner)); Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIndex); + Asserts.AreEqual(inner, fe.LambdaClosureParameterUsages[0].LambdaIdx); Asserts.AreEqual(fe.Nodes[v].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); } @@ -700,14 +700,14 @@ public void Flat_lambda_closure_parameter_usages_track_captures_from_expression_ var fe = sysLambda.ToFlatExpression(); Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count); - Asserts.AreEqual(fe.Nodes[fe.LambdaClosureParameterUsages[0].ParameterIndex].ChildIdx, fe.LambdaClosureParameterUsages[0].ParameterId); + 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.RootIndex) { ++nestedLambdaCount; - Asserts.AreEqual(fe.LambdaNodes[i], fe.LambdaClosureParameterUsages[0].LambdaIndex); + Asserts.AreEqual(fe.LambdaNodes[i], fe.LambdaClosureParameterUsages[0].LambdaIdx); } Asserts.AreEqual(1, nestedLambdaCount); From 772ea385a4bd92712576edab57da73da79c46dea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 08:26:34 +0000 Subject: [PATCH 5/6] Document ushort flat capture index range Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/c6cac0b3-185b-4595-91a8-f2d3176e31e5 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FlatExpression.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs index 0e27412d..cd9023a6 100644 --- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs +++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs @@ -146,7 +146,8 @@ internal bool ShouldCloneWhenLinked() => Kind == ExprNodeKind.ObjectReference || ChildCount == 0; } -/// Maps a lambda node to a captured outer parameter/variable identity used for closure creation. +/// Maps a lambda node to a captured outer parameter/variable identity used for closure creation. +/// Uses the same 16-bit index range already used by flat-expression node links and identities. [StructLayout(LayoutKind.Explicit, Size = 6)] public struct LambdaClosureParameterUsage { @@ -225,7 +226,8 @@ public struct ExprTree /// Gets or sets the captured outer parameter/variable usages for lambdas. /// Populated automatically by and , /// mirroring the nested-lambda non-passed-parameter information collected by TryCollectInfo - /// so closure preparation data is available directly on the flat tree. + /// so closure preparation data is available directly on the flat tree. + /// The stored indexes use the same 16-bit range as and . public SmallList, NoArrayPool> LambdaClosureParameterUsages; /// Adds a parameter node and returns its index. @@ -1478,11 +1480,11 @@ private void CollectLambdaClosureParameterUsages(int lambdaIndex) SmallList, NoArrayPool> lambdaParameterIds = default; for (var i = 1; i < children.Count; ++i) - lambdaParameterIds.Add(checked((ushort)Nodes[children[i]].ChildIdx)); + lambdaParameterIds.Add(ToStoredUShortIdx(Nodes[children[i]].ChildIdx)); SmallList, NoArrayPool> localParameterIds = default; SmallList, NoArrayPool> captures = default; - CollectClosureParameterUsages(children[0], checked((ushort)lambdaIndex), ref lambdaParameterIds, ref localParameterIds, ref captures); + CollectClosureParameterUsages(children[0], ToStoredUShortIdx(lambdaIndex), ref lambdaParameterIds, ref localParameterIds, ref captures); for (var i = 0; i < captures.Count; ++i) LambdaClosureParameterUsages.Add(captures[i]); @@ -1500,14 +1502,14 @@ private void CollectClosureParameterUsages( { case ExpressionType.Parameter: { - var parameterId = checked((ushort)node.ChildIdx); + var parameterId = ToStoredUShortIdx(node.ChildIdx); if (!Contains(ref lambdaParameterIds, parameterId) && !Contains(ref localParameterIds, parameterId)) - AddClosureParameterUsage(lambdaIdx, checked((ushort)index), parameterId, ref captures); + AddClosureParameterUsage(lambdaIdx, ToStoredUShortIdx(index), parameterId, ref captures); return; } case ExpressionType.Lambda: - PropagateNestedLambdaClosureParameterUsages(checked((ushort)index), lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); + PropagateNestedLambdaClosureParameterUsages(ToStoredUShortIdx(index), lambdaIdx, ref lambdaParameterIds, ref localParameterIds, ref captures); return; case ExpressionType.Block: { @@ -1518,7 +1520,7 @@ private void CollectClosureParameterUsages( { var variableIndexes = GetChildren(children[0]); for (var i = 0; i < variableIndexes.Count; ++i) - localParameterIds.Add(checked((ushort)Nodes[variableIndexes[i]].ChildIdx)); + localParameterIds.Add(ToStoredUShortIdx(Nodes[variableIndexes[i]].ChildIdx)); } var expressionIndexes = GetChildren(children[children.Count - 1]); @@ -1570,7 +1572,7 @@ private void CollectCatchBlockClosureParameterUsages( var localCount = localParameterIds.Count; var childIndex = 0; if (node.HasFlag(CatchHasVariableFlag)) - localParameterIds.Add(checked((ushort)Nodes[children[childIndex++]].ChildIdx)); + localParameterIds.Add(ToStoredUShortIdx(Nodes[children[childIndex++]].ChildIdx)); var bodyIndex = children[childIndex++]; if (node.HasFlag(CatchHasFilterFlag)) @@ -1636,6 +1638,9 @@ private static bool Contains(ref SmallList return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort ToStoredUShortIdx(int idx) => checked((ushort)idx); + /// Reconstructs System.Linq nodes from the flat representation while reusing parameter and label identities. private struct Reader { From c3881215e7fa2c5bb8adcdf782920121a89d50d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 10:05:48 +0000 Subject: [PATCH 6/6] Enable nullable context in Issue500 test Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/ca45f74d-d8c3-439d-ba63-e44066b21a13 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- ...tOfRangeException_with_value_objects_implicit_conversions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs b/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs index 903e08a5..7e676bd0 100644 --- a/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs +++ b/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs @@ -1,6 +1,8 @@ using System; using System.Reflection; +#nullable enable + #if LIGHT_EXPRESSION using static FastExpressionCompiler.LightExpression.Expression; namespace FastExpressionCompiler.LightExpression.IssueTests;