From 85678eb6d2566a26b76cdf89ffc0fdc5f82b0df9 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 9 Aug 2026 20:01:46 +0200 Subject: [PATCH 1/2] Extend UseLambdaSyntax to emit statement lambdas Under UseLambdaSyntax, anonymous functions became lambdas only when an expression body was possible; statement-bodied ones kept C# 2 delegate syntax. Now every anonymous function whose parameter shape a lambda can express uses lambda syntax; delegate syntax remains for ref/out/in and params parameters and for pre-C# 3 language profiles. Two latent issues surfaced by the wider lambda coverage: DeclareVariables assumed an insertion point directly under a LambdaExpression is an expression body it must convert to a block, which block-bodied lambdas now violate; and anonymous methods declared without a parameter list carry compiler-generated parameter names like '' that are not valid identifiers, so the lambda's mandatory parameter list regenerates such names from the parameter type: (object obj, EventArgs e) => ... A side effect visible in fixtures: an explicit parameter list can make a delegate-creation cast redundant that bare 'delegate' syntax needed for overload resolution, e.g. new Thread((ThreadStart)delegate { }) becomes new Thread(() => { }). Assisted-by: Claude:claude-fable-5:Claude Code --- .../ILPrettyTestRunner.cs | 6 ++ .../ILPretty/AnonymousMethodEdgeCases.cs | 22 ++++++ .../ILPretty/AnonymousMethodEdgeCases.il | 79 +++++++++++++++++++ .../TestCases/ILPretty/Issue1038.cs | 2 +- .../TestCases/Pretty/Async.cs | 2 +- .../TestCases/Pretty/CustomTaskType.cs | 2 +- .../TestCases/Pretty/DeconstructionTests.cs | 2 +- .../TestCases/Pretty/DelegateConstruction.cs | 68 ++++++++++------ .../TestCases/Pretty/ExpressionTrees.cs | 6 +- .../TestCases/Pretty/FixProxyCalls.cs | 6 +- .../TestCases/Pretty/InitializerTests.cs | 6 +- .../TestCases/Pretty/Issue3439.cs | 4 +- .../TestCases/Pretty/Issue3751.cs | 2 +- .../TestCases/Pretty/LocalFunctions.cs | 26 +++--- .../TestCases/Pretty/OutVariables.cs | 2 +- .../TestCases/Pretty/PropertiesAndEvents.cs | 6 +- .../TestCases/Pretty/QualifierTests.cs | 2 +- .../TestCases/Pretty/TupleTests.cs | 6 +- ...eScalarReplacementOfAggregates.Expected.cs | 2 +- .../CSharp/ExpressionBuilder.cs | 5 +- .../CSharp/Transforms/DeclareVariables.cs | 15 +++- .../IL/Transforms/AssignVariableNames.cs | 8 ++ 22 files changed, 216 insertions(+), 63 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.il diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index f0d8afb67a..117f54bef3 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -51,6 +51,12 @@ public void AllFilesHaveTests() } } + [Test] + public async Task AnonymousMethodEdgeCases() + { + await Run(); + } + [Test, Ignore("Need to decide how to represent virtual methods without 'newslot' flag")] public async Task Issue379() { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.cs new file mode 100644 index 0000000000..84d19f5f1d --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.cs @@ -0,0 +1,22 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ + public class AnonymousMethodEdgeCases + { + public Func AssignmentIsTheLambdaBody() + { + return (int x) => { + int num; + return num = x; + }; + } + + public Action UsedParameterWithInvalidName() + { + return (object value) => { + Console.WriteLine(value); + }; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.il new file mode 100644 index 0000000000..e227e3be22 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/AnonymousMethodEdgeCases.il @@ -0,0 +1,79 @@ +// Hand-written input for two anonymous-method shapes that C# cannot express directly: +// an expression-bodied lambda whose body is the assignment declaring its own local, and +// a parameter whose metadata name is not a valid C# identifier but IS used in the body. +.assembly extern mscorlib +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) + .ver 4:0:0:0 +} +.assembly extern System.Core +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) + .ver 4:0:0:0 +} +.assembly AnonymousMethodEdgeCases +{ + .ver 1:0:0:0 +} +.module AnonymousMethodEdgeCases.dll +.imagebase 0x00400000 +.file alignment 0x00000200 +.stackreserve 0x00100000 +.subsystem 0x0003 +.corflags 0x00000001 + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.AnonymousMethodEdgeCases + extends [mscorlib]System.Object +{ + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed + { + .maxstack 8 + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } + + .method public hidebysig instance class [System.Core]System.Func`2 AssignmentIsTheLambdaBody () cil managed + { + .maxstack 8 + ldarg.0 + ldftn instance int32 ICSharpCode.Decompiler.Tests.TestCases.ILPretty.AnonymousMethodEdgeCases::'b__1_0'(int32) + newobj instance void class [System.Core]System.Func`2::.ctor(object, native int) + ret + } + + // return num = x; -- the store's value is the return value, so the local has a store and + // no load, and the decompiled lambda body is the assignment itself. + .method private hidebysig instance int32 'b__1_0' (int32 x) cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + .maxstack 2 + .locals init ( + [0] int32 num + ) + ldarg.1 + dup + stloc.0 + ret + } + + .method public hidebysig instance class [System.Core]System.Action`1 UsedParameterWithInvalidName () cil managed + { + .maxstack 8 + ldarg.0 + ldftn instance void ICSharpCode.Decompiler.Tests.TestCases.ILPretty.AnonymousMethodEdgeCases::'b__2_0'(object) + newobj instance void class [System.Core]System.Action`1::.ctor(object, native int) + ret + } + + // The parameter name '' is what C# emits for an anonymous method declared without a + // parameter list; used here, so the lambda's parameter list has to name it somehow. + .method private hidebysig instance void 'b__2_0' (object '') cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) + .maxstack 8 + ldarg.1 + call void [mscorlib]System.Console::WriteLine(object) + ret + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1038.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1038.cs index 2ce15ac0fb..83e6790281 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1038.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1038.cs @@ -4,7 +4,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty { public class Issue1038 where TR : class, new() { - public event Action TestEvent = delegate { + public event Action TestEvent = (TK A_0, TR A_1) => { }; } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs index 2c4e38545c..0623e8f571 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Async.cs @@ -524,7 +524,7 @@ public static Func> AsyncLambda() public static Func> AsyncDelegate() { - return async delegate { + return async () => { await Task.Delay(10); return 2; }; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CustomTaskType.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CustomTaskType.cs index 409263b7a9..9254eeafc0 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CustomTaskType.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CustomTaskType.cs @@ -119,7 +119,7 @@ public static Func> AsyncLambda() public static Func> AsyncDelegate() { - return async delegate { + return async () => { await Task.Delay(10); return 2; }; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs index a07a220e37..7cbf45a4ff 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DeconstructionTests.cs @@ -990,7 +990,7 @@ public async Task DeconstructionAssignmentToCapturedLocals(string file) { int a = 0; int b = 0; - await Task.Run(delegate { + await Task.Run(() => { (a, b) = GetTuple(); }); return a + b; diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs index dae612b3a7..f1a8b708e7 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs @@ -58,14 +58,14 @@ public struct SomeData public Action CaptureOfThis() { - return delegate { + return () => { CaptureOfThis(); }; } public Action CaptureOfThisAndParameter(int a) { - return delegate { + return () => { CaptureOfThisAndParameter(a); }; } @@ -76,7 +76,7 @@ public Action CaptureOfThisAndParameterInForEach(int a) { if (item > 0) { - return delegate { + return () => { CaptureOfThisAndParameter(item + a); }; } @@ -91,7 +91,7 @@ public Action CaptureOfThisAndParameterInForEachWithItemCopy(int a) int copyOfItem = item; if (item > 0) { - return delegate { + return () => { CaptureOfThisAndParameter(item + a + copyOfItem); }; } @@ -118,18 +118,18 @@ public void Bar(Func f) private void Bug955() { - new Thread((ThreadStart)delegate { + new Thread(() => { }); } public void Bug951(int amount) { - DoAction(delegate { + DoAction(() => { if (amount < 0) { amount = 0; } - DoAction(delegate { + DoAction(() => { NoOp(amount); }); }); @@ -138,12 +138,12 @@ public void Bug951(int amount) public void Bug951b() { int amount = Foo(); - DoAction(delegate { + DoAction(() => { if (amount < 0) { amount = 0; } - DoAction(delegate { + DoAction(() => { NoOp(amount); }); }); @@ -151,8 +151,8 @@ public void Bug951b() public void Bug951c(SomeData data) { - DoAction(delegate { - DoAction(delegate { + DoAction(() => { + DoAction(() => { DoSomething(data.Value); }); }); @@ -165,7 +165,7 @@ public Func Issue2143() public Action Bug971_DelegateWithoutParameterList() { - return delegate { + return (object obj) => { }; } @@ -256,7 +256,7 @@ public class GenericTest public Func GetFunc(Func f) { TCaptured captured = f(default(TNonCaptured)); - return delegate { + return () => { Console.WriteLine(captured.GetType().FullName); return captured; }; @@ -265,7 +265,7 @@ public Func GetFunc(Func f) public Func GetFunc(Func f) { TCaptured captured = f(); - return delegate (TNonCaptured a, TNonCapturedMP d) { + return (TNonCaptured a, TNonCapturedMP d) => { Console.WriteLine(a.GetHashCode()); Console.WriteLine(captured.GetType().FullName); return captured; @@ -343,7 +343,7 @@ public static List> AnonymousMethodStoreWithinLoop() for (int i = 0; i < 10; i++) { int counter; - list.Add(delegate (int x) { + list.Add((int x) => { counter = x; }); } @@ -356,7 +356,7 @@ public static List> AnonymousMethodStoreOutsideLoop() int counter; for (int i = 0; i < 10; i++) { - list.Add(delegate (int x) { + list.Add((int x) => { counter = x; }); } @@ -365,7 +365,7 @@ public static List> AnonymousMethodStoreOutsideLoop() public static Action StaticAnonymousMethodNoClosure() { - return delegate { + return () => { Console.WriteLine(); }; } @@ -383,7 +383,7 @@ public static void NameConflict() int j; for (j = 0; j < 10; j++) { - list.Add(delegate (int k) { + list.Add((int k) => { for (int l = 0; l < j; l += k) { Console.WriteLine(); @@ -398,7 +398,7 @@ public static void NameConflict2(int j) List> list = new List>(); for (int i = 0; i < 10; i++) { - list.Add(delegate (int k) { + list.Add((int k) => { Console.WriteLine(k); }); } @@ -406,7 +406,7 @@ public static void NameConflict2(int j) public static Action NameConflict3(int i) { - return delegate (int j) { + return (int j) => { for (int k = 0; k < j; k++) { Console.WriteLine(k); @@ -427,7 +427,7 @@ public static Func>> CurriedAddition2(int a) public static Func CapturedTypeParameter1(TNonCaptured a, Func f) { TCaptured captured = f(a); - return delegate { + return () => { Console.WriteLine(captured.GetType().FullName); return captured; }; @@ -436,7 +436,7 @@ public static Func CapturedTypeParameter1(TN public static Func CapturedTypeParameter2(TNonCaptured a, Func> f) { List captured = f(a); - return delegate { + return () => { Console.WriteLine(captured.GetType().FullName); return captured.FirstOrDefault(); }; @@ -624,14 +624,14 @@ internal class Issue2791 { public void M() { - Run(delegate (object o) { + Run((object o) => { try { List list = o as List; - Action action = delegate { + Action action = () => { list.Select((int x) => x * 2); }; - Action action2 = delegate { + Action action2 = () => { list.Select((int x) => x * 2); }; Console.WriteLine(); @@ -655,6 +655,20 @@ private void Run(ParameterizedThreadStart del, object x) del(x); } + public void AnonymousMethodWithByRefParameters() + { + RefAction refAction = delegate (ref int reference) { + reference++; + }; + OutAction outAction = delegate (out int reference) { + reference = 1; + }; + int value = 0; + refAction(ref value); + outAction(out value); + Console.WriteLine(value); + } + public void Issue1572(DelegateConstruction.Dummy dum) { #if EXPECTED_OUTPUT @@ -675,4 +689,8 @@ public void Issue1572(DelegateConstruction.Dummy dum) internal class MyAttribute : Attribute { } + + public delegate void OutAction(out int value); + + public delegate void RefAction(ref int value); } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs index 99acb21865..d2253581f0 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs @@ -818,15 +818,15 @@ public static void Call() Test>((int a) => a.ToString(), (int a) => a.ToString()); Test>((string a) => a.ToArray(), (string a) => a.ToArray()); Test>(() => 'a'.CompareTo('b') < 0, () => 'a'.CompareTo('b') < 0); - Test>(delegate (object lockObj, bool lockTaken) { + Test>((object lockObj, bool lockTaken) => { Monitor.Enter(lockObj, ref lockTaken); }, (object lockObj, bool lockTaken) => Monitor.Enter(lockObj, ref lockTaken)); Test>((string str, int num) => int.TryParse(str, out num), (string str, int num) => int.TryParse(str, out num)); Test>((string str, SimpleType t) => int.TryParse(str, out t.Field), (string str, SimpleType t) => int.TryParse(str, out t.Field)); - Test>(delegate (object o) { + Test>((object o) => { TestCall(o); }, (object o) => TestCall(o)); - Test>(delegate (object o) { + Test>((object o) => { TestCall(ref o); }, (object o) => TestCall(ref o)); } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FixProxyCalls.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FixProxyCalls.cs index 2ad2039ca5..a8bbaa5f42 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FixProxyCalls.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FixProxyCalls.cs @@ -96,7 +96,7 @@ internal class H : G protected internal override void Test(string test) { - action = delegate (string a) { + action = (string a) => { base.Test(a); }; if (test.Equals(1)) @@ -119,7 +119,7 @@ public class Issue1660 : Issue1660Base { public Action M(object state) { - return delegate (object x) { + return (object x) => { base.BaseCall(x, state, () => (object)null); }; } @@ -136,7 +136,7 @@ internal class J : I { protected internal override void Test(int a) { - Action action = delegate { + Action action = () => { base.Test(a); }; if (a.Equals(1)) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs index c59c83add3..5afcdb5a21 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/InitializerTests.cs @@ -916,7 +916,11 @@ public static void NotAnObjectInitializer() public static void NotAnObjectInitializerWithEvent() { Data data = new Data(); - data.TestEvent += delegate { +#if NET50 + data.TestEvent += (object? obj, EventArgs e) => { +#else + data.TestEvent += (object obj, EventArgs e) => { +#endif Console.WriteLine(); }; X(Y(), data); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3439.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3439.cs index e6c3512e6f..0fc84fdabc 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3439.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3439.cs @@ -12,7 +12,7 @@ private class Item private void Test(List list1) { - AddAction(delegate (List list2) { + AddAction((List list2) => { long num2 = 1L; foreach (string item in list1) { @@ -28,7 +28,7 @@ private void Test(List list1) { int preservedName = num; num++; - AddAction(item2, delegate (object x) { + AddAction(item2, (object x) => { SetValue(x, preservedName); }); } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs index 536f859fec..eeb51fa1aa 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue3751.cs @@ -13,7 +13,7 @@ private static T Infer(Func factory) public object Trigger() { - return Infer(delegate { + return Infer(() => { if (Cond) { Console.WriteLine(); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs index e12420b356..838c529fdc 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs @@ -329,7 +329,7 @@ static void Method() private int field; - private Lazy nonCapturinglocalFunctionInLambda = new Lazy(delegate { + private Lazy nonCapturinglocalFunctionInLambda = new Lazy(() => { return CreateValue(); #if CS80 @@ -342,7 +342,7 @@ object CreateValue() } }); - private Lazy capturinglocalFunctionInLambda = new Lazy(delegate { + private Lazy capturinglocalFunctionInLambda = new Lazy(() => { int x = 42; return Do(); @@ -648,7 +648,7 @@ int B() public static int LocalFunctionInLambda(IEnumerable xs) { - return xs.First(delegate (int x) { + return xs.First((int x) => { return Do(); bool Do() @@ -797,20 +797,20 @@ int ZZZ() { t0 = 0; int t2 = t0; - return ((Func)delegate { + return ((Func)(() => { t0 = 0; t2 = 0; return ZZZ2(); - })(); + }))(); } int ZZZ2() { t0 = 0; int t3 = t0; #if !OPT - Func func = delegate { + Func func = () => { #else - return ((Func)delegate { + return ((Func)(() => { #endif t0 = 0; t3 = 0; @@ -819,7 +819,7 @@ int ZZZ2() }; return func(); #else - })(); + }))(); #endif } } @@ -840,20 +840,20 @@ int ZZZ() { t0 = 0; int t2 = t0; - return ((Func)delegate { + return ((Func)(() => { t0 = 0; t2 = 0; return ZZZ2(); - })(); + }))(); } int ZZZ2() { t0 = 0; int t3 = t0; #if !OPT - Func func = delegate { + Func func = () => { #else - return ((Func)delegate { + return ((Func)(() => { #endif t0 = 0; t3 = 0; @@ -862,7 +862,7 @@ int ZZZ2() }; return func(); #else - })(); + }))(); #endif } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs index a7cfc0d428..37a621916e 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs @@ -37,7 +37,7 @@ public static Action CapturedOutVarInShortCircuit(Dictionary d) // to ensure that the value is initialized when the delegate is declared. if (d.Count > 2 && d.TryGetValue(42, out var value)) { - return delegate { + return () => { Console.WriteLine(value); }; } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PropertiesAndEvents.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PropertiesAndEvents.cs index 0206dade0c..36bebd6291 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PropertiesAndEvents.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/PropertiesAndEvents.cs @@ -169,7 +169,11 @@ public int NotAnAutoProperty { public event EventHandler AutomaticEvent; [field: NonSerialized] - public event EventHandler AutomaticEventWithInitializer = delegate { +#if NET50 + public event EventHandler AutomaticEventWithInitializer = (object? obj, EventArgs e) => { +#else + public event EventHandler AutomaticEventWithInitializer = (object obj, EventArgs e) => { +#endif }; #if ROSLYN diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs index 57d0dee5fd..2f5eb6f5fd 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/QualifierTests.cs @@ -280,7 +280,7 @@ private void Capturing() { int fieldConflict = 5; Capturer(() => this.fieldConflict + fieldConflict); - Capturer(delegate { + Capturer(() => { int innerConflict = 5; return this.fieldConflict + fieldConflict + Capturer2(() => this.innerConflict + innerConflict + this.fieldConflict + fieldConflict); }); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleTests.cs index 3599b10055..5c028cf00b 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleTests.cs @@ -100,14 +100,14 @@ public struct GenericStruct public (int, int) AccessRest => (1, 2, 3, 4, 5, 6, 7, 8, 9).Rest; - public (string, object, Action) TargetTyping => (null, 1, delegate { + public (string, object, Action) TargetTyping => (null, 1, () => { #pragma warning disable format }); #pragma warning restore format - public object NotTargetTyping => ((string)null, (object)1, (Action)delegate { + public object NotTargetTyping => ((string)null, (object)1, (Action)(() => { #pragma warning disable format - }); + })); #pragma warning restore format public void UnnamedTupleOut(out (int, string, Action, dynamic) tuple) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/AggressiveScalarReplacementOfAggregates.Expected.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/AggressiveScalarReplacementOfAggregates.Expected.cs index b94c00d522..e022498b31 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/AggressiveScalarReplacementOfAggregates.Expected.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/AggressiveScalarReplacementOfAggregates.Expected.cs @@ -207,7 +207,7 @@ public void Test13(Program other) field1 = 1, field2 = "Hello World!" }; - Invoke(delegate { + Invoke(() => { displayClass.thisField = new Program(); }); Console.WriteLine("{0} {1}", this, displayClass.thisField); diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 6d1e9ef7ab..9547f4f744 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2550,8 +2550,9 @@ internal ExpressionWithResolveResult TranslateFunction(IType delegateType, ILFun } else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ReferenceKind.None && !p.IsParams)) { - // otherwise use lambda only if an expression lambda is possible - isLambda = (body.Statements.Count == 1 && body.Statements.Single() is ReturnStatement); + // Lambdas cover statement bodies too; anonymous method syntax remains only for + // parameter shapes a lambda cannot express (ref/out/in and params modifiers). + isLambda = true; } // Remove the parameter list from an AnonymousMethodExpression if the parameters are not used in the method body var parameterReferencingIdentifiers = diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index fe3c2e70eb..f6411f4940 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -472,8 +472,11 @@ void ResolveCollisions() { // We can only insert variable declarations in blocks, but FindInsertionPoints() didn't // guarantee that it finds only blocks. - // Fix that up now. - while (!(v.InsertionPoint.nextNode.Parent is BlockStatement or LambdaExpression)) + // Fix that up now. A lambda is a valid stop only for its expression body (insertion + // will convert that body to a block); a point at a statement-bodied lambda's block + // itself must keep moving up into the enclosing scope. + while (!(v.InsertionPoint.nextNode.Parent is BlockStatement + || (v.InsertionPoint.nextNode.Parent is LambdaExpression && v.InsertionPoint.nextNode is Expression))) { if (v.InsertionPoint.nextNode.Parent is ForStatement f && v.InsertionPoint.nextNode == f.Initializers.FirstOrDefault() && IsMatchingAssignment(v, out _)) { @@ -593,6 +596,14 @@ bool CombineDeclarationAndInitializer(VariableToDeclare v, TransformContext cont if (v.Type.IsByRefLike) return true; // by-ref-like variables always must be initialized at their declaration. + if (v.InsertionPoint.nextNode.Parent is LambdaExpression) + { + // The insertion point is an expression-bodied lambda's body. Combining would put a + // declaration statement in expression position ("x => int num = x;"); the separate + // declaration path turns the body into a block first, which stays valid C#. + return false; + } + if (v.InsertionPoint.nextNode.Slot?.Kind == Slots.ForInitializer) return true; // for-statement initializers always should combine declaration and initialization. diff --git a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs index 7aba498bce..06acdff08b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/AssignVariableNames.cs @@ -234,6 +234,14 @@ static bool IsSetOrEventAccessor(IMethod method) if (variables.TryGetValue(i, out var v)) variableMapping[v] = name; } + else if (!IsValidName(name)) + { + // Compiler-generated parameter names (e.g. "" on an anonymous method + // declared without a parameter list) are not valid C# identifiers. Skipping + // the reservation and the mapping leaves the parameter to AssignName, which + // generates a fresh name from the type for any name that fails IsValidName. + continue; + } string nameWithoutNumber = SplitName(name, out int newIndex); if (!parentScope.IsReservedVariableName(nameWithoutNumber, out _)) { From a22b5450885a7dbdfd87c767547872e3185d365a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 10 Aug 2026 07:05:16 +0200 Subject: [PATCH 2/2] Keep 'delegate {}' when a parameter type is inaccessible The parameter-list-less anonymous method form is compatible with any delegate signature, and C# code must rely on exactly that when a delegate's parameter types cannot be named at the use site: IL, unlike C#, permits a delegate signature to reference less accessible types. Expanding such an anonymous method into a lambda would force the unnameable type into a parameter list. Keep the delegate form, with its parameter list dropped, when the parameters are unused and one of their types is not accessible from the current context. Assisted-by: Claude:claude-fable-5:Claude Code --- .../ICSharpCode.Decompiler.Tests.csproj | 3 + .../ILPrettyTestRunner.cs | 6 + .../ILPretty/InaccessibleParameterTypes.cs | 31 ++++ .../ILPretty/InaccessibleParameterTypes.il | 138 ++++++++++++++++++ .../TestCases/Pretty/DelegateConstruction.cs | 8 +- .../CSharp/ExpressionBuilder.cs | 65 +++++++-- 6 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.il diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 29015d86ae..f112879f55 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -105,6 +105,7 @@ + @@ -209,6 +210,8 @@ + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 117f54bef3..ea17c49f82 100644 --- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs @@ -359,6 +359,12 @@ public async Task GuessAccessors() await Run(); } + [Test] + public async Task InaccessibleParameterTypes() + { + await Run(); + } + [Test] public async Task EmptyBodies() { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.cs new file mode 100644 index 0000000000..e1dcd80b3e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.cs @@ -0,0 +1,31 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty +{ + public class InaccessibleParameterTypes + { + private class Hidden + { + } + + public delegate void Handler(Hidden h); + + public static void Register(Action callback) + { + } + } + public class InaccessibleParameterTypesConsumer + { + public InaccessibleParameterTypes.Handler Create() + { + return delegate { + }; + } + + public void Run() + { + InaccessibleParameterTypes.Register(delegate { + }); + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.il new file mode 100644 index 0000000000..968ca42645 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InaccessibleParameterTypes.il @@ -0,0 +1,138 @@ +// Anonymous methods whose delegate signature contains a type the use site cannot name. +// IL (unlike C#) permits a public delegate with a less-accessible parameter type, so the +// parameter-list-less "delegate {}" form is the only C# syntax the consumer class below +// could legally have used - the decompiler must not expand it to a lambda parameter list. +.assembly extern mscorlib +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. + .ver 4:0:0:0 +} +.assembly extern System.Core +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. + .ver 4:0:0:0 +} +.assembly InaccessibleParameterTypes +{ + .ver 1:0:0:0 +} +.module InaccessibleParameterTypes.dll +.imagebase 0x00400000 +.file alignment 0x00000200 +.stackreserve 0x00100000 +.subsystem 0x0003 // WINDOWS_CUI +.corflags 0x00020003 // ILONLY 32BITPREFERRED + +.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes + extends [mscorlib]System.Object +{ + .class nested private auto ansi beforefieldinit Hidden + extends [mscorlib]System.Object + { + .method public hidebysig specialname rtspecialname instance void .ctor () cil managed + { + .maxstack 8 + + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } + } + + .class nested public auto ansi sealed Handler + extends [mscorlib]System.MulticastDelegate + { + .method public hidebysig specialname rtspecialname instance void .ctor (object 'object', native int 'method') runtime managed + { + } + + .method public hidebysig newslot virtual instance void Invoke (class ICSharpCode.Decompiler.Tests.TestCases.ILPretty.InaccessibleParameterTypes/Hidden h) runtime managed + { + } + } + + .method public hidebysig static void Register (class [System.Core]System.Action`1