(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 .
public static class FlatExpressionExtensions
{
diff --git a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
index b91c23c0..eea42063 100644
--- a/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
+++ b/test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs
@@ -34,7 +34,13 @@ 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();
+ Flat_enum_constant_stored_inline_roundtrip();
+ return 23;
}
@@ -516,5 +522,169 @@ 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));
+ }
+
+ enum ByteEnum : byte { A = 1, B = 200 }
+ enum SByteEnum : sbyte { A = -1, B = 50 }
+ enum ShortEnum : short { A = -1000, B = 30000 }
+ enum UShortEnum : ushort { A = 0, B = 60000 }
+ enum IntEnum : int { A = int.MinValue, B = 42 }
+ enum UIntEnum : uint { A = 0, B = uint.MaxValue }
+
+ public void Flat_enum_constant_stored_inline_roundtrip()
+ {
+ // Verify that enum constants with ≤32-bit underlying types are stored inline
+ // (no ClosureConstants entry, no boxing) and round-trip correctly.
+ void Check(TEnum enumValue) where TEnum : Enum
+ {
+ var fe = default(ExprTree);
+ var idx = fe.Constant(enumValue, typeof(TEnum));
+ Asserts.AreEqual(0, fe.ClosureConstants.Count,
+ $"{typeof(TEnum).Name}.{enumValue} should be inline (no ClosureConstants), but got {fe.ClosureConstants.Count}");
+ fe.RootIndex = fe.Lambda>(idx);
+ var result = (TEnum)((System.Linq.Expressions.LambdaExpression)fe.ToExpression()).Compile().DynamicInvoke()!;
+ Asserts.AreEqual(enumValue, result, $"Round-trip failed for {typeof(TEnum).Name}.{enumValue}");
+ }
+
+ Check(ByteEnum.A);
+ Check(ByteEnum.B);
+ Check(SByteEnum.A);
+ Check(SByteEnum.B);
+ Check(ShortEnum.A);
+ Check(ShortEnum.B);
+ Check(UShortEnum.A);
+ Check(UShortEnum.B);
+ Check(IntEnum.A);
+ Check(IntEnum.B);
+ Check(UIntEnum.A);
+ Check(UIntEnum.B);
+ }
}
}