From decfd36a3faee663d4bac0179dead256e855c331 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 05:18:26 +0000 Subject: [PATCH 1/2] Initial plan From ffd609d1c0ee54125e1127b6621f11e5ca3150fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:43:11 +0000 Subject: [PATCH 2/2] Fix IndexOutOfRangeException with implicit conversion operator from base/abstract class param Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/82c35acc-2d27-4de0-9359-3eb4a7da07c8 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com> --- .../FastExpressionCompiler.cs | 9 +- ...with_value_objects_implicit_conversions.cs | 124 ++++++++++++++++++ .../Program.cs | 3 + 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index f3d986fb..0a7a17e4 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -3205,10 +3205,13 @@ private static bool TryEmitConvert(UnaryExpression expr, ILGenerator il, ref Com EmitStoreAndLoadLocalVariableAddress(il, sourceType); EmitMethodCall(il, sourceType.GetNullableValueGetterMethod()); } - else if (methodParamType != sourceType) // This is an unlikely case of Target(Source? source) + else if (methodParamType != sourceType) // This is an unlikely case of Target(Source? source), or a polymorphic base type { - Debug.Assert(Nullable.GetUnderlyingType(methodParamType) == sourceType, "Expecting that the parameter type is the Nullable"); - il.Demit(OpCodes.Newobj, methodParamType.GetNullableConstructor()); + if (Nullable.GetUnderlyingType(methodParamType) == sourceType) + il.Demit(OpCodes.Newobj, methodParamType.GetNullableConstructor()); + // else: methodParamType is a base type/interface of sourceType - the value on the stack is already assignment-compatible, no additional emit needed + else Debug.Assert(methodParamType.IsAssignableFrom(sourceType), + $"Expected the conversion operator parameter type `{methodParamType}` to be either Nullable<{sourceType}> or a base type of `{sourceType}`"); } EmitMethodCallOrVirtualCall(il, convertMethod); diff --git a/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs b/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs new file mode 100644 index 00000000..903e08a5 --- /dev/null +++ b/test/FastExpressionCompiler.IssueTests/Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions.cs @@ -0,0 +1,124 @@ +using System; +using System.Reflection; + +#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 class Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions : ITest +{ + public int Run() + { + Implicit_conv_op_via_abstract_base_class_param_in_closure(); + Implicit_conv_op_via_non_abstract_base_class_param_in_closure(); + Implicit_conv_op_via_abstract_base_class_param_directly(); + return 3; + } + + public abstract class PrimitiveValueObject + { + public TInput Value { get; protected set; } = default!; + + public static implicit operator TInput(PrimitiveValueObject? primitiveValueObject) => + primitiveValueObject == null ? default! : primitiveValueObject.Value; + } + + public class MyPrimitive : PrimitiveValueObject + { + public MyPrimitive(string val) => Value = val; + } + + // Reproduces the original issue: implicit conversion operator on an abstract base class + // used within a closure-captured variable wrapped in Convert(..., typeof(object)) + public void Implicit_conv_op_via_abstract_base_class_param_in_closure() + { + var captured = new MyPrimitive("Hello world"); + + // Expression with implicit conversion on closure-captured variable + System.Linq.Expressions.Expression> sysExpr = () => captured; +#if LIGHT_EXPRESSION + var innerExpr = sysExpr.FromSysExpression(); +#else + var innerExpr = sysExpr; +#endif + + // Wrap in Convert to object (common pattern in LINQ providers) + var toObject = Convert(innerExpr.Body, typeof(object)); + var lambda = Lambda>(toObject); + + lambda.PrintCSharp(); + + var fs = lambda.CompileSys(); + Asserts.AreEqual("Hello world", (string)fs()); + + var ff = lambda.CompileFast(true); + Asserts.AreEqual("Hello world", (string)ff()); + } + + public class NonAbstractBase + { + public TInput Value { get; protected set; } = default!; + + public static implicit operator TInput(NonAbstractBase? obj) => + obj == null ? default! : obj.Value; + } + + public class DerivedPrimitive : NonAbstractBase + { + public DerivedPrimitive(string val) => Value = val; + } + + // Same case but with a non-abstract base class (previously caused InvalidProgramException) + public void Implicit_conv_op_via_non_abstract_base_class_param_in_closure() + { + var captured = new DerivedPrimitive("Hello world"); + + System.Linq.Expressions.Expression> sysExpr = () => captured; +#if LIGHT_EXPRESSION + var innerExpr = sysExpr.FromSysExpression(); +#else + var innerExpr = sysExpr; +#endif + + var toObject = Convert(innerExpr.Body, typeof(object)); + var lambda = Lambda>(toObject); + + lambda.PrintCSharp(); + + var fs = lambda.CompileSys(); + Asserts.AreEqual("Hello world", (string)fs()); + + var ff = lambda.CompileFast(true); + Asserts.AreEqual("Hello world", (string)ff()); + } + + // Same conversion but with a direct (non-closure) parameter; the method must be passed explicitly + // because the derived class does not directly declare the static op_Implicit (it is on the base class). + public void Implicit_conv_op_via_abstract_base_class_param_directly() + { + var param = Parameter(typeof(MyPrimitive), "p"); + + // Get the op_Implicit method declared on the abstract base class + var opImplicit = typeof(PrimitiveValueObject) + .GetMethod("op_Implicit", BindingFlags.Public | BindingFlags.Static); + Asserts.IsNotNull(opImplicit); + + // Convert MyPrimitive -> string via op_Implicit (which takes PrimitiveValueObject) + // then Convert string -> object + var toObject = Convert(Convert(param, typeof(string), opImplicit), typeof(object)); + var lambda = Lambda>(toObject, param); + + lambda.PrintCSharp(); + + var fs = lambda.CompileSys(); + Asserts.AreEqual("Hello world", (string)fs(new MyPrimitive("Hello world"))); + + var ff = lambda.CompileFast(true); + Asserts.AreEqual("Hello world", (string)ff(new MyPrimitive("Hello world"))); + } +} diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index fff2cf82..7eefa4a1 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -429,6 +429,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 Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions().Run); + Run(new LightExpression.IssueTests.Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions().Run); + Console.WriteLine($"{Environment.NewLine}//IssueTests are passing in {sw.ElapsedMilliseconds} ms."); });