From dcfc28f2d6a1b6b211dd56d29e0bde8cc1e175dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Mon, 2 Nov 2026 17:05:32 +0100 Subject: [PATCH] Fix compile of int.GetType() GetType is not virtual, but it still need to be called with Constrained prefix, since it needs boxing. The previous check can be simplified to DeclaringType != exprType, I couldn't find any example where direct call could be used for method on other declaring type. --- .../FastExpressionCompiler.cs | 6 +- .../Program.cs | 2 + .../Program.cs | 2 + .../CallTests.cs | 226 ++++++++++++++++++ 4 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/CallTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index d95a7cf6..b51e7d0c 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -5556,13 +5556,11 @@ private static bool TryEmitMethodCall(Expression expr, ILGenerator il, ref Compi var ok = true; if (!objIsValueType) ok = EmitMethodCallOrVirtualCall(il, method); - else if (method.DeclaringType != typeof(Enum) && - (!method.IsVirtual || - method.DeclaringType == objExpr.Type || - objExpr is ParameterExpression pe && pe.IsByRef)) + else if (method.DeclaringType == objExpr.Type) ok = EmitMethodCall(il, method); else { + // method on base class (System.Enum, System.Object) or interface il.Demit(OpCodes.Constrained, objExpr.Type); ok = EmitVirtualMethodCall(il, method); } diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index b14becd1..9d2f88f6 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -133,6 +133,8 @@ void Run(Func run, string name = null) Run(new LightExpression.UnitTests.BinaryExpressionTests().Run); Run(new BlockTests().Run); Run(new LightExpression.UnitTests.BlockTests().Run); + Run(new CallTests().Run); + Run(new LightExpression.UnitTests.CallTests().Run); Run(new ClosureConstantTests().Run); Run(new LightExpression.UnitTests.ClosureConstantTests().Run); Run(new CoalesceTests().Run); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index b4cdbea1..70ad71c2 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -148,6 +148,8 @@ void Run(Func run, string name = null) Run(new LightExpression.UnitTests.BinaryExpressionTests().Run); Run(new BlockTests().Run); Run(new LightExpression.UnitTests.BlockTests().Run); + Run(new CallTests().Run); + Run(new LightExpression.UnitTests.CallTests().Run); Run(new ClosureConstantTests().Run); Run(new LightExpression.UnitTests.ClosureConstantTests().Run); Run(new CoalesceTests().Run); diff --git a/test/FastExpressionCompiler.UnitTests/CallTests.cs b/test/FastExpressionCompiler.UnitTests/CallTests.cs new file mode 100644 index 00000000..aeaf898b --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/CallTests.cs @@ -0,0 +1,226 @@ +using System; + +#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 CallTests : ITest +{ + public int Run() + { + GetType_on_int_parameter(); + GetType_on_local_int(); + GetType_on_nullable(); + GetType_on_enum(); + Nonvirtual_struct_method(); + Virtual_struct_method(); + Inherited_virtual_struct_method(); + Inherited_ValueType_method(); + Enum_HasFlag(); + Enum_ToString(); + Interface_method_on_struct(); + Virtual_class_method(); + Ignored_call_result(); + Call_result_as_argument(); + Call_result_field_access(); + Void_call(); + return 16; + } + + public void GetType_on_int_parameter() + { + var value = Parameter(typeof(int), "value"); + var expression = Lambda>( + Call(value, typeof(object).GetMethod(nameof(GetType))), value); + + Asserts.AreEqual(typeof(int), expression.CompileSys()(42)); + Asserts.AreEqual(typeof(int), expression.CompileFast(true)(42)); + } + + public void GetType_on_local_int() + { + var value = Variable(typeof(int), "a"); + var expression = Lambda>(Block(new[] { value }, + Assign(value, Constant(1)), Call(value, typeof(object).GetMethod(nameof(GetType))))); + + Asserts.AreEqual(typeof(int), expression.CompileSys()()); + Asserts.AreEqual(typeof(int), expression.CompileFast(true)()); + } + + public void GetType_on_nullable() + { + var value = Parameter(typeof(int?), "value"); + var expression = Lambda>( + Call(value, typeof(object).GetMethod(nameof(GetType))), value); + + var system = expression.CompileSys(); + var fast = expression.CompileFast(true); + Asserts.AreEqual(typeof(int), system(42)); + Asserts.AreEqual(typeof(int), fast(42)); + Asserts.Throws(() => system(null)); + Asserts.Throws(() => fast(null)); + } + + public void GetType_on_enum() + { + var value = Parameter(typeof(Bits), "value"); + var expression = Lambda>( + Call(value, typeof(object).GetMethod(nameof(GetType))), value); + + Asserts.AreEqual(typeof(Bits), expression.CompileSys()(Bits.A)); + Asserts.AreEqual(typeof(Bits), expression.CompileFast(true)(Bits.A)); + } + + public void Nonvirtual_struct_method() + { + var counter = Parameter(typeof(Counter), "counter"); + var expression = Lambda>( + Call(counter, typeof(Counter).GetMethod(nameof(Counter.Add)), Constant(1)), counter); + + Asserts.AreEqual(43, expression.CompileSys()(new Counter { Value = 42 }).Value); + Asserts.AreEqual(43, expression.CompileFast(true)(new Counter { Value = 42 }).Value); + } + + public void Virtual_struct_method() + { + var counter = Parameter(typeof(Counter), "counter"); + var expression = Lambda>( + Call(counter, typeof(Counter).GetMethod(nameof(ToString))), counter); + + Asserts.AreEqual("counter", expression.CompileSys()(new Counter())); + Asserts.AreEqual("counter", expression.CompileFast(true)(new Counter())); + } + + public void Inherited_virtual_struct_method() + { + var counter = Parameter(typeof(Counter), "counter"); + var expression = Lambda>( + Call(counter, typeof(object).GetMethod(nameof(ToString))), counter); + + Asserts.AreEqual("counter", expression.CompileSys()(new Counter())); + Asserts.AreEqual("counter", expression.CompileFast(true)(new Counter())); + } + + public void Inherited_ValueType_method() + { + var value = Parameter(typeof(PlainValue), "value"); + var expression = Lambda>( + Call(value, typeof(ValueType).GetMethod(nameof(ToString))), value); + + Asserts.AreEqual(typeof(PlainValue).FullName, expression.CompileSys()(new PlainValue())); + Asserts.AreEqual(typeof(PlainValue).FullName, expression.CompileFast(true)(new PlainValue())); + } + + public void Enum_HasFlag() + { + var value = Parameter(typeof(Bits), "value"); + var expression = Lambda>(Call(value, + typeof(Enum).GetMethod(nameof(Enum.HasFlag)), Constant(Bits.B, typeof(Enum))), value); + + var system = expression.CompileSys(); + var fast = expression.CompileFast(true); + Asserts.IsTrue(system(Bits.A | Bits.B)); + Asserts.IsTrue(fast(Bits.A | Bits.B)); + Asserts.IsFalse(system(Bits.A)); + Asserts.IsFalse(fast(Bits.A)); + } + + public void Enum_ToString() + { + var value = Parameter(typeof(Bits), "value"); + var expression = Lambda>( + Call(value, typeof(Enum).GetMethod(nameof(ToString), Type.EmptyTypes)), value); + + Asserts.AreEqual("A, B", expression.CompileSys()(Bits.A | Bits.B)); + Asserts.AreEqual("A, B", expression.CompileFast(true)(Bits.A | Bits.B)); + } + + public void Interface_method_on_struct() + { + var counter = Parameter(typeof(Counter), "counter"); + var expression = Lambda>( + Call(counter, typeof(IAdder).GetMethod(nameof(IAdder.Add)), Constant(1)), counter); + + Asserts.AreEqual(143, expression.CompileSys()(new Counter { Value = 42 }).Value); + Asserts.AreEqual(143, expression.CompileFast(true)(new Counter { Value = 42 }).Value); + } + + public void Virtual_class_method() + { + var target = Parameter(typeof(Target), "target"); + var expression = Lambda>( + Call(target, typeof(Target).GetMethod(nameof(Target.GetValue))), target); + + Asserts.AreEqual(2, expression.CompileSys()(new DerivedTarget())); + Asserts.AreEqual(2, expression.CompileFast(true)(new DerivedTarget())); + } + + public void Ignored_call_result() + { + var counter = Variable(typeof(Counter), "counter"); + var expression = Lambda>(Block(new[] { counter }, + Assign(counter, Default(typeof(Counter))), + Call(counter, typeof(Counter).GetMethod(nameof(Counter.Add)), Constant(1)), + Field(counter, nameof(Counter.Value)))); + + Asserts.AreEqual(1, expression.CompileSys()()); + Asserts.AreEqual(1, expression.CompileFast(true)()); + } + + public void Call_result_as_argument() + { + var counter = Parameter(typeof(Counter), "counter"); + var expression = Lambda>( + Call(typeof(CallTests).GetMethod(nameof(ReadResult)), + Call(counter, typeof(Counter).GetMethod(nameof(Counter.Add)), Constant(1))), counter); + + Asserts.AreEqual(43, expression.CompileSys()(new Counter { Value = 42 })); + Asserts.AreEqual(43, expression.CompileFast(true)(new Counter { Value = 42 })); + } + + public void Call_result_field_access() + { + var counter = Parameter(typeof(Counter), "counter"); + var expression = Lambda>( + Field(Call(counter, typeof(Counter).GetMethod(nameof(Counter.Add)), Constant(1)), + nameof(Result.Value)), counter); + + Asserts.AreEqual(43, expression.CompileSys()(new Counter { Value = 42 })); + Asserts.AreEqual(43, expression.CompileFast(true)(new Counter { Value = 42 })); + } + + public void Void_call() + { + var counter = Variable(typeof(Counter), "counter"); + var expression = Lambda>(Block(new[] { counter }, + Assign(counter, Default(typeof(Counter))), + Call(counter, typeof(Counter).GetMethod(nameof(Counter.Increment))), + Field(counter, nameof(Counter.Value)))); + + Asserts.AreEqual(1, expression.CompileSys()()); + Asserts.AreEqual(1, expression.CompileFast(true)()); + } + + [Flags] + public enum Bits { A = 1, B = 2 } + public struct PlainValue { } + public struct Result { public int Value; } + public interface IAdder { Result Add(int amount); } + + public struct Counter : IAdder + { + public int Value; + public Result Add(int amount) => new Result { Value = Value += amount }; + Result IAdder.Add(int amount) => new Result { Value = Value += amount + 100 }; + public void Increment() => ++Value; + public override string ToString() => "counter"; + } + + public class Target { public virtual int GetValue() => 1; } + public class DerivedTarget : Target { public override int GetValue() => 2; } + public static int ReadResult(Result result) => result.Value; +}