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;