(object value, TypeCode tc) =>
+ throw new NotSupportedException($"Cannot convert value '{value}' of TypeCode {tc} to an inline constant");
}
/// Provides conversions from System and LightExpression trees to .
From 3ba217f3c4fa277a2f1927648169e7cc784d67dc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Apr 2026 06:37:25 +0000
Subject: [PATCH 3/7] fix(#533): align constant classification with
IsClosureBoundConstant
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Primitives that fit in 32 bits (bool/byte/sbyte/char/short/ushort/int/uint/float) still use inline _data
- Wide primitives (long/ulong/double) are stored boxed in Obj — NOT in ClosureConstants
- decimal, null, string, Type, enum also stay in Obj
- Only Delegate, arrays, and user-defined types go to ClosureConstants
(matches TryCollectInfo's IsClosureBoundConstant: !IsPrimitive && !IsEnum && !string && !Type && !decimal)
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/5c0eee4e-2b40-466f-af3a-40eb078c0a9d
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 27 +++++++++++++------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index c22c0006..fa8f6891 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -190,13 +190,19 @@ public int Constant(object value) =>
/// Adds a constant node with an explicit constant type.
public int Constant(object value, Type type)
{
- if (value == null || value is string || value is Type || type.IsEnum)
+ if (value == null || value is string || value is Type || type.IsEnum || value is decimal)
return AddRawExpressionNode(type, value, ExpressionType.Constant);
- var tc = Type.GetTypeCode(type);
- if (IsSmallPrimitive(tc))
- return AddInlineConstantNode(type, ToInlineValue(value, tc));
+ if (type.IsPrimitive)
+ {
+ var tc = Type.GetTypeCode(type);
+ if (IsSmallPrimitive(tc))
+ return AddInlineConstantNode(type, ToInlineValue(value, tc));
+ // long, ulong, double: primitive but too wide for _data, store boxed in Obj
+ return AddRawExpressionNode(type, value, ExpressionType.Constant);
+ }
+ // Delegate, array types, and user-defined reference/value types go to ClosureConstants
var constantIndex = ClosureConstants.Add(value);
return AddRawExpressionNodeWithChildIndex(type, ClosureConstantMarker, ExpressionType.Constant, constantIndex);
}
@@ -964,12 +970,17 @@ private int AddConstant(System.Linq.Expressions.ConstantExpression constant)
var value = constant.Value;
var type = constant.Type;
- if (value == null || value is string || value is Type || type.IsEnum)
+ if (value == null || value is string || value is Type || type.IsEnum || value is decimal)
return _tree.AddRawExpressionNode(type, value, ExpressionType.Constant);
- var tc = Type.GetTypeCode(type);
- if (IsSmallPrimitive(tc))
- return _tree.AddInlineConstantNode(type, ToInlineValue(value, tc));
+ if (type.IsPrimitive)
+ {
+ var tc = Type.GetTypeCode(type);
+ if (IsSmallPrimitive(tc))
+ return _tree.AddInlineConstantNode(type, ToInlineValue(value, tc));
+ // long, ulong, double: primitive but too wide for _data, store boxed in Obj
+ return _tree.AddRawExpressionNode(type, value, ExpressionType.Constant);
+ }
var constantIndex = _tree.ClosureConstants.Add(value);
return _tree.AddRawExpressionNodeWithChildIndex(type, ClosureConstantMarker, ExpressionType.Constant, constantIndex);
From e1f66620ab93aa8087030055a6c0b7930e9dcf4c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Apr 2026 09:57:44 +0000
Subject: [PATCH 4/7] test(#533): add FlatExpression decl/ref/out-of-order
identity tests for params/lambdas/blocks
5 new tests covering:
- Flat_lambda_parameter_ref_before_decl_preserves_identity
- 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_out_of_order_decl_block_in_lambda_compiles_correctly
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/7f7fb7fe-7940-4cfb-ba92-1454b7953749
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../LightExpressionTests.cs | 135 +++++++++++++++++-
1 file changed, 134 insertions(+), 1 deletion(-)
diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
index b91c23c0..4c780604 100644
--- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
+++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
@@ -34,7 +34,12 @@ public int Run()
Can_build_flat_expression_directly_with_light_expression_like_api();
Can_build_flat_expression_control_flow_directly();
Can_property_test_generated_flat_expression_roundtrip_structurally();
- return 17;
+ Flat_lambda_parameter_ref_before_decl_preserves_identity();
+ 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_out_of_order_decl_block_in_lambda_compiles_correctly();
+ return 22;
}
@@ -516,5 +521,133 @@ public void Can_embed_normal_Expression_into_LightExpression_eg_as_Constructor_a
Asserts.IsInstanceOf(func());
}
+
+ // Tests for decl vs ref nodes and out-of-order decl in lambdas/blocks
+
+ ///
+ /// In the flat encoding, a lambda stores body first then parameters.
+ /// So when reading, parameter refs in the body are encountered BEFORE
+ /// the parameter decl node in the parameter list (out-of-order decl).
+ /// Both should resolve to the exact same SysParameterExpression.
+ ///
+ public void Flat_lambda_parameter_ref_before_decl_preserves_identity()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ // body uses p: ref nodes come first when the lambda is encoded/read
+ fe.RootIndex = fe.Lambda>(fe.Add(p, fe.ConstantInt(1)), p);
+
+ var sysLambda = (System.Linq.Expressions.LambdaExpression)fe.ToExpression();
+ var add = (System.Linq.Expressions.BinaryExpression)sysLambda.Body;
+
+ // The parameter in the params list and its ref in the body must be the same object
+ Asserts.AreSame(sysLambda.Parameters[0], add.Left);
+ }
+
+ ///
+ /// A parameter referenced more than once in a lambda body (all refs are
+ /// out-of-order relative to the single decl at the end of the child list)
+ /// must all resolve to the same SysParameterExpression.
+ ///
+ public void Flat_lambda_multiple_parameter_refs_all_yield_same_identity()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ // p * p + p: three independent refs to the same parameter
+ fe.RootIndex = fe.Lambda>(
+ fe.Add(fe.MakeBinary(System.Linq.Expressions.ExpressionType.Multiply, p, p), p),
+ p);
+
+ var sysLambda = (System.Linq.Expressions.LambdaExpression)fe.ToExpression();
+ var add = (System.Linq.Expressions.BinaryExpression)sysLambda.Body;
+ var mul = (System.Linq.Expressions.BinaryExpression)add.Left;
+ var paramDecl = sysLambda.Parameters[0];
+
+ Asserts.AreSame(paramDecl, mul.Left);
+ Asserts.AreSame(paramDecl, mul.Right);
+ Asserts.AreSame(paramDecl, add.Right);
+ }
+
+ ///
+ /// Block variables are read before body expressions (normal order),
+ /// but each variable index is cloned whenever it appears as a child.
+ /// All clones must resolve to the same SysParameterExpression.
+ ///
+ public void Flat_block_variables_and_refs_yield_same_identity()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ var v1 = fe.Variable(typeof(int), "v1");
+ var v2 = fe.Variable(typeof(int), "v2");
+ // { int v1, v2; v1 = p; v2 = v1 + 1; v2 }
+ var block = fe.Block(typeof(int),
+ new[] { v1, v2 },
+ fe.Assign(v1, p),
+ fe.Assign(v2, fe.Add(v1, fe.ConstantInt(1))),
+ v2);
+ fe.RootIndex = fe.Lambda>(block, p);
+
+ var sysLambda = (System.Linq.Expressions.LambdaExpression)fe.ToExpression();
+ var sysBlock = (System.Linq.Expressions.BlockExpression)sysLambda.Body;
+ var assign1 = (System.Linq.Expressions.BinaryExpression)sysBlock.Expressions[0]; // v1 = p
+ var assign2 = (System.Linq.Expressions.BinaryExpression)sysBlock.Expressions[1]; // v2 = v1 + 1
+ var addExpr = (System.Linq.Expressions.BinaryExpression)assign2.Right; // v1 + 1
+
+ // v1 decl and its ref on the left of assign1 are the same object
+ Asserts.AreSame(sysBlock.Variables[0], assign1.Left);
+ // v1 decl and its ref inside the add expression are the same object
+ Asserts.AreSame(sysBlock.Variables[0], addExpr.Left);
+ // v2 decl and its ref on the left of assign2 are the same object
+ Asserts.AreSame(sysBlock.Variables[1], assign2.Left);
+ // v2 decl and the final block result expression are the same object
+ Asserts.AreSame(sysBlock.Variables[1], sysBlock.Expressions[2]);
+ }
+
+ ///
+ /// An outer lambda parameter captured in a nested lambda body creates
+ /// a ref node in the nested lambda scope. All three occurrences —
+ /// the outer params list, the inner body, and any outer body usage —
+ /// must resolve to the exact same SysParameterExpression.
+ ///
+ public void Flat_nested_lambda_captures_outer_parameter_identity()
+ {
+ var fe = default(ExprTree);
+ var x = fe.ParameterOf("x");
+ // outer: x => () => x (inner lambda closes over outer param)
+ var inner = fe.Lambda>(x);
+ fe.RootIndex = fe.Lambda>>(inner, x);
+
+ var sysOuter = (System.Linq.Expressions.LambdaExpression)fe.ToExpression();
+ var sysInner = (System.Linq.Expressions.LambdaExpression)sysOuter.Body;
+
+ // The inner lambda body (the x ref) must be the same object as the outer param decl
+ Asserts.AreSame(sysOuter.Parameters[0], sysInner.Body);
+ }
+
+ ///
+ /// End-to-end compile-and-run test with a block containing two variables,
+ /// verifying that out-of-order parameter decls and variable refs produce
+ /// a correctly executing delegate.
+ ///
+ public void Flat_out_of_order_decl_block_in_lambda_compiles_correctly()
+ {
+ var fe = default(ExprTree);
+ var p = fe.ParameterOf("p");
+ var v1 = fe.Variable(typeof(int), "v1");
+ var v2 = fe.Variable(typeof(int), "v2");
+ // (int p) => { int v1 = p * 2; int v2 = v1 + p; v2 }
+ var block = fe.Block(typeof(int),
+ new[] { v1, v2 },
+ fe.Assign(v1, fe.MakeBinary(System.Linq.Expressions.ExpressionType.Multiply, p, fe.ConstantInt(2))),
+ fe.Assign(v2, fe.Add(v1, p)),
+ v2);
+ fe.RootIndex = fe.Lambda>(block, p);
+
+ var func = (Func)((System.Linq.Expressions.LambdaExpression)fe.ToExpression()).Compile();
+ // p=3 → v1 = 3*2=6, v2 = 6+3=9
+ Asserts.AreEqual(9, func(3));
+ // p=0 → v1 = 0, v2 = 0
+ Asserts.AreEqual(0, func(0));
+ }
}
}
From 170ad9016863d85ca01a8e389d27d0fa01ea941e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Apr 2026 10:03:46 +0000
Subject: [PATCH 5/7] docs(#533): annotate Lambda and Block node child-list
layouts in FlatExpression.cs
Add layout comments in all three locations (factory methods, Builder cases,
Reader cases) explaining:
- Lambda: children[0]=body, children[1..n]=param decls; out-of-order decl pattern
- Block: children[0]=ChildList(vars) [optional], children[last]=ChildList(exprs)
with the children.Count==2 sentinel for variable presence
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/4646dbb8-d85d-44da-9029-87386284f968
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 43 +++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index fa8f6891..8945ee47 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -335,6 +335,18 @@ public int Block(params int[] expressions) =>
Block(null, null, expressions);
/// Adds a block node with optional explicit result type and variables.
+ ///
+ /// Child layout of the Block node depends on whether there are explicit variables:
+ ///
+ /// - With variables: children[0] = ChildList(variable₀, variable₁, …)
+ /// children[1] = ChildList(expr₀, expr₁, …)
+ /// - Without variables: children[0] = ChildList(expr₀, expr₁, …)
+ ///
+ /// A children.Count == 2 check is therefore the canonical way to detect variables.
+ /// 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]).
+ ///
public int Block(Type type, IEnumerable variables, params int[] expressions)
{
if (expressions == null || expressions.Length == 0)
@@ -361,6 +373,19 @@ public int Lambda(int body, params int[] parameters) where TDelegate
Lambda(typeof(TDelegate), body, parameters);
/// Adds a lambda node.
+ ///
+ /// Child layout of the Lambda node:
+ ///
+ /// - children[0] = body expression
+ /// - children[1…n] = parameter decl nodes (parameter₀, parameter₁, …)
+ ///
+ /// The body is stored first; parameter decl nodes follow. This means that when the
+ /// body contains refs to those parameters, the ref nodes are encountered by the
+ /// before the corresponding decl node — an intentional
+ /// 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.
+ ///
public int Lambda(Type delegateType, int body, params int[] parameters) =>
parameters == null || parameters.Length == 0
? AddFactoryExpressionNode(delegateType, null, ExpressionType.Lambda, 0, body)
@@ -734,6 +759,10 @@ private int AddExpression(SysExpr expression)
}
case ExpressionType.Lambda:
{
+ // Layout: children[0] = body, children[1..n] = parameter decl nodes.
+ // Body is stored before parameters so that the Reader encounters parameter
+ // refs in the body before their decl nodes (out-of-order decl); identity
+ // is preserved via the shared _parametersById id-map.
var lambda = (System.Linq.Expressions.LambdaExpression)expression;
ChildList children = default;
children.Add(AddExpression(lambda.Body));
@@ -743,6 +772,10 @@ private int AddExpression(SysExpr expression)
}
case ExpressionType.Block:
{
+ // Layout (with variables): children[0] = ChildList(var₀, var₁, …)
+ // children[1] = ChildList(expr₀, expr₁, …)
+ // Layout (without variables): children[0] = ChildList(expr₀, expr₁, …)
+ // 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)
@@ -1319,6 +1352,9 @@ public SysExpr ReadExpression(int index)
}
case ExpressionType.Lambda:
{
+ // Layout: children[0] = body, children[1..n] = parameter decl nodes.
+ // Body is read first; parameter refs inside it are resolved via _parametersById
+ // even before the decl nodes at children[1..n] are visited (out-of-order decl).
var children = GetChildren(index);
var body = ReadExpression(children[0]);
var parameters = new SysParameterExpression[children.Count - 1];
@@ -1328,6 +1364,13 @@ public SysExpr ReadExpression(int index)
}
case ExpressionType.Block:
{
+ // Layout (with variables): children[0] = ChildList(var₀, var₁, …)
+ // children[1] = ChildList(expr₀, expr₁, …)
+ // Layout (without variables): children[0] = ChildList(expr₀, expr₁, …)
+ // children.Count == 2 is the canonical test for the presence of variables.
+ // Variable decl nodes in children[0] are registered in _parametersById before
+ // the body expressions in children[1] are read, so refs in the body resolve
+ // to the same SysParameterExpression object as the decl (normal order here).
var children = GetChildren(index);
var hasVariables = children.Count == 2;
var variableIndexes = hasVariables ? GetChildren(children[0]) : default;
From 02e3372f79472cb1f6570bc726dcd006cfc04298 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Apr 2026 10:26:08 +0000
Subject: [PATCH 6/7] feat(#533): inline small-enum constants in _data to avoid
boxing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Extend the inline-constant path to cover enum types whose underlying type
is ≤ 32 bits (byte/sbyte/char/short/ushort/int/uint — virtually all C# enums).
Raw bits are stored in _data via System.Convert.ToInt64 + uint cast; the
Reader reconstructs the typed enum via Enum.ToObject using the underlying
TypeCode. Long/ulong-backed enums (extremely rare) continue to be boxed
in Obj.
Add FlatExpressionThrow.UnsupportedInlineConstantType(Type, TypeCode)
overload for the error path in ReadInlineValue.
Add Flat_enum_constant_stored_inline_roundtrip test covering all six
underlying types (byte/sbyte/short/ushort/int/uint), verifying no
ClosureConstants allocation and correct value round-trip. (1679 tests)
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/0ffda673-c511-49f5-ad08-e070318b3781
Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
---
.../FlatExpression.cs | 44 +++++++++++++++++--
.../LightExpressionTests.cs | 39 +++++++++++++++-
2 files changed, 78 insertions(+), 5 deletions(-)
diff --git a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
index 8945ee47..5f2dca69 100644
--- a/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
+++ b/src/FastExpressionCompiler.LightExpression/FlatExpression.cs
@@ -190,9 +190,18 @@ public int Constant(object value) =>
/// Adds a constant node with an explicit constant type.
public int Constant(object value, Type type)
{
- if (value == null || value is string || value is Type || type.IsEnum || value is decimal)
+ if (value == null || value is string || value is Type || value is decimal)
return AddRawExpressionNode(type, value, ExpressionType.Constant);
+ if (type.IsEnum)
+ {
+ var underlyingTc = Type.GetTypeCode(Enum.GetUnderlyingType(type));
+ if (IsSmallPrimitive(underlyingTc))
+ return AddInlineConstantNode(type, (uint)System.Convert.ToInt64(value));
+ // long/ulong-backed enum (extremely rare): store boxed in Obj
+ return AddRawExpressionNode(type, value, ExpressionType.Constant);
+ }
+
if (type.IsPrimitive)
{
var tc = Type.GetTypeCode(type);
@@ -1003,8 +1012,17 @@ private int AddConstant(System.Linq.Expressions.ConstantExpression constant)
var value = constant.Value;
var type = constant.Type;
- if (value == null || value is string || value is Type || type.IsEnum || value is decimal)
+ if (value == null || value is string || value is Type || value is decimal)
+ return _tree.AddRawExpressionNode(type, value, ExpressionType.Constant);
+
+ if (type.IsEnum)
+ {
+ var underlyingTc = Type.GetTypeCode(Enum.GetUnderlyingType(type));
+ if (IsSmallPrimitive(underlyingTc))
+ return _tree.AddInlineConstantNode(type, (uint)System.Convert.ToInt64(value));
+ // long/ulong-backed enum (extremely rare): store boxed in Obj
return _tree.AddRawExpressionNode(type, value, ExpressionType.Constant);
+ }
if (type.IsPrimitive)
{
@@ -1669,8 +1687,21 @@ private ChildList GetChildren(int index)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static object ReadInlineValue(Type type, uint data) =>
- Type.GetTypeCode(type) switch
+ private static object ReadInlineValue(Type type, uint data)
+ {
+ if (type.IsEnum)
+ return Enum.ToObject(type, Type.GetTypeCode(Enum.GetUnderlyingType(type)) switch
+ {
+ TypeCode.Byte => (object)(byte)data,
+ TypeCode.SByte => (object)(sbyte)(byte)data,
+ TypeCode.Char => (object)(char)(ushort)data,
+ TypeCode.Int16 => (object)(short)(ushort)data,
+ TypeCode.UInt16 => (object)(ushort)data,
+ TypeCode.Int32 => (object)(int)data,
+ TypeCode.UInt32 => (object)data,
+ var tc => FlatExpressionThrow.UnsupportedInlineConstantType