From 0457a35774f7adce91c9a2f3d93eaa578e805856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 22 Jan 2026 00:16:53 +0100 Subject: [PATCH 1/2] Add test suite for constructor call expression --- .../Program.cs | 2 + .../ConstructorCallTests.cs | 434 ++++++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 test/FastExpressionCompiler.UnitTests/ConstructorCallTests.cs diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 45082583..8fdb0482 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -98,6 +98,8 @@ void Run(Func run, string name = null) Run(new LightExpression.UnitTests.ConvertOperatorsTests().Run); Run(new DefaultTests().Run); Run(new LightExpression.UnitTests.DefaultTests().Run); + Run(new ConstructorCallTests().Run); + Run(new LightExpression.UnitTests.ConstructorCallTests().Run); Run(new EqualityOperatorsTests().Run); Run(new LightExpression.UnitTests.EqualityOperatorsTests().Run); Run(new GotoTests().Run); diff --git a/test/FastExpressionCompiler.UnitTests/ConstructorCallTests.cs b/test/FastExpressionCompiler.UnitTests/ConstructorCallTests.cs new file mode 100644 index 00000000..83da123f --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/ConstructorCallTests.cs @@ -0,0 +1,434 @@ + +using System; +using System.Reflection; + +#if LIGHT_EXPRESSION +using static FastExpressionCompiler.LightExpression.Expression; +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +using static System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.UnitTests; +#endif + +public class ConstructorCallTests : ITest +{ + public int Run() + { + Constructor_ignored_result(TestClass.Ctor); + Constructor_ignored_result(TestStruct.Ctor); + + Constructor_read_struct_property(TestClass.Ctor, "A"); + Constructor_read_struct_property(TestClass.Ctor, "PropertyA"); + Constructor_read_struct_property(TestStruct.Ctor, "A"); + Constructor_read_struct_property(TestStruct.Ctor, "PropertyA"); + + Constructor_read_reference_property(TestClass.Ctor, "B"); + Constructor_read_reference_property(TestClass.Ctor, "PropertyB"); + Constructor_read_reference_property(TestStruct.Ctor, "B"); + Constructor_read_reference_property(TestStruct.Ctor, "PropertyB"); + + Constructor_assign_ignore_property(TestClass.Ctor); + Constructor_assign_ignore_property(TestStruct.Ctor); + Constructor_assign_read_property(TestClass.Ctor); + Constructor_assign_read_property(TestStruct.Ctor); + Constructor_assign_ignore_property_struct_parameter(); + + Read_field_in_ctor_param(); + + Nested_constructor_calls(); + Condition_in_constructor_arguments(); + TryCatch_in_constructor_arguments(); + Constructor_in_array_index(); + Constructor_in_array_index_argument(); + Constructor_in_custom_indexer_argument(); + Constructor_in_indexer_target_struct(TestClass.Ctor); + Constructor_in_indexer_target_struct(TestStruct.Ctor); + Nullable_constructor_and_conversion(); + Ctor_in_instance_method_this(TestClass.Ctor); + Ctor_in_instance_method_this(TestStruct.Ctor); + Ctor_in_static_method_arg(TestClass.Ctor); + Ctor_in_static_method_arg(TestStruct.Ctor); + Constructor_nested_in_block_member_access(); + Constructor_conversions_and_boxing(); + + return 32; + } + + public void Constructor_conversions_and_boxing() + { + var fBox = Lambda>( + Convert(New(TestStruct.Ctor, Constant(42), Constant("a")), typeof(object)) + ).CompileFast(true); + Asserts.AreEqual(42, ((TestStruct)fBox()).A); + + var fInterface = Lambda>( + Convert(New(TestStruct.Ctor, Constant(43), Constant("a")), typeof(ITestInterfacce)) + ).CompileFast(true); + Asserts.AreEqual(43, fInterface().PropertyA); + + var fImplicit = Lambda>( + Convert(New(TestStruct.Ctor, Constant(44), Constant("a")), typeof(int)) + ).CompileFast(true); + Asserts.AreEqual(44, fImplicit()); + + var fClassBox = Lambda>( + Convert(New(TestClass.Ctor, Constant(45), Constant("a")), typeof(object)) + ).CompileFast(true); + Asserts.AreEqual(45, ((TestClass)fClassBox()).A); + } + + public void Constructor_ignored_result(ConstructorInfo ctor) + { + var f = Lambda(New(ctor, Constant(++Ctr), Constant("abc"))).CompileFast(true); + + Asserts.IsNotNull(f); + f(); + + Asserts.AreEqual(LastA, Ctr); + } + + public void Constructor_read_struct_property(ConstructorInfo ctor, string prop) + { + var f = Lambda>( + PropertyOrField(New(ctor, Constant(++Ctr), Constant("abc")), prop) + ).CompileFast(true); + + Asserts.IsNotNull(f); + + Asserts.AreEqual(Ctr, f()); + Asserts.AreEqual(LastA, Ctr); + } + + public void Constructor_read_reference_property(ConstructorInfo ctor, string prop) + { + var param = Parameter(typeof(string)); + var f = Lambda>( + PropertyOrField(New(ctor, Constant(0), param), prop), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + + Asserts.AreEqual("test1", f("test1")); + Asserts.AreEqual("test2", f("test2")); + } + + public void Constructor_assign_ignore_property(ConstructorInfo ctor) + { + var param = Parameter(typeof(int)); + var f = Lambda>( + Assign(Property(New(ctor, Constant(0), Constant("test")), "PropertyA"), param), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + + f(44); + Asserts.AreEqual(44, LastA); + f(45); + Asserts.AreEqual(45, LastA); + } + + public void Constructor_assign_read_property(ConstructorInfo ctor) + { + var param = Parameter(typeof(int)); + var f = Lambda>( + Assign(Property(New(ctor, Constant(0), Constant("test")), "PropertyA"), param), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + + Asserts.AreEqual(46, f(46)); + Asserts.AreEqual(46, LastA); + Asserts.AreEqual(47, f(47)); + Asserts.AreEqual(47, LastA); + } + + public void Constructor_assign_ignore_property_struct_parameter() + { + var structParam = Parameter(typeof(TestStruct)); + var valueParam = Parameter(typeof(int)); + + var f = Lambda>( + Assign(Property(structParam, "PropertyA"), valueParam), + structParam, valueParam + ).CompileFast(true); + + Asserts.IsNotNull(f); + + var s = new TestStruct(0, "x"); + f(s, 44); + Asserts.AreEqual(44, LastA); + f(s, 45); + Asserts.AreEqual(45, LastA); + } + + public TestStruct fieldStruct; + public void Read_field_in_ctor_param() + { + var param = Parameter(typeof(ConstructorCallTests)); + var f = Lambda>( + New(TestClass.Ctor, Field(Field(param, nameof(fieldStruct)), "A"), Field(Field(param, nameof(fieldStruct)), "B")), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + + this.fieldStruct = new(12333, "1"); + var result = f(this); + Asserts.AreEqual(12333, result.A); + Asserts.AreEqual("1", result.B); + } + + public void Nested_constructor_calls() + { + var f = Lambda>( + New(TestClass.Ctor, + PropertyOrField(New(TestClass.Ctor, Constant(100), Constant("inner")), "A"), + Constant("outer")) + ).CompileFast(true); + + Asserts.IsNotNull(f); + var result = f(); + Asserts.AreEqual(100, result.A); + Asserts.AreEqual("outer", result.B); + } + + public void Condition_in_constructor_arguments() + { + var param = Parameter(typeof(bool)); + var f = Lambda>( + New(TestClass.Ctor, + Condition(param, Constant(42), Constant(99)), + Condition(param, Constant("a"), Constant("b"))), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + + Asserts.AreEqual(42, f(true).A); + Asserts.AreEqual("a", f(true).B); + Asserts.AreEqual(99, f(false).A); + Asserts.AreEqual("b", f(false).B); + } + + public void TryCatch_in_constructor_arguments() + { + var param = Parameter(typeof(int)); + var exVar = Parameter(typeof(Exception)); + + var f = Lambda>( + New(TestClass.Ctor, + TryCatch( + Divide(Constant(123), param), + Catch(exVar, Constant(456)) + ), + TryCatch( + Call(Divide(Constant(100), param), "ToString", Type.EmptyTypes), + Catch(exVar, Constant("caught")) + )), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + + var resultNoException = f(1); + Asserts.AreEqual(123, resultNoException.A); + Asserts.AreEqual("100", resultNoException.B); + + var resultWithException = f(0); + Asserts.AreEqual(456, resultWithException.A); + Asserts.AreEqual("caught", resultWithException.B); + } + + public void Constructor_in_array_index() + { + TestClass[] array = [ new(0, "zero"), new(1, "one"), new(2, "two") ]; + + var f = Lambda>( + ArrayIndex( + Constant(array), + PropertyOrField(New(TestClass.Ctor, Constant(1), Constant("index")), "A")) + ).CompileFast(true); + + Asserts.IsNotNull(f); + var result = f(); + + Asserts.AreEqual(1, result.A); + Asserts.AreEqual("one", result.B); + } + + public void Constructor_in_array_index_argument() + { + TestClass[] array = [ new(0, "zero"), new(1, "one"), new(2, "two") ]; + + var intPtrCtor = typeof(IntPtr).GetConstructor([typeof(int)])!; + + var indexExpr = Convert(New(intPtrCtor, Constant(0)), typeof(int)); + var f = Lambda>( + ArrayIndex( + Constant(array), + indexExpr) + ).CompileFast(true); + + Asserts.IsNotNull(f); + var result = f(); + + Asserts.AreEqual(0, result.A); + Asserts.AreEqual("zero", result.B); + } + + public void Constructor_in_custom_indexer_argument() + { + var intPtrCtor = typeof(IntPtr).GetConstructor([typeof(int)])!; + + var indexer = typeof(TestClass).GetProperty("Item", [typeof(IntPtr)])!; + var p = Parameter(typeof(TestClass)); + var f = Lambda>( + MakeIndex(p, indexer, [New(intPtrCtor, Constant(2))]), + p + ).CompileFast(true); + + Asserts.AreEqual(42, f(new(40, "x"))); + } + + public void Constructor_in_indexer_target_struct(ConstructorInfo ctor) + { + var indexer = ctor.DeclaringType!.GetProperty("Item", [typeof(nint)])!; + + var f = Lambda>( + MakeIndex( + New(ctor, Constant(40), Constant("x")), + indexer, + [Constant(new IntPtr(2))]) + ).CompileFast(true); + + Asserts.AreEqual(42, f()); + } + + public void Nullable_constructor_and_conversion() + { + var ctor = typeof(int?).GetConstructor([typeof(int)]); + + var fConvert = Lambda>( + Convert(New(ctor, Constant(42)), typeof(int)) + ).CompileFast(true); + + Asserts.IsNotNull(fConvert); + Asserts.AreEqual(42, fConvert()); + + var fValue = Lambda>( + Property(New(ctor, Constant(43)), "Value") + ).CompileFast(true); + + Asserts.IsNotNull(fValue); + Asserts.AreEqual(43, fValue()); + } + + private static void Ctor_in_instance_method_this(ConstructorInfo ctor) + { + var type = ctor.DeclaringType; + + var param = Parameter(typeof(int)); + var f = Lambda>( + Call(New(ctor, Constant(10), Constant("arg")), "InstanceAdd", [], param), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + Asserts.AreEqual(15, f(5)); + Asserts.AreEqual(25, f(15)); + } + + private static void Ctor_in_static_method_arg(ConstructorInfo ctor) + { + var param = Parameter(typeof(int)); + var f = Lambda>( + Call(ctor.DeclaringType, "StaticAdd", [], New(ctor, Constant(10), Constant("arg")), param), + param + ).CompileFast(true); + + Asserts.IsNotNull(f); + Asserts.AreEqual(15, f(5)); + Asserts.AreEqual(25, f(15)); + } + + [ThreadStatic] + static int Ctr = 1; + + [ThreadStatic] + static int LastA; + [ThreadStatic] + static string LastB; + + public void Constructor_nested_in_block_member_access() + { + var block = Block( + New(typeof(TestStruct)), + Constant(new TestStruct(42, "s")) + ); + + var expr = Property(block, nameof(TestStruct.PropertyA)); + + var lambda = Lambda>(expr); + + var fastCompiled = lambda.CompileFast(true); + Asserts.AreEqual(42, fastCompiled()); + } + + public interface ITestInterfacce { int PropertyA { get; } } + + public struct TestStruct : ITestInterfacce + { + public static readonly ConstructorInfo Ctor = typeof(TestStruct).GetConstructors()[0]; + public int A; + + public string B; + public int PropertyA + { + get => A; + set => LastA = A = value; + } + public string PropertyB => B; + public TestStruct(int a, string b) + { + LastA = this.A = a; + LastB = this.B = b; + } + + public int this[nint delta] => A + (int)delta; + + public int InstanceAdd(int value) => A + value; + + public static int StaticAdd(TestStruct s, int value) => s.A + value; + + public static implicit operator int(TestStruct s) => s.A; + } + + public class TestClass : ITestInterfacce + { + public static readonly ConstructorInfo Ctor = typeof(TestClass).GetConstructors()[0]; + public int A; + + public string B; + public int PropertyA + { + get => A; + set => LastA = A = value; + } + public string PropertyB => B; + public TestClass(int a, string b) + { + LastA = this.A = a; + LastB = this.B = b; + } + + public int this[nint offset] => A + (int)offset; + + public int InstanceAdd(int value) => A + value; + + public static int StaticAdd(TestClass c, int value) => c.A + value; + } + +} From 62bde56051e68584617adf8c060161787fdcabe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Thu, 22 Jan 2026 00:17:16 +0100 Subject: [PATCH 2/2] Fix ParentFlags handling in TryEmitNewExpression * new ParentFlags is created, similarly to TryEmitMethodCall - this avoids unwanted propagation of IgnoreResult, MemberAccess, ... * handle ParentFlags.InstanceAccess - emit load address instruction * set closure.LastEmitIsAddress (this should be reset to avoid leaking this value from the emit of last argument) * handle IgnoresResult --- .../FastExpressionCompiler.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 2ee422ec..e36868d8 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -2430,7 +2430,7 @@ private static bool TryEmitNew(Expression expr, IReadOnlyList paramExprs, IL CompilerFlags setup, ParentFlags parent) #endif { - parent |= ParentFlags.CtorCall; + var flags = ParentFlags.CtorCall; var newExpr = (NewExpression)expr; #if SUPPORTS_ARGUMENT_PROVIDER var argExprs = (IArgumentProvider)newExpr; @@ -2449,7 +2449,7 @@ private static bool TryEmitNew(Expression expr, IReadOnlyList paramExprs, IL // see the #488 for the details. if (argCount == 1) { - if (!TryEmit(argExprs.GetArgument(0), paramExprs, il, ref closure, setup, parent, pars[0].ParameterType.IsByRef ? 0 : -1)) + if (!TryEmit(argExprs.GetArgument(0), paramExprs, il, ref closure, setup, flags, pars[0].ParameterType.IsByRef ? 0 : -1)) return false; } else @@ -2457,7 +2457,7 @@ private static bool TryEmitNew(Expression expr, IReadOnlyList paramExprs, IL if (!closure.ArgsContainingComplexExpression.Map.ContainsKey(newExpr)) { for (var i = 0; i < argCount; ++i) - if (!TryEmit(argExprs.GetArgument(i), paramExprs, il, ref closure, setup, parent, pars[i].ParameterType.IsByRef ? i : -1)) + if (!TryEmit(argExprs.GetArgument(i), paramExprs, il, ref closure, setup, flags, pars[i].ParameterType.IsByRef ? i : -1)) return false; } else @@ -2467,7 +2467,7 @@ private static bool TryEmitNew(Expression expr, IReadOnlyList paramExprs, IL { var argExpr = argExprs.GetArgument(i); var parType = pars[i].ParameterType; - if (!TryEmit(argExpr, paramExprs, il, ref closure, setup, parent, parType.IsByRef ? i : -1)) + if (!TryEmit(argExpr, paramExprs, il, ref closure, setup, flags, parType.IsByRef ? i : -1)) return false; argVars.Add(EmitStoreLocalVariable(il, parType)); } @@ -2476,21 +2476,32 @@ private static bool TryEmitNew(Expression expr, IReadOnlyList paramExprs, IL } } } + var newType = newExpr.Type; // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (ctor != null) il.Demit(OpCodes.Newobj, ctor); - else if (newExpr.Type.IsValueType) + else if (newType.IsValueType) { - ctor = newExpr.Type.GetConstructor( + ctor = newType.GetConstructor( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, default, CallingConventions.Any, Tools.Empty(), default); if (ctor != null) il.Demit(OpCodes.Newobj, ctor); else - EmitLoadLocalVariable(il, InitValueTypeVariable(il, newExpr.Type)); + EmitLoadLocalVariable(il, InitValueTypeVariable(il, newType)); } else return false; + + closure.LastEmitIsAddress = newType.IsValueType && (parent & ParentFlags.InstanceAccess) != 0 && !parent.IgnoresResult(); + if (closure.LastEmitIsAddress) + { + EmitStoreAndLoadLocalVariableAddress(il, newType); + } + + if (parent.IgnoresResult()) + il.Demit(OpCodes.Pop); + return true; } @@ -5081,7 +5092,7 @@ public static bool TryEmitMemberGet(MemberExpression expr, // if the field is not used as an index, #302 // or if the field is not accessed from the just constructed object `new Widget().DodgyValue`, #333 if (((parent & ParentFlags.InstanceAccess) != 0 & - (parent & (ParentFlags.IndexAccess | ParentFlags.Ctor)) == 0) && field.FieldType.IsValueType) + (parent & ParentFlags.IndexAccess) == 0) && field.FieldType.IsValueType) isByAddress = true; // we don't need to duplicate the instance if we are working with the field address to save to it directly,