From db3617364b32e5d04616e6cfbe2a07148f87b45b Mon Sep 17 00:00:00 2001 From: ayyyabean-alt Date: Fri, 23 Jan 2026 20:33:14 +0800 Subject: [PATCH] fix issue 490 (#1) * add unittest for issue490 * fix issue490 --- .../FastExpressionCompiler.cs | 39 ++++++++- .../Issue490_ref_struct_conditional.cs | 82 +++++++++++++++++++ .../Program.cs | 3 + .../Program.cs | 3 + 4 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 test/FastExpressionCompiler.IssueTests/Issue490_ref_struct_conditional.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 2ee422ec..f0783a0d 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -4368,7 +4368,8 @@ private static bool TryEmitArithmeticAndOrAssign( // required for calling the method on the value type parameter var objType = objExpr.Type; objVarByAddress = !closure.LastEmitIsAddress && objType.IsValueType && // todo: @wip avoid ad-hocking with parameter here - (objExpr.NodeType != ExpressionType.Parameter || !((ParameterExpression)objExpr).IsByRef); + (objExpr.NodeType != ExpressionType.Parameter || !((ParameterExpression)objExpr).IsByRef) && + !objType.IsByRefLike(); // ref struct types cannot be stored in local variables if (objVarByAddress) objVar = EmitStoreAndLoadLocalVariableAddress(il, objType); else @@ -4938,7 +4939,7 @@ private static bool TryEmitMethodCall(Expression expr, if (!TryEmit(objExpr, paramExprs, il, ref closure, setup, flags | ParentFlags.InstanceAccess)) return false; objIsValueType = objExpr.Type.IsValueType; - loadObjByAddress = objIsValueType && objExpr.NodeType != ExpressionType.Parameter && !closure.LastEmitIsAddress; + loadObjByAddress = objIsValueType && objExpr.NodeType != ExpressionType.Parameter && !closure.LastEmitIsAddress && !objExpr.Type.IsByRefLike(); } var parCount = methodParams.Length; @@ -5047,7 +5048,10 @@ public static bool TryEmitMemberGet(MemberExpression expr, // Value type special treatment to load address of value instance in order to call a method. // For the parameters, we will skip the address loading because the `LastEmitIsAddress == true` for `Ldarga`, // so the condition here will be skipped - if (!closure.LastEmitIsAddress && objExpr.Type.IsValueType) + // Ref struct types cannot be stored in local variables, so we skip them here + if (!closure.LastEmitIsAddress && + objExpr.Type.IsValueType && + !objExpr.Type.IsByRefLike()) EmitStoreAndLoadLocalVariableAddress(il, objExpr.Type); } @@ -5094,7 +5098,7 @@ public static bool TryEmitMemberGet(MemberExpression expr, closure.LastEmitIsAddress = isByAddress; if (!isByAddress) { - if (objExpr.Type.IsEnum) + if (objExpr.Type.IsEnum && !objExpr.Type.IsByRefLike()) EmitStoreAndLoadLocalVariableAddress(il, objExpr.Type); il.Demit(OpCodes.Ldfld, field); } @@ -7816,6 +7820,33 @@ internal static bool IsFloatingPoint(this Type type) => type == typeof(float) || type == typeof(double); + /// Checks if the type is a ref struct (byref-like type). + /// Compatible with older frameworks that don't have Type.IsByRefLike property. + [MethodImpl((MethodImplOptions)256)] + [RequiresUnreferencedCode(Trimming.Message)] + internal static bool IsByRefLike(this Type type) + { +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER || NET6_0_OR_GREATER + return type.IsByRefLike; +#else + // For older frameworks (net472, netstandard2.0), check for IsByRefLikeAttribute via reflection + // IsByRefLike property was introduced in .NET Standard 2.1 / .NET Core 2.1 + if (!type.IsValueType) + return false; + + // Check if the type has IsByRefLikeAttribute + var attributes = type.GetCustomAttributes(inherit: false); + foreach (var attr in attributes) + { + var attrType = attr.GetType(); + if (attrType.Name == "IsByRefLikeAttribute" || + attrType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute") + return true; + } + return false; +#endif + } + internal static bool IsPrimitiveWithZeroDefaultExceptDecimal(this Type type) { switch (Type.GetTypeCode(type)) diff --git a/test/FastExpressionCompiler.IssueTests/Issue490_ref_struct_conditional.cs b/test/FastExpressionCompiler.IssueTests/Issue490_ref_struct_conditional.cs new file mode 100644 index 00000000..5c2b7af3 --- /dev/null +++ b/test/FastExpressionCompiler.IssueTests/Issue490_ref_struct_conditional.cs @@ -0,0 +1,82 @@ +using System; + +#if LIGHT_EXPRESSION +using static FastExpressionCompiler.LightExpression.Expression; +namespace FastExpressionCompiler.LightExpression.IssueTests; +#else +using System.Linq.Expressions; +using static System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.IssueTests; +#endif + +public struct Issue490_ref_struct_conditional : ITest, ITestX +{ + public int Run() + { + Return_true_when_token_is_null(); + Return_false_when_token_isnot_null(); + return 1; + } + + public void Run(TestRun t) + { + Return_true_when_token_is_null(t); + Return_false_when_token_isnot_null(t); + } + + public delegate bool RefStructReaderDelegate(ref MyJsonReader reader); + + public enum MyJsonTokenType : byte + { + None = 0, + StartObject = 1, + Null = 11 + } + + public ref struct MyJsonReader + { + public MyJsonTokenType TokenType { get; set; } + } + + public void Return_true_when_token_is_null(TestContext t = default) + { + var readerParam = Parameter(typeof(MyJsonReader).MakeByRefType(), "reader"); + var tokenType = Property(readerParam, nameof(MyJsonReader.TokenType)); + var nullToken = Constant(MyJsonTokenType.Null); + + var body = Condition( + Equal(tokenType, nullToken), + Constant(true), + Constant(false)); + + var lambda = Lambda(body, readerParam); + var func = lambda.CompileFast(); + + var reader = new MyJsonReader(); + reader.TokenType = MyJsonTokenType.Null; + + var result = func(ref reader); + t.AreEqual(true, result); + } + + public void Return_false_when_token_isnot_null(TestContext t = default) + { + var readerParam = Parameter(typeof(MyJsonReader).MakeByRefType(), "reader"); + var tokenType = Property(readerParam, nameof(MyJsonReader.TokenType)); + var nullToken = Constant(MyJsonTokenType.Null); + + var body = Condition( + Equal(tokenType, nullToken), + Constant(true), + Constant(false)); + + var lambda = Lambda(body, readerParam); + var func = lambda.CompileFast(); + + var reader = new MyJsonReader(); + reader.TokenType = MyJsonTokenType.StartObject; + + var result = func(ref reader); + t.AreEqual(false, result); + } +} \ No newline at end of file diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 614d728b..acd49ea2 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -404,6 +404,9 @@ void Run(Func run, string name = null) Run(new Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run); Run(new LightExpression.IssueTests.Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run); + Run(new Issue490_ref_struct_conditional().Run); + Run(new LightExpression.IssueTests.Issue490_ref_struct_conditional().Run); + Console.WriteLine($"{Environment.NewLine}IssueTests are passing in {sw.ElapsedMilliseconds} ms."); }); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 45082583..4e027151 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -389,6 +389,9 @@ void Run(Func run, string name = null) Run(new Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run); Run(new LightExpression.IssueTests.Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run); + Run(new Issue490_ref_struct_conditional().Run); + Run(new LightExpression.IssueTests.Issue490_ref_struct_conditional().Run); + Console.WriteLine($"{Environment.NewLine}IssueTests are passing in {sw.ElapsedMilliseconds} ms."); });