diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index 01338164..11c12e74 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -167,6 +167,36 @@ public struct ExprTree
/// Gets or sets closure constants that are referenced from constant nodes.
public SmallList, NoArrayPool> ClosureConstants;
+ /// Gets or sets the indices of all lambda nodes added during construction.
+ /// The root lambda index is stored in ; all other entries are nested lambdas.
+ /// Populated automatically by and ,
+ /// enabling callers to discover nested lambdas without a full tree traversal.
+ public SmallList, NoArrayPool> LambdaNodes;
+
+ /// Gets or sets the indices of all block nodes that carry explicit variable declarations.
+ /// These are the block nodes where children.Count == 2 (variable list + expression list).
+ /// Populated automatically by and ,
+ /// enabling callers to enumerate block-scoped variables without a full tree traversal.
+ public SmallList, NoArrayPool> BlocksWithVariables;
+
+ /// Gets or sets the indices of all nodes
+ /// (including return and break /continue goto-family nodes).
+ /// Populated automatically by and ,
+ /// enabling callers to link gotos to their label targets without a full tree traversal.
+ public SmallList, NoArrayPool> GotoNodes;
+
+ /// Gets or sets the indices of all expression nodes.
+ /// Populated automatically by and ,
+ /// enabling callers to link label expressions to their targets without a full tree traversal.
+ public SmallList, NoArrayPool> LabelNodes;
+
+ /// Gets or sets the indices of all nodes
+ /// (try/catch, try/finally, try/fault, and combined forms).
+ /// Populated automatically by , ,
+ /// , and ,
+ /// enabling callers to locate all try regions without a full tree traversal.
+ public SmallList, NoArrayPool> TryCatchNodes;
+
/// Adds a parameter node and returns its index.
public int Parameter(Type type, string name = null)
{
@@ -374,6 +404,9 @@ public int Block(params int[] expressions) =>
/// Variable parameter nodes share the same id-slot as the refs used inside the body
/// (out-of-order: the variable decl nodes appear in children[0] before the body expressions
/// that reference them in children[1]).
+ /// When the block has explicit variable declarations its node index is recorded in
+ /// , enabling callers to enumerate block-scoped variables
+ /// without a full tree traversal.
///
public int Block(Type type, IEnumerable variables, params int[] expressions)
{
@@ -381,19 +414,26 @@ public int Block(Type type, IEnumerable variables, params int[] expressions
throw new ArgumentException("Block should contain at least one expression.", nameof(expressions));
ChildList children = default;
+ var hasVariables = false;
if (variables != null)
{
ChildList variableChildren = default;
foreach (var variable in variables)
variableChildren.Add(variable);
if (variableChildren.Count != 0)
+ {
children.Add(AddChildListNode(in variableChildren));
+ hasVariables = true;
+ }
}
ChildList bodyChildren = default;
for (var i = 0; i < expressions.Length; ++i)
bodyChildren.Add(expressions[i]);
children.Add(AddChildListNode(in bodyChildren));
- return AddFactoryExpressionNode(type ?? Nodes[expressions[expressions.Length - 1]].Type, null, ExpressionType.Block, in children);
+ var index = AddFactoryExpressionNode(type ?? Nodes[expressions[expressions.Length - 1]].Type, null, ExpressionType.Block, in children);
+ if (hasVariables)
+ BlocksWithVariables.Add(index);
+ return index;
}
/// Adds a typed lambda node.
@@ -414,11 +454,17 @@ public int Lambda(int body, params int[] parameters) where TDelegate
/// out-of-order decl pattern. The Reader resolves identity through a shared id map
/// so that all refs and the single decl resolve to the same
/// object.
+ /// The lambda node index is recorded in so callers can discover
+ /// nested lambdas (all entries except ) without a full tree traversal.
///
- public int Lambda(Type delegateType, int body, params int[] parameters) =>
- parameters == null || parameters.Length == 0
+ public int Lambda(Type delegateType, int body, params int[] parameters)
+ {
+ var index = parameters == null || parameters.Length == 0
? AddFactoryExpressionNode(delegateType, null, ExpressionType.Lambda, 0, body)
: AddFactoryExpressionNode(delegateType, null, ExpressionType.Lambda, PrependToChildList(body, parameters));
+ LambdaNodes.Add(index);
+ return index;
+ }
/// Adds a member-assignment binding node.
public int Bind(System.Reflection.MemberInfo member, int expression) =>
@@ -456,18 +502,26 @@ public int Label(Type type = null, string name = null)
}
/// Adds a label-expression node.
- public int Label(int target, int? defaultValue = null) =>
- defaultValue.HasValue
+ /// The node index is recorded in .
+ public int Label(int target, int? defaultValue = null)
+ {
+ var index = defaultValue.HasValue
? AddFactoryExpressionNode(Nodes[target].Type, null, ExpressionType.Label, 0, target, defaultValue.Value)
: AddFactoryExpressionNode(Nodes[target].Type, null, ExpressionType.Label, 0, target);
+ LabelNodes.Add(index);
+ return index;
+ }
/// Adds a goto-family node.
+ /// The node index is recorded in .
public int MakeGoto(GotoExpressionKind kind, int target, int? value = null, Type type = null)
{
var resultType = type ?? (value.HasValue ? Nodes[value.Value].Type : typeof(void));
- return value.HasValue
+ var index = value.HasValue
? AddFactoryExpressionNode(resultType, kind, ExpressionType.Goto, 0, target, value.Value)
: AddFactoryExpressionNode(resultType, kind, ExpressionType.Goto, 0, target);
+ GotoNodes.Add(index);
+ return index;
}
/// Adds a goto node.
@@ -545,29 +599,48 @@ public int MakeCatchBlock(Type test, int? variable, int body, int? filter)
}
/// Adds a try/catch node.
+ /// The node index is recorded in .
public int TryCatch(int body, params int[] handlers)
{
+ int index;
if (handlers == null || handlers.Length == 0)
- return AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, 0, body);
-
- ChildList handlerChildren = default;
- for (var i = 0; i < handlers.Length; ++i)
- handlerChildren.Add(handlers[i]);
- ChildList children = default;
- children.Add(body);
- children.Add(AddChildListNode(in handlerChildren));
- return AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, in children);
+ {
+ index = AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, 0, body);
+ }
+ else
+ {
+ ChildList handlerChildren = default;
+ for (var i = 0; i < handlers.Length; ++i)
+ handlerChildren.Add(handlers[i]);
+ ChildList children = default;
+ children.Add(body);
+ children.Add(AddChildListNode(in handlerChildren));
+ index = AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, in children);
+ }
+ TryCatchNodes.Add(index);
+ return index;
}
/// Adds a try/finally node.
- public int TryFinally(int body, int @finally) =>
- AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, 0, body, @finally);
+ /// The node index is recorded in .
+ public int TryFinally(int body, int @finally)
+ {
+ var index = AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, 0, body, @finally);
+ TryCatchNodes.Add(index);
+ return index;
+ }
/// Adds a try/fault node.
- public int TryFault(int body, int fault) =>
- AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, TryFaultFlag, body, fault);
+ /// The node index is recorded in .
+ public int TryFault(int body, int fault)
+ {
+ var index = AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, TryFaultFlag, body, fault);
+ TryCatchNodes.Add(index);
+ return index;
+ }
/// Adds a try node with optional finally block and catch handlers.
+ /// The node index is recorded in .
public int TryCatchFinally(int body, int? @finally, params int[] handlers)
{
ChildList children = default;
@@ -581,7 +654,9 @@ public int TryCatchFinally(int body, int? @finally, params int[] handlers)
handlerChildren.Add(handlers[i]);
children.Add(AddChildListNode(in handlerChildren));
}
- return AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, 0, in children);
+ var index = AddFactoryExpressionNode(Nodes[body].Type, null, ExpressionType.Try, 0, in children);
+ TryCatchNodes.Add(index);
+ return index;
}
/// Adds a type-test node.
@@ -823,7 +898,9 @@ private int AddExpression(SysExpr expression)
children.Add(AddExpression(lambda.Body));
for (var i = 0; i < lambda.Parameters.Count; ++i)
children.Add(AddExpression(lambda.Parameters[i]));
- return _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, children);
+ var lambdaIndex = _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, children);
+ _tree.LambdaNodes.Add(lambdaIndex);
+ return lambdaIndex;
}
case ExpressionType.Block:
{
@@ -833,7 +910,8 @@ private int AddExpression(SysExpr expression)
// children.Count == 2 is the canonical test for the presence of variables.
var block = (System.Linq.Expressions.BlockExpression)expression;
ChildList children = default;
- if (block.Variables.Count != 0)
+ var hasVariables = block.Variables.Count != 0;
+ if (hasVariables)
{
ChildList variables = default;
for (var i = 0; i < block.Variables.Count; ++i)
@@ -844,7 +922,10 @@ private int AddExpression(SysExpr expression)
for (var i = 0; i < block.Expressions.Count; ++i)
expressions.Add(AddExpression(block.Expressions[i]));
children.Add(_tree.AddChildListNode(in expressions));
- return _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, in children);
+ var blockIndex = _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, in children);
+ if (hasVariables)
+ _tree.BlocksWithVariables.Add(blockIndex);
+ return blockIndex;
}
case ExpressionType.MemberAccess:
{
@@ -930,7 +1011,9 @@ private int AddExpression(SysExpr expression)
children.Add(AddLabelTarget(@goto.Target));
if (@goto.Value != null)
children.Add(AddExpression(@goto.Value));
- return _tree.AddRawExpressionNode(expression.Type, @goto.Kind, expression.NodeType, children);
+ var gotoIndex = _tree.AddRawExpressionNode(expression.Type, @goto.Kind, expression.NodeType, children);
+ _tree.GotoNodes.Add(gotoIndex);
+ return gotoIndex;
}
case ExpressionType.Label:
{
@@ -939,7 +1022,9 @@ private int AddExpression(SysExpr expression)
children.Add(AddLabelTarget(label.Target));
if (label.DefaultValue != null)
children.Add(AddExpression(label.DefaultValue));
- return _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, children);
+ var labelIndex = _tree.AddRawExpressionNode(expression.Type, null, expression.NodeType, children);
+ _tree.LabelNodes.Add(labelIndex);
+ return labelIndex;
}
case ExpressionType.Switch:
{
@@ -977,7 +1062,9 @@ private int AddExpression(SysExpr expression)
handlers.Add(AddCatchBlock(@try.Handlers[i]));
children.Add(_tree.AddChildListNode(in handlers));
}
- return _tree.AddNode(expression.Type, null, expression.NodeType, ExprNodeKind.Expression, flags, in children);
+ var tryIndex = _tree.AddNode(expression.Type, null, expression.NodeType, ExprNodeKind.Expression, flags, in children);
+ _tree.TryCatchNodes.Add(tryIndex);
+ return tryIndex;
}
case ExpressionType.MemberInit:
{
diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
index eea42063..edf89600 100644
--- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
+++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
@@ -40,7 +40,17 @@ public int Run()
Flat_nested_lambda_captures_outer_parameter_identity();
Flat_out_of_order_decl_block_in_lambda_compiles_correctly();
Flat_enum_constant_stored_inline_roundtrip();
- return 23;
+ Flat_lambda_nodes_tracks_all_lambdas_during_direct_construction();
+ Flat_lambda_nodes_tracks_deeply_nested_lambdas_during_direct_construction();
+ Flat_lambda_nodes_tracks_lambdas_from_expression_conversion();
+ Flat_lambda_nodes_has_single_entry_for_root_only_lambda();
+ Flat_blocks_with_variables_tracked_during_direct_construction();
+ Flat_goto_and_label_nodes_tracked_during_direct_construction();
+ Flat_try_catch_nodes_tracked_during_direct_construction();
+ Flat_blocks_with_variables_tracked_from_expression_conversion();
+ Flat_goto_and_label_nodes_tracked_from_expression_conversion();
+ Flat_try_catch_nodes_tracked_from_expression_conversion();
+ return 33;
}
@@ -686,5 +696,240 @@ void Check(TEnum enumValue) where TEnum : Enum
Check(UIntEnum.A);
Check(UIntEnum.B);
}
+
+ ///
+ /// When building a flat expression directly, calling Lambda() for a nested lambda
+ /// and then for the root lambda should result in both indices recorded in LambdaNodes.
+ /// The root is identified by RootIndex; all others are nested.
+ ///
+ public void Flat_lambda_nodes_tracks_all_lambdas_during_direct_construction()
+ {
+ var fe = default(ExprTree);
+ var x = fe.ParameterOf("x");
+
+ // Build: outer: x => () => x
+ var inner = fe.Lambda>(x);
+ fe.RootIndex = fe.Lambda>>(inner, x);
+
+ // Both the root and nested lambda indices should be recorded
+ Asserts.AreEqual(2, fe.LambdaNodes.Count);
+
+ // Check that inner and root are both in LambdaNodes
+ var foundInner = false;
+ var foundRoot = false;
+ for (var i = 0; i < fe.LambdaNodes.Count; i++)
+ {
+ if (fe.LambdaNodes[i] == inner) foundInner = true;
+ if (fe.LambdaNodes[i] == fe.RootIndex) foundRoot = true;
+ }
+ Asserts.IsTrue(foundInner);
+ Asserts.IsTrue(foundRoot);
+
+ // Nested lambdas are all LambdaNodes entries that are not the root
+ var nestedCount = 0;
+ for (var i = 0; i < fe.LambdaNodes.Count; i++)
+ if (fe.LambdaNodes[i] != fe.RootIndex)
+ ++nestedCount;
+ Asserts.AreEqual(1, nestedCount);
+ }
+
+ ///
+ /// When building a flat expression with multiple levels of nesting,
+ /// all lambda node indices are captured in LambdaNodes.
+ ///
+ public void Flat_lambda_nodes_tracks_deeply_nested_lambdas_during_direct_construction()
+ {
+ var fe = default(ExprTree);
+ var x = fe.ParameterOf("x");
+
+ // Build: outer: x => (() => (() => x))
+ var innermost = fe.Lambda>(x);
+ var middle = fe.Lambda>>(innermost);
+ fe.RootIndex = fe.Lambda>>>(middle, x);
+
+ // All three lambda nodes should be recorded
+ Asserts.AreEqual(3, fe.LambdaNodes.Count);
+
+ // Count nested (non-root) lambdas
+ var nestedCount = 0;
+ for (var i = 0; i < fe.LambdaNodes.Count; i++)
+ if (fe.LambdaNodes[i] != fe.RootIndex)
+ ++nestedCount;
+ Asserts.AreEqual(2, nestedCount);
+ }
+
+ ///
+ /// When converting a System.Linq expression tree with nested lambdas via FromExpression,
+ /// the resulting ExprTree should have all lambda indices populated in LambdaNodes.
+ ///
+ public void Flat_lambda_nodes_tracks_lambdas_from_expression_conversion()
+ {
+ var p = SysExpr.Parameter(typeof(int), "p");
+ // Build: p => () => p using System.Linq.Expressions
+ var sysLambda = SysExpr.Lambda>>(
+ SysExpr.Lambda>(p),
+ p);
+
+ var fe = sysLambda.ToFlatExpression();
+
+ // Both root and nested lambda indices should be recorded
+ Asserts.AreEqual(2, fe.LambdaNodes.Count);
+
+ // The root lambda must be in the list
+ var foundRoot = false;
+ for (var i = 0; i < fe.LambdaNodes.Count; i++)
+ if (fe.LambdaNodes[i] == fe.RootIndex) { foundRoot = true; break; }
+ Asserts.IsTrue(foundRoot);
+
+ // Exactly one nested lambda
+ var nestedCount = 0;
+ for (var i = 0; i < fe.LambdaNodes.Count; i++)
+ if (fe.LambdaNodes[i] != fe.RootIndex)
+ ++nestedCount;
+ Asserts.AreEqual(1, nestedCount);
+ }
+
+ ///
+ /// A flat expression with no nested lambdas (root-only) should have exactly one
+ /// entry in LambdaNodes (the root itself).
+ ///
+ public void Flat_lambda_nodes_has_single_entry_for_root_only_lambda()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ fe.RootIndex = fe.Lambda>(fe.Add(p, fe.ConstantInt(1)), p);
+
+ Asserts.AreEqual(1, fe.LambdaNodes.Count);
+ Asserts.AreEqual(fe.RootIndex, fe.LambdaNodes[0]);
+ }
+
+ ///
+ /// Block nodes with explicit variable declarations are recorded in BlocksWithVariables;
+ /// blocks without variables produce no entry.
+ ///
+ public void Flat_blocks_with_variables_tracked_during_direct_construction()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ var v = fe.Variable(typeof(int), "v");
+
+ // Block with one variable: should be tracked
+ var blockWithVar = fe.Block(typeof(int), new[] { v }, fe.Assign(v, p), v);
+ // Block without variables: should NOT be tracked
+ var blockNoVar = fe.Block(fe.Add(p, fe.ConstantInt(1)));
+
+ fe.RootIndex = fe.Lambda>(fe.Block(blockWithVar, blockNoVar), p);
+
+ Asserts.AreEqual(1, fe.BlocksWithVariables.Count);
+ Asserts.AreEqual(blockWithVar, fe.BlocksWithVariables[0]);
+ }
+
+ ///
+ /// Goto and label expression nodes are recorded in GotoNodes and LabelNodes respectively.
+ ///
+ public void Flat_goto_and_label_nodes_tracked_during_direct_construction()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ var target = fe.Label(typeof(int), "done");
+
+ var gotoNode = fe.Goto(target, p, typeof(int));
+ var labelNode = fe.Label(target, fe.ConstantInt(0));
+
+ fe.RootIndex = fe.Lambda>(fe.Block(gotoNode, labelNode), p);
+
+ Asserts.AreEqual(1, fe.GotoNodes.Count);
+ Asserts.AreEqual(gotoNode, fe.GotoNodes[0]);
+
+ Asserts.AreEqual(1, fe.LabelNodes.Count);
+ Asserts.AreEqual(labelNode, fe.LabelNodes[0]);
+ }
+
+ ///
+ /// Try/catch, try/finally and try/fault node indices are all recorded in TryCatchNodes.
+ ///
+ public void Flat_try_catch_nodes_tracked_during_direct_construction()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+
+ var tryCatchNode = fe.TryCatch(
+ fe.Add(p, fe.ConstantInt(1)),
+ fe.Catch(typeof(Exception), fe.ConstantInt(-1)));
+
+ var tryFinallyNode = fe.TryFinally(
+ fe.Add(p, fe.ConstantInt(2)),
+ fe.Default(typeof(void)));
+
+ fe.RootIndex = fe.Lambda>(
+ fe.Block(tryCatchNode, tryFinallyNode), p);
+
+ Asserts.AreEqual(2, fe.TryCatchNodes.Count);
+
+ var foundTryCatch = false;
+ var foundTryFinally = false;
+ for (var i = 0; i < fe.TryCatchNodes.Count; i++)
+ {
+ if (fe.TryCatchNodes[i] == tryCatchNode) foundTryCatch = true;
+ if (fe.TryCatchNodes[i] == tryFinallyNode) foundTryFinally = true;
+ }
+ Asserts.IsTrue(foundTryCatch);
+ Asserts.IsTrue(foundTryFinally);
+ }
+
+ ///
+ /// When converting a System.Linq expression tree, blocks with variables are
+ /// recorded in BlocksWithVariables; plain blocks are not.
+ ///
+ public void Flat_blocks_with_variables_tracked_from_expression_conversion()
+ {
+ var p = SysExpr.Parameter(typeof(int), "p");
+ var v = SysExpr.Variable(typeof(int), "v");
+ // block with variable
+ var sysBlock = SysExpr.Block(new[] { v }, SysExpr.Assign(v, p), v);
+ var sysLambda = SysExpr.Lambda>(sysBlock, p);
+
+ var fe = sysLambda.ToFlatExpression();
+
+ Asserts.AreEqual(1, fe.BlocksWithVariables.Count);
+ }
+
+ ///
+ /// When converting a System.Linq expression tree with goto/label, both
+ /// GotoNodes and LabelNodes are populated.
+ ///
+ public void Flat_goto_and_label_nodes_tracked_from_expression_conversion()
+ {
+ var p = SysExpr.Parameter(typeof(int), "p");
+ var target = SysExpr.Label(typeof(int), "done");
+ var sysLambda = SysExpr.Lambda>(
+ SysExpr.Block(
+ SysExpr.Goto(target, p, typeof(int)),
+ SysExpr.Label(target, SysExpr.Constant(0))),
+ p);
+
+ var fe = sysLambda.ToFlatExpression();
+
+ Asserts.AreEqual(1, fe.GotoNodes.Count);
+ Asserts.AreEqual(1, fe.LabelNodes.Count);
+ }
+
+ ///
+ /// When converting a System.Linq expression tree with a try/catch,
+ /// TryCatchNodes is populated.
+ ///
+ public void Flat_try_catch_nodes_tracked_from_expression_conversion()
+ {
+ var p = SysExpr.Parameter(typeof(int), "p");
+ var sysLambda = SysExpr.Lambda>(
+ SysExpr.TryCatch(
+ SysExpr.Add(p, SysExpr.Constant(1)),
+ SysExpr.Catch(typeof(Exception), SysExpr.Constant(-1))),
+ p);
+
+ var fe = sysLambda.ToFlatExpression();
+
+ Asserts.AreEqual(1, fe.TryCatchNodes.Count);
+ }
}
}