Skip to content

Commit ffd609d

Browse files
Copilotdadhi
andauthored
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>
1 parent decfd36 commit ffd609d

3 files changed

Lines changed: 133 additions & 3 deletions

File tree

src/FastExpressionCompiler/FastExpressionCompiler.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,10 +3205,13 @@ private static bool TryEmitConvert(UnaryExpression expr, ILGenerator il, ref Com
32053205
EmitStoreAndLoadLocalVariableAddress(il, sourceType);
32063206
EmitMethodCall(il, sourceType.GetNullableValueGetterMethod());
32073207
}
3208-
else if (methodParamType != sourceType) // This is an unlikely case of Target(Source? source)
3208+
else if (methodParamType != sourceType) // This is an unlikely case of Target(Source? source), or a polymorphic base type
32093209
{
3210-
Debug.Assert(Nullable.GetUnderlyingType(methodParamType) == sourceType, "Expecting that the parameter type is the Nullable<sourceType>");
3211-
il.Demit(OpCodes.Newobj, methodParamType.GetNullableConstructor());
3210+
if (Nullable.GetUnderlyingType(methodParamType) == sourceType)
3211+
il.Demit(OpCodes.Newobj, methodParamType.GetNullableConstructor());
3212+
// else: methodParamType is a base type/interface of sourceType - the value on the stack is already assignment-compatible, no additional emit needed
3213+
else Debug.Assert(methodParamType.IsAssignableFrom(sourceType),
3214+
$"Expected the conversion operator parameter type `{methodParamType}` to be either Nullable<{sourceType}> or a base type of `{sourceType}`");
32123215
}
32133216

32143217
EmitMethodCallOrVirtualCall(il, convertMethod);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System;
2+
using System.Reflection;
3+
4+
#if LIGHT_EXPRESSION
5+
using static FastExpressionCompiler.LightExpression.Expression;
6+
namespace FastExpressionCompiler.LightExpression.IssueTests;
7+
#else
8+
using System.Linq.Expressions;
9+
using static System.Linq.Expressions.Expression;
10+
namespace FastExpressionCompiler.IssueTests;
11+
#endif
12+
13+
public class Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions : ITest
14+
{
15+
public int Run()
16+
{
17+
Implicit_conv_op_via_abstract_base_class_param_in_closure();
18+
Implicit_conv_op_via_non_abstract_base_class_param_in_closure();
19+
Implicit_conv_op_via_abstract_base_class_param_directly();
20+
return 3;
21+
}
22+
23+
public abstract class PrimitiveValueObject<TInput, TOutput>
24+
{
25+
public TInput Value { get; protected set; } = default!;
26+
27+
public static implicit operator TInput(PrimitiveValueObject<TInput, TOutput>? primitiveValueObject) =>
28+
primitiveValueObject == null ? default! : primitiveValueObject.Value;
29+
}
30+
31+
public class MyPrimitive : PrimitiveValueObject<string, MyPrimitive>
32+
{
33+
public MyPrimitive(string val) => Value = val;
34+
}
35+
36+
// Reproduces the original issue: implicit conversion operator on an abstract base class
37+
// used within a closure-captured variable wrapped in Convert(..., typeof(object))
38+
public void Implicit_conv_op_via_abstract_base_class_param_in_closure()
39+
{
40+
var captured = new MyPrimitive("Hello world");
41+
42+
// Expression with implicit conversion on closure-captured variable
43+
System.Linq.Expressions.Expression<Func<string>> sysExpr = () => captured;
44+
#if LIGHT_EXPRESSION
45+
var innerExpr = sysExpr.FromSysExpression();
46+
#else
47+
var innerExpr = sysExpr;
48+
#endif
49+
50+
// Wrap in Convert to object (common pattern in LINQ providers)
51+
var toObject = Convert(innerExpr.Body, typeof(object));
52+
var lambda = Lambda<Func<object>>(toObject);
53+
54+
lambda.PrintCSharp();
55+
56+
var fs = lambda.CompileSys();
57+
Asserts.AreEqual("Hello world", (string)fs());
58+
59+
var ff = lambda.CompileFast(true);
60+
Asserts.AreEqual("Hello world", (string)ff());
61+
}
62+
63+
public class NonAbstractBase<TInput, TOutput>
64+
{
65+
public TInput Value { get; protected set; } = default!;
66+
67+
public static implicit operator TInput(NonAbstractBase<TInput, TOutput>? obj) =>
68+
obj == null ? default! : obj.Value;
69+
}
70+
71+
public class DerivedPrimitive : NonAbstractBase<string, DerivedPrimitive>
72+
{
73+
public DerivedPrimitive(string val) => Value = val;
74+
}
75+
76+
// Same case but with a non-abstract base class (previously caused InvalidProgramException)
77+
public void Implicit_conv_op_via_non_abstract_base_class_param_in_closure()
78+
{
79+
var captured = new DerivedPrimitive("Hello world");
80+
81+
System.Linq.Expressions.Expression<Func<string>> sysExpr = () => captured;
82+
#if LIGHT_EXPRESSION
83+
var innerExpr = sysExpr.FromSysExpression();
84+
#else
85+
var innerExpr = sysExpr;
86+
#endif
87+
88+
var toObject = Convert(innerExpr.Body, typeof(object));
89+
var lambda = Lambda<Func<object>>(toObject);
90+
91+
lambda.PrintCSharp();
92+
93+
var fs = lambda.CompileSys();
94+
Asserts.AreEqual("Hello world", (string)fs());
95+
96+
var ff = lambda.CompileFast(true);
97+
Asserts.AreEqual("Hello world", (string)ff());
98+
}
99+
100+
// Same conversion but with a direct (non-closure) parameter; the method must be passed explicitly
101+
// because the derived class does not directly declare the static op_Implicit (it is on the base class).
102+
public void Implicit_conv_op_via_abstract_base_class_param_directly()
103+
{
104+
var param = Parameter(typeof(MyPrimitive), "p");
105+
106+
// Get the op_Implicit method declared on the abstract base class
107+
var opImplicit = typeof(PrimitiveValueObject<string, MyPrimitive>)
108+
.GetMethod("op_Implicit", BindingFlags.Public | BindingFlags.Static);
109+
Asserts.IsNotNull(opImplicit);
110+
111+
// Convert MyPrimitive -> string via op_Implicit (which takes PrimitiveValueObject<string,MyPrimitive>)
112+
// then Convert string -> object
113+
var toObject = Convert(Convert(param, typeof(string), opImplicit), typeof(object));
114+
var lambda = Lambda<Func<MyPrimitive, object>>(toObject, param);
115+
116+
lambda.PrintCSharp();
117+
118+
var fs = lambda.CompileSys();
119+
Asserts.AreEqual("Hello world", (string)fs(new MyPrimitive("Hello world")));
120+
121+
var ff = lambda.CompileFast(true);
122+
Asserts.AreEqual("Hello world", (string)ff(new MyPrimitive("Hello world")));
123+
}
124+
}

test/FastExpressionCompiler.TestsRunner/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@ void Run(Func<int> run, string name = null)
429429
Run(new Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run);
430430
Run(new LightExpression.IssueTests.Issue461_InvalidProgramException_when_null_checking_type_by_ref().Run);
431431

432+
Run(new Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions().Run);
433+
Run(new LightExpression.IssueTests.Issue500_IndexOutOfRangeException_with_value_objects_implicit_conversions().Run);
434+
432435
Console.WriteLine($"{Environment.NewLine}//IssueTests are passing in {sw.ElapsedMilliseconds} ms.");
433436
});
434437

0 commit comments

Comments
 (0)