From a9c4f6c6bbac8804227187edb2680d282f22a543 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 14 Aug 2026 07:59:43 +0200 Subject: [PATCH] Report a call as unresolvable when nothing matched at all Overload resolution reports no error for an empty candidate set - there is no best candidate to attach one to - so the null result passed for success and was dereferenced while checking the call target. Decompiling FSharp.DataFrame from nuget.org crashes that way: F# compiles its comparison members to instance methods carrying operator metadata names, and the operator candidate search looks at the operand types rather than at the receiver type the member belongs to. The new fixture pins that such an assembly decompiles at all. It still renders those instance methods as operators and drops the receiver at the call sites, which is the misclassification behind the empty candidate set and is handled separately; this is the guard that keeps an empty candidate set from being read as a resolved call. Assisted-by: Claude:claude-opus-5:Claude Code --- .../ICSharpCode.Decompiler.Tests.csproj | 5 +- .../ILPrettyTestRunner.cs | 6 ++ .../ILPretty/InstanceOperatorCall.cs | 24 +++++ .../ILPretty/InstanceOperatorCall.il | 89 +++++++++++++++++++ ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 7 ++ 5 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.il diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index c0d10da6f..6e96297d7 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -196,6 +196,9 @@ + + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index ea17c49f8..dca1cd7bc 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -333,6 +333,12 @@ namespace ICSharpCode.Decompiler.Tests await Run(); } + [Test] + public async Task InstanceOperatorCall() + { + await Run(); + } + [Test] public async Task FSharpLoops_Debug() { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.cs new file mode 100644 index 000000000..96b356467 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.cs @@ -0,0 +1,24 @@ +public class CallSite +{ + public static bool CompareVirtual(Source s, T a, T b) + { + return a < b; + } + + public static bool CompareDirect(Source s, int a, int b) + { + return a > b; + } +} +public class Source +{ + public virtual bool operator <(T a, T b) + { + return false; + } + + public bool operator >(T a, T b) + { + return false; + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.il new file mode 100644 index 000000000..edc89891e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.il @@ -0,0 +1,89 @@ +// Metadata version: v4.0.30319 +.assembly extern System.Runtime +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....: + .ver 11:0:0:0 +} +.assembly InstanceOperatorCall +{ + .ver 1:0:0:0 +} +.module InstanceOperatorCall.dll +.imagebase 0x10000000 +.file alignment 0x00000200 +.stackreserve 0x00100000 +.subsystem 0x0003 // WINDOWS_CUI +.corflags 0x00000001 // ILONLY + +// F# emits comparison operators as instance members, so operator metadata names appear on +// methods C# would never bind as operators: the operands are the arguments, and the operator +// lives on the receiver type rather than on either operand type. +.class public auto ansi beforefieldinit Source`1 + extends [System.Runtime]System.Object +{ + .method public hidebysig specialname newslot virtual + instance bool op_LessThan(!T a, + !T b) cil managed + { + .maxstack 8 + IL_0000: ldc.i4.0 + IL_0001: ret + } + + .method public hidebysig specialname + instance bool op_GreaterThan(!T a, + !T b) cil managed + { + .maxstack 8 + IL_0000: ldc.i4.0 + IL_0001: ret + } + + .method public hidebysig specialname rtspecialname + instance void .ctor() cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ret + } +} + +.class public auto ansi beforefieldinit CallSite + extends [System.Runtime]System.Object +{ + .method public hidebysig static bool CompareVirtual(class Source`1 s, + !!T a, + !!T b) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldarg.2 + IL_0003: callvirt instance bool class Source`1::op_LessThan(!0, + !0) + IL_0008: ret + } + + .method public hidebysig static bool CompareDirect(class Source`1 s, + int32 a, + int32 b) cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: ldarg.2 + IL_0003: call instance bool class Source`1::op_GreaterThan(!0, + !0) + IL_0008: ret + } + + .method public hidebysig specialname rtspecialname + instance void .ctor() cil managed + { + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: call instance void [System.Runtime]System.Object::.ctor() + IL_0006: ret + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 1efe31af6..62e7d9f66 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1643,6 +1643,13 @@ namespace ICSharpCode.Decompiler.CSharp if (or.IsAmbiguous) return OverloadResolutionErrors.AmbiguousMatch; foundMember = or.GetBestCandidateWithSubstitutedTypeArguments(); + if (foundMember == null) + { + // Overload resolution reports no error for an empty candidate set - there is no + // best candidate to carry one - so a call that matched nothing has to be reported + // as unresolvable here. + return OverloadResolutionErrors.AmbiguousMatch; + } if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMember)) return OverloadResolutionErrors.AmbiguousMatch; var map = or.GetArgumentToParameterMap();