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."); }