From ad2ca2c118bf473c387568cac7b5f1214699a076 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Apr 2026 06:06:44 +0000
Subject: [PATCH 1/4] Initial plan
From e27283a942675e16e2f705fb4f3ca319dc51e5ff Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Apr 2026 06:12:50 +0000
Subject: [PATCH 2/4] feat: track nested-lambda closure parameter usage in flat
expression
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/92cd188a-ef88-4f30-a837-32e0c40e9a1a
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 87 +++++++++++++++++++
.../LightExpressionTests.cs | 68 ++++++++++++++-
2 files changed, 154 insertions(+), 1 deletion(-)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index 11c12e74..e8a3a7e5 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -45,6 +45,22 @@ public enum ExprNodeKind : byte
UInt16Pair,
}
+/// Maps a lambda node to a parameter identity used from an outer scope and therefore captured in closure.
+public readonly struct LambdaClosureParameterUsage
+{
+ /// The lambda node index containing the parameter usage.
+ public readonly int LambdaNodeIndex;
+
+ /// The parameter identity id () referenced from outer scope.
+ public readonly int ParameterId;
+
+ public LambdaClosureParameterUsage(int lambdaNodeIndex, int parameterId)
+ {
+ LambdaNodeIndex = lambdaNodeIndex;
+ ParameterId = parameterId;
+ }
+}
+
/// Stores one flat expression node plus its intrusive child-link metadata in 24 bytes on 64-bit runtimes.
///
/// Layout (64-bit): Type(8) | Obj(8) | _meta(4) | _data(4) = 24 bytes.
@@ -197,6 +213,12 @@ public struct ExprTree
/// enabling callers to locate all try regions without a full tree traversal.
public SmallList, NoArrayPool> TryCatchNodes;
+ /// Gets or sets mappings of lambda-node index to captured parameter id for nested-lambda closures.
+ /// Populated by while flattening System.Linq lambdas;
+ /// each entry means that the lambda body references a parameter that is not declared as that lambda parameter
+ /// and not declared as a local block/catch variable in that lambda body scope.
+ public SmallList, NoArrayPool> LambdaClosureParameterUsages;
+
/// Adds a parameter node and returns its index.
public int Parameter(Type type, string name = null)
{
@@ -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);
+ CollectLambdaClosureParameterUsages(lambda, lambdaIndex);
return lambdaIndex;
}
case ExpressionType.Block:
@@ -1140,6 +1163,70 @@ private int AddExpression(SysExpr expression)
}
}
+ private void CollectLambdaClosureParameterUsages(System.Linq.Expressions.LambdaExpression lambda, int lambdaNodeIndex)
+ {
+ var collector = new LambdaClosureUsageCollector(lambda);
+ collector.Visit(lambda.Body);
+
+ var captured = collector.CapturedParameters;
+ for (var i = 0; i < captured.Count; ++i)
+ _tree.LambdaClosureParameterUsages.Add(new LambdaClosureParameterUsage(lambdaNodeIndex,
+ GetId(ref _parameterIds, captured[i])));
+ }
+
+ private sealed class LambdaClosureUsageCollector : System.Linq.Expressions.ExpressionVisitor
+ {
+ private readonly System.Linq.Expressions.LambdaExpression _lambda;
+ private readonly List _scopedParameters = new();
+
+ public readonly List CapturedParameters = new();
+
+ public LambdaClosureUsageCollector(System.Linq.Expressions.LambdaExpression lambda)
+ {
+ _lambda = lambda;
+ for (var i = 0; i < lambda.Parameters.Count; ++i)
+ _scopedParameters.Add(lambda.Parameters[i]);
+ }
+
+ protected override Expression VisitLambda(System.Linq.Expressions.Expression node) =>
+ ReferenceEquals(node, _lambda) ? base.VisitLambda(node) : node;
+
+ protected override Expression VisitParameter(SysParameterExpression node)
+ {
+ if (!ContainsReference(_scopedParameters, node) && !ContainsReference(CapturedParameters, node))
+ CapturedParameters.Add(node);
+ return node;
+ }
+
+ protected override Expression VisitBlock(System.Linq.Expressions.BlockExpression node)
+ {
+ var initialScopeCount = _scopedParameters.Count;
+ for (var i = 0; i < node.Variables.Count; ++i)
+ _scopedParameters.Add(node.Variables[i]);
+ var result = base.VisitBlock(node);
+ _scopedParameters.RemoveRange(initialScopeCount, _scopedParameters.Count - initialScopeCount);
+ return result;
+ }
+
+ protected override SysCatchBlock VisitCatchBlock(SysCatchBlock node)
+ {
+ var initialScopeCount = _scopedParameters.Count;
+ if (node.Variable != null)
+ _scopedParameters.Add(node.Variable);
+ var result = base.VisitCatchBlock(node);
+ _scopedParameters.RemoveRange(initialScopeCount, _scopedParameters.Count - initialScopeCount);
+ return result;
+ }
+
+ private static bool ContainsReference(List parameters, SysParameterExpression parameter)
+ {
+ for (var i = 0; i < parameters.Count; ++i)
+ if (ReferenceEquals(parameters[i], parameter))
+ return true;
+ return false;
+ }
+ }
+
private int AddConstant(System.Linq.Expressions.ConstantExpression constant) =>
_tree.Constant(constant.Value, constant.Type);
diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
index edf89600..18124121 100644
--- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
+++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
@@ -50,7 +50,9 @@ 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;
+ Flat_lambda_closure_parameter_usages_tracked_for_nested_lambda_from_expression_conversion();
+ Flat_lambda_closure_parameter_usages_excludes_nested_lambda_locals();
+ return 35;
}
@@ -931,5 +933,69 @@ public void Flat_try_catch_nodes_tracked_from_expression_conversion()
Asserts.AreEqual(1, fe.TryCatchNodes.Count);
}
+
+ public void Flat_lambda_closure_parameter_usages_tracked_for_nested_lambda_from_expression_conversion()
+ {
+ var p = SysExpr.Parameter(typeof(int), "p");
+ var sysLambda = SysExpr.Lambda>>(
+ SysExpr.Lambda>(p),
+ p);
+
+ var fe = sysLambda.ToFlatExpression();
+
+ Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count);
+
+ var nestedLambdaIndex = GetSingleNestedLambdaIndex(ref fe);
+ var pId = GetParameterIdByName(ref fe, "p");
+
+ var usage = fe.LambdaClosureParameterUsages[0];
+ Asserts.AreEqual(nestedLambdaIndex, usage.LambdaNodeIndex);
+ Asserts.AreEqual(pId, usage.ParameterId);
+ }
+
+ public void Flat_lambda_closure_parameter_usages_excludes_nested_lambda_locals()
+ {
+ var p = SysExpr.Parameter(typeof(int), "p");
+ var local = SysExpr.Variable(typeof(int), "local");
+ var nested = SysExpr.Lambda>(
+ SysExpr.Block(new[] { local }, SysExpr.Assign(local, p), local));
+ var sysLambda = SysExpr.Lambda>>(nested, p);
+
+ var fe = sysLambda.ToFlatExpression();
+
+ Asserts.AreEqual(1, fe.LambdaClosureParameterUsages.Count);
+
+ var pId = GetParameterIdByName(ref fe, "p");
+ var localId = GetParameterIdByName(ref fe, "local");
+ var usage = fe.LambdaClosureParameterUsages[0];
+ Asserts.AreEqual(pId, usage.ParameterId);
+ Asserts.IsFalse(usage.ParameterId == localId);
+ }
+
+ private static int GetSingleNestedLambdaIndex(ref ExprTree fe)
+ {
+ var nestedLambdaIndex = -1;
+ for (var i = 0; i < fe.LambdaNodes.Count; ++i)
+ {
+ var lambdaIndex = fe.LambdaNodes[i];
+ if (lambdaIndex == fe.RootIndex)
+ continue;
+ if (nestedLambdaIndex != -1)
+ throw new InvalidOperationException("Expected a single nested lambda.");
+ nestedLambdaIndex = lambdaIndex;
+ }
+ return nestedLambdaIndex;
+ }
+
+ private static int GetParameterIdByName(ref ExprTree fe, string name)
+ {
+ for (var i = 0; i < fe.Nodes.Count; ++i)
+ {
+ ref var node = ref fe.Nodes[i];
+ if (node.NodeType == ExpressionType.Parameter && string.Equals((string)node.Obj, name, StringComparison.Ordinal))
+ return node.ChildIdx;
+ }
+ throw new InvalidOperationException($"Parameter node '{name}' was not found.");
+ }
}
}
From 84157d23100bff8b857c8d2cc03c116fbcc9d65f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Apr 2026 06:17:30 +0000
Subject: [PATCH 3/4] refactor: optimize flat-expression lambda capture lookup
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/92cd188a-ef88-4f30-a837-32e0c40e9a1a
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 36 ++++++++++++++-----
.../LightExpressionTests.cs | 2 +-
2 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index e8a3a7e5..ac0a6007 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -1178,6 +1178,8 @@ private sealed class LambdaClosureUsageCollector : System.Linq.Expressions.Expre
{
private readonly System.Linq.Expressions.LambdaExpression _lambda;
private readonly List _scopedParameters = new();
+ private readonly HashSet _scopedParameterSet = new(ReferenceParameterComparer.Instance);
+ private readonly HashSet _capturedParameterSet = new(ReferenceParameterComparer.Instance);
public readonly List CapturedParameters = new();
@@ -1185,15 +1187,21 @@ public LambdaClosureUsageCollector(System.Linq.Expressions.LambdaExpression lamb
{
_lambda = lambda;
for (var i = 0; i < lambda.Parameters.Count; ++i)
- _scopedParameters.Add(lambda.Parameters[i]);
+ {
+ var parameter = lambda.Parameters[i];
+ _scopedParameters.Add(parameter);
+ _scopedParameterSet.Add(parameter);
+ }
}
protected override Expression VisitLambda(System.Linq.Expressions.Expression node) =>
+ // Intentionally skip nested lambdas: each lambda closure map is collected independently
+ // when that lambda node is visited by the parent Builder traversal.
ReferenceEquals(node, _lambda) ? base.VisitLambda(node) : node;
protected override Expression VisitParameter(SysParameterExpression node)
{
- if (!ContainsReference(_scopedParameters, node) && !ContainsReference(CapturedParameters, node))
+ if (!_scopedParameterSet.Contains(node) && _capturedParameterSet.Add(node))
CapturedParameters.Add(node);
return node;
}
@@ -1202,8 +1210,14 @@ protected override Expression VisitBlock(System.Linq.Expressions.BlockExpression
{
var initialScopeCount = _scopedParameters.Count;
for (var i = 0; i < node.Variables.Count; ++i)
- _scopedParameters.Add(node.Variables[i]);
+ {
+ var variable = node.Variables[i];
+ _scopedParameters.Add(variable);
+ _scopedParameterSet.Add(variable);
+ }
var result = base.VisitBlock(node);
+ for (var i = _scopedParameters.Count - 1; i >= initialScopeCount; --i)
+ _scopedParameterSet.Remove(_scopedParameters[i]);
_scopedParameters.RemoveRange(initialScopeCount, _scopedParameters.Count - initialScopeCount);
return result;
}
@@ -1212,18 +1226,24 @@ protected override SysCatchBlock VisitCatchBlock(SysCatchBlock node)
{
var initialScopeCount = _scopedParameters.Count;
if (node.Variable != null)
+ {
_scopedParameters.Add(node.Variable);
+ _scopedParameterSet.Add(node.Variable);
+ }
var result = base.VisitCatchBlock(node);
+ for (var i = _scopedParameters.Count - 1; i >= initialScopeCount; --i)
+ _scopedParameterSet.Remove(_scopedParameters[i]);
_scopedParameters.RemoveRange(initialScopeCount, _scopedParameters.Count - initialScopeCount);
return result;
}
- private static bool ContainsReference(List parameters, SysParameterExpression parameter)
+ private sealed class ReferenceParameterComparer : IEqualityComparer
{
- for (var i = 0; i < parameters.Count; ++i)
- if (ReferenceEquals(parameters[i], parameter))
- return true;
- return false;
+ public static readonly ReferenceParameterComparer Instance = new();
+
+ public bool Equals(SysParameterExpression x, SysParameterExpression y) => ReferenceEquals(x, y);
+
+ public int GetHashCode(SysParameterExpression obj) => RuntimeHelpers.GetHashCode(obj);
}
}
diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
index 18124121..de789300 100644
--- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
+++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
@@ -968,8 +968,8 @@ public void Flat_lambda_closure_parameter_usages_excludes_nested_lambda_locals()
var pId = GetParameterIdByName(ref fe, "p");
var localId = GetParameterIdByName(ref fe, "local");
var usage = fe.LambdaClosureParameterUsages[0];
+ Asserts.IsFalse(pId == localId);
Asserts.AreEqual(pId, usage.ParameterId);
- Asserts.IsFalse(usage.ParameterId == localId);
}
private static int GetSingleNestedLambdaIndex(ref ExprTree fe)
From b678694b0c3e72f04c7035fd62954eb712710143 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 30 Apr 2026 07:51:08 +0000
Subject: [PATCH 4/4] refactor: use short indexes for flat lambda closure usage
entries
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/775f1746-e6c9-4aef-8b08-84f5734431e5
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 12 +++++++-----
.../LightExpressionTests.cs | 8 ++++----
2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index ac0a6007..b458e288 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -46,15 +46,16 @@ public enum ExprNodeKind : byte
}
/// Maps a lambda node to a parameter identity used from an outer scope and therefore captured in closure.
+[StructLayout(LayoutKind.Sequential, Pack = 2)]
public readonly struct LambdaClosureParameterUsage
{
/// The lambda node index containing the parameter usage.
- public readonly int LambdaNodeIndex;
+ public readonly short LambdaNodeIndex;
/// The parameter identity id () referenced from outer scope.
- public readonly int ParameterId;
+ public readonly short ParameterId;
- public LambdaClosureParameterUsage(int lambdaNodeIndex, int parameterId)
+ public LambdaClosureParameterUsage(short lambdaNodeIndex, short parameterId)
{
LambdaNodeIndex = lambdaNodeIndex;
ParameterId = parameterId;
@@ -1170,8 +1171,9 @@ private void CollectLambdaClosureParameterUsages(System.Linq.Expressions.LambdaE
var captured = collector.CapturedParameters;
for (var i = 0; i < captured.Count; ++i)
- _tree.LambdaClosureParameterUsages.Add(new LambdaClosureParameterUsage(lambdaNodeIndex,
- GetId(ref _parameterIds, captured[i])));
+ _tree.LambdaClosureParameterUsages.Add(new LambdaClosureParameterUsage(
+ checked((short)lambdaNodeIndex),
+ checked((short)GetId(ref _parameterIds, captured[i]))));
}
private sealed class LambdaClosureUsageCollector : System.Linq.Expressions.ExpressionVisitor
diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
index de789300..44808035 100644
--- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
+++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
@@ -972,7 +972,7 @@ public void Flat_lambda_closure_parameter_usages_excludes_nested_lambda_locals()
Asserts.AreEqual(pId, usage.ParameterId);
}
- private static int GetSingleNestedLambdaIndex(ref ExprTree fe)
+ private static short GetSingleNestedLambdaIndex(ref ExprTree fe)
{
var nestedLambdaIndex = -1;
for (var i = 0; i < fe.LambdaNodes.Count; ++i)
@@ -984,16 +984,16 @@ private static int GetSingleNestedLambdaIndex(ref ExprTree fe)
throw new InvalidOperationException("Expected a single nested lambda.");
nestedLambdaIndex = lambdaIndex;
}
- return nestedLambdaIndex;
+ return checked((short)nestedLambdaIndex);
}
- private static int GetParameterIdByName(ref ExprTree fe, string name)
+ private static short GetParameterIdByName(ref ExprTree fe, string name)
{
for (var i = 0; i < fe.Nodes.Count; ++i)
{
ref var node = ref fe.Nodes[i];
if (node.NodeType == ExpressionType.Parameter && string.Equals((string)node.Obj, name, StringComparison.Ordinal))
- return node.ChildIdx;
+ return checked((short)node.ChildIdx);
}
throw new InvalidOperationException($"Parameter node '{name}' was not found.");
}