From ac8a8ff64de89636b1b772da84280ca87d0ef2dc Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 14 Aug 2026 19:12:02 +0200 Subject: [PATCH] Add an ILPretty test for #3729 Structs whose assembly is missing decompile through the unresolved-type path, which is not covered anywhere: the fixture pins the constructor shapes that path has to recognize, and the reference-type cases that must keep falling through to a plain call. Assisted-by: Claude:claude-opus-5:Claude Code --- .../ICSharpCode.Decompiler.Tests.csproj | 3 + .../ILPrettyTestRunner.cs | 6 + .../TestCases/ILPretty/Issue3729.cs | 80 ++++++ .../TestCases/ILPretty/Issue3729.il | 228 ++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index c0d10da6f..79116b15f 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -202,6 +202,9 @@ + + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index ea17c49f8..281b12ee4 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -405,6 +405,12 @@ namespace ICSharpCode.Decompiler.Tests await Run(settings: new DecompilerSettings { SortSwitchSections = true, FileScopedNamespaces = false }); } + [Test] + public async Task Issue3729() + { + await Run(); + } + async Task Run([CallerMemberName] string testName = null, DecompilerSettings settings = null, AssemblerOptions assemblerOptions = AssemblerOptions.Library) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs new file mode 100644 index 000000000..4ac1684c3 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.CompilerServices; +using Library1; + +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ + public class Issue3729 + { + private MyStruct structField; + + public void TestSingleArgCtor() + { + //IL_0003: Unknown result type (might be due to invalid IL or missing references) + Console.WriteLine("Test new struct: " + ((object)new MyStruct(4)/*cast due to constrained. prefix*/).ToString()); + } + + public void TestParameterlessCtor() + { + //IL_0002: Unknown result type (might be due to invalid IL or missing references) + //IL_0007: Unknown result type (might be due to invalid IL or missing references) + MyEmptyStruct val = new MyEmptyStruct(); + Console.WriteLine(val); + } + + public void TestMultiArgCtor() + { + //IL_0011: Unknown result type (might be due to invalid IL or missing references) + //IL_0016: Unknown result type (might be due to invalid IL or missing references) + MyBigStruct val = new MyBigStruct(1, "hello", 3.14); + Console.WriteLine(val); + } + + public void TestFieldCtor() + { + //IL_0007: Unknown result type (might be due to invalid IL or missing references) + ref MyStruct reference = ref structField; + reference = new MyStruct(5); + } + + public unsafe static void TestPointerCtor(void* ptr) + { + //IL_0002: Unknown result type (might be due to invalid IL or missing references) + System.Runtime.CompilerServices.Unsafe.Write(ptr, new MyStruct(6)); + } + + public void TestArrayElemCtor() + { + //IL_000f: Unknown result type (might be due to invalid IL or missing references) + MyStruct[] array = (MyStruct[])(object)new MyStruct[1] { + new MyStruct(7) + }; + } + + public void TestGenericStructCtor() + { + //IL_0003: Unknown result type (might be due to invalid IL or missing references) + //IL_0008: Unknown result type (might be due to invalid IL or missing references) + MyGenericStruct val = new MyGenericStruct(4); + Console.WriteLine(val); + } + + public void TestRefTypeNewobj() + { + //IL_0000: Unknown result type (might be due to invalid IL or missing references) + //IL_0006: Expected O, but got Unknown + MyClass value = new MyClass(); + Console.WriteLine(value); + } + } + public class Issue3729_DerivedFromUnknown : MissingBase + { + } + public class Issue3729_DerivedFromUnknownWithArgs : MissingBase + { + public Issue3729_DerivedFromUnknownWithArgs() + : base(42) + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il new file mode 100644 index 000000000..f468655e6 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue3729.il @@ -0,0 +1,228 @@ +// Reproducer for issue #3729: +// Decompilation of a struct constructor call where the struct's defining +// assembly (Library1) cannot be resolved at decompile time. +// Mirrors what csc emits for `new Library1.MyStruct(4)` followed by +// string concat with the struct value. + +.assembly extern Library1 +{ +} +.assembly extern System.Runtime +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) + .ver 4:0:0:0 +} +.assembly extern System.Console +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) + .ver 4:0:0:0 +} +.assembly Issue3729 +{ + .ver 1:0:0:0 +} +.module Issue3729.dll + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.Issue3729 + extends [System.Runtime]System.Object +{ + .field private valuetype [Library1]Library1.MyStruct structField + + // single-int-arg struct ctor with subsequent constrained.callvirt ToString() + .method public hidebysig + instance void TestSingleArgCtor () cil managed + { + .maxstack 2 + .locals init ( + [0] valuetype [Library1]Library1.MyStruct + ) + + IL_0000: ldloca.s 0 + IL_0002: ldc.i4.4 + IL_0003: call instance void [Library1]Library1.MyStruct::.ctor(int32) + IL_0008: ldstr "Test new struct: " + IL_000d: ldloca.s 0 + IL_000f: constrained. [Library1]Library1.MyStruct + IL_0015: callvirt instance string [System.Runtime]System.Object::ToString() + IL_001a: call string [System.Runtime]System.String::Concat(string, string) + IL_001f: call void [System.Console]System.Console::WriteLine(string) + IL_0024: ret + } + + // explicit parameterless struct ctor (C# 10+ feature in source) + // Single-argument ctor: the receiver sits directly below the argument. + .method public hidebysig + instance void TestParameterlessCtor () cil managed + { + .maxstack 1 + .locals init ( + [0] valuetype [Library1]Library1.MyEmptyStruct + ) + + IL_0000: ldloca.s 0 + IL_0002: call instance void [Library1]Library1.MyEmptyStruct::.ctor() + IL_0007: ldloc.0 + IL_0008: box [Library1]Library1.MyEmptyStruct + IL_000d: call void [System.Console]System.Console::WriteLine(object) + IL_0012: ret + } + + // Multi-argument ctor with mixed parameter types: the receiver sits below three arguments + .method public hidebysig + instance void TestMultiArgCtor () cil managed + { + .maxstack 5 + .locals init ( + [0] valuetype [Library1]Library1.MyBigStruct + ) + + IL_0000: ldloca.s 0 + IL_0002: ldc.i4.1 + IL_0003: ldstr "hello" + IL_0008: ldc.r8 3.14 + IL_0011: call instance void [Library1]Library1.MyBigStruct::.ctor(int32, string, float64) + IL_0016: ldloc.0 + IL_0017: box [Library1]Library1.MyBigStruct + IL_001c: call void [System.Console]System.Console::WriteLine(object) + IL_0021: ret + } + + // struct field init via `ldflda + call .ctor`. Receiver is the field + // address (StackType.Ref) - same shape as `ldloca`, just sourced from + // an instance field instead of a local. + .method public hidebysig + instance void TestFieldCtor () cil managed + { + .maxstack 2 + + IL_0000: ldarg.0 + IL_0001: ldflda valuetype [Library1]Library1.MyStruct ICSharpCode.Decompiler.Tests.TestCases.ILPretty.Issue3729::structField + IL_0006: ldc.i4.5 + IL_0007: call instance void [Library1]Library1.MyStruct::.ctor(int32) + IL_000c: ret + } + + // Unsafe code passes the target as a pointer, so the receiver is a native + // pointer (StackType.I) rather than a managed reference. + .method public hidebysig static + void TestPointerCtor ( + void* ptr + ) cil managed + { + .maxstack 2 + + IL_0000: ldarg.0 + IL_0001: ldc.i4.6 + IL_0002: call instance void [Library1]Library1.MyStruct::.ctor(int32) + IL_0007: ret + } + + // struct array element init via `ldelema + call .ctor`. Receiver is the + // element address (StackType.Ref) - again the same shape. + .method public hidebysig + instance void TestArrayElemCtor () cil managed + { + .maxstack 4 + .locals init ( + [0] valuetype [Library1]Library1.MyStruct[] + ) + + IL_0000: ldc.i4.1 + IL_0001: newarr [Library1]Library1.MyStruct + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: ldc.i4.0 + IL_0009: ldelema [Library1]Library1.MyStruct + IL_000e: ldc.i4.7 + IL_000f: call instance void [Library1]Library1.MyStruct::.ctor(int32) + IL_0014: ret + } + + // generic struct instantiation `new MyGenericStruct(4)`. Declaring + // type at the call site is a ParameterizedType wrapping the unresolved + // generic def - its Kind delegates to the underlying UnknownType.Kind, + // so a constructed generic type is unresolved in the same way. + .method public hidebysig + instance void TestGenericStructCtor () cil managed + { + .maxstack 2 + .locals init ( + [0] valuetype [Library1]Library1.MyGenericStruct`1 + ) + + IL_0000: ldloca.s 0 + IL_0002: ldc.i4.4 + IL_0003: call instance void valuetype [Library1]Library1.MyGenericStruct`1::.ctor(!0) + IL_0008: ldloc.0 + IL_0009: box valuetype [Library1]Library1.MyGenericStruct`1 + IL_000e: call void [System.Console]System.Console::WriteLine(object) + IL_0013: ret + } + + // newobj on an unresolved reference type - uses OpCode.NewObj, a different + // IL path that already produces correct C# (HandleConstructorCall in CallBuilder). + // Included as a control case for the value-type ctor handling above. + .method public hidebysig + instance void TestRefTypeNewobj () cil managed + { + .maxstack 1 + .locals init ( + [0] class [Library1]Library1.MyClass + ) + + IL_0000: newobj instance void [Library1]Library1.MyClass::.ctor() + IL_0005: stloc.0 + IL_0006: ldloc.0 + IL_0007: call void [System.Console]System.Console::WriteLine(object) + IL_000c: 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 + } +} + +// Classes deriving from an unresolved base class. The synthesized .ctor +// calls `[Library1]Library1.MissingBase::.ctor(...)` with `this` +// (StackType.O) as the receiver - exercises the StackType.Ref guard on +// a reference-typed receiver, which has to keep falling through to the +// default call path so the C# decompiler can render it as a normal +// `: base(...)` constructor initializer. +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.Issue3729_DerivedFromUnknown + extends [Library1]Library1.MissingBase +{ + // parameterless base ctor - the implicit ctor would normally be elided. + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + .maxstack 8 + + IL_0000: ldarg.0 + IL_0001: call instance void [Library1]Library1.MissingBase::.ctor() + IL_0006: ret + } +} + +// Same as above but the base ctor takes an argument - receiver is at +// depth 1 (one parameter on the stack above it). Output should render +// the explicit `: base()` initializer. +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.Issue3729_DerivedFromUnknownWithArgs + extends [Library1]Library1.MissingBase +{ + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + .maxstack 2 + + IL_0000: ldarg.0 + IL_0001: ldc.i4.s 42 + IL_0003: call instance void [Library1]Library1.MissingBase::.ctor(int32) + IL_0008: ret + } +}