From 482002ebc06d9fb8db4e6dfb8d4f8f9899b3ea9b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 6 Jul 2026 11:30:37 +0200 Subject: [PATCH] Add pretty tests for C# 14 simple lambda parameters with modifiers The C# 14 feature is pure source sugar: a lambda parameter may carry ref/out/in/scoped/ref readonly without declaring a type, but the IL always has fully-typed parameters, so the decompiler's explicitly-typed output is the correct round-trip. LambdaParameterModifiers pins that this round-trip works: custom delegate types with each modifier (including scoped by-value, scoped ref, and mixed parameter lists), block- and expression-bodied lambdas, captures alongside ref parameters, generic ref delegates, lambda parameter attributes, call-site modifiers (including 'in' for 'ref readonly' parameters), unused-parameter anonymous methods, and a local-function contrast. CS140-gated branches compile the untyped sugar (and attributed sugar) under Roslyn latest to prove it produces the same decompilation. ScopedLambdaParameters is Assert.Ignore'd: when an anonymous function's 'scoped ref' parameter is unused, the decompiler drops the parameter list, losing the required 'scoped' modifier; the output then fails to recompile with CS8986 because the implicit parameters of a parameter-list-less anonymous method are unscoped. The fixture is the desired output (parameter list retained); all four Roslyn 4.14/latest debug/opt configs compile it and fail only at the output comparison. Assisted-by: Claude:claude-fable-5:Claude Code --- .../PrettyTestRunner.cs | 13 ++ .../Pretty/LambdaParameterModifiers.cs | 157 ++++++++++++++++++ .../Pretty/ScopedLambdaParameters.cs | 19 +++ 3 files changed, 189 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaParameterModifiers.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/ScopedLambdaParameters.cs diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 5bb191410..af285a55c 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -794,6 +794,19 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task LambdaParameterModifiers([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + + [Test] + public async Task ScopedLambdaParameters([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + Assert.Ignore("Anonymous functions with an unused 'scoped ref' parameter lose the required 'scoped' modifier when the parameter list is dropped. See https://github.com/icsharpcode/ILSpy/issues/829"); + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task ExpandParamsArgumentsDisabled([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaParameterModifiers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaParameterModifiers.cs new file mode 100644 index 000000000..c68dab27f --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LambdaParameterModifiers.cs @@ -0,0 +1,157 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class LambdaParameterModifiers + { + public delegate void RefAction(ref int x); + + public delegate bool TryParseDelegate(string s, out int result); + + public delegate int InFunc(in DateTime d); + + public delegate int RefReadonlyFunc(ref readonly int x); + + public delegate void RefGenericAction(ref T item); + + public delegate int ScopedSpanFunc(scoped Span s); + + public delegate Span ScopedMixedFunc(scoped ref int x, ref int y); + + public delegate void ScopedRefAction(scoped ref int x); + + private int instanceField; + + public RefAction RefBlockBody() + { + return delegate (ref int x) { + x++; + }; + } + + public RefAction RefExpressionBody() + { +#if EXPECTED_OUTPUT + return delegate (ref int x) { + x *= 2; + }; +#elif CS140 + // C# 14 simple lambda parameter with modifiers: no type needed + return (ref x) => x *= 2; +#else + return (ref int x) => x *= 2; +#endif + } + + public TryParseDelegate OutLambda() + { +#if EXPECTED_OUTPUT + return delegate (string s, out int result) { + return int.TryParse(s, out result); + }; +#elif CS140 + // C# 14 simple lambda parameter with modifiers: no type needed + return (s, out result) => int.TryParse(s, out result); +#else + return (string s, out int result) => int.TryParse(s, out result); +#endif + } + + public InFunc InLambda() + { + return delegate (in DateTime d) { + return d.Year; + }; + } + + public RefReadonlyFunc RefReadonlyLambda() + { + return delegate (ref readonly int x) { + return x + 1; + }; + } + + public RefGenericAction GenericRefLambda() + { + return delegate (ref string item) { + item += "!"; + }; + } + + public RefAction CaptureAlongsideRefParameter(int offset) + { + return delegate (ref int x) { + x += offset + instanceField; + }; + } + + public ScopedSpanFunc ScopedSpanLambda() + { + return (scoped Span s) => s.Length; + } + + public ScopedMixedFunc ScopedMixedLambda() + { + return delegate (scoped ref int x, ref int y) { + x += y; + return default(Span); + }; + } + + public ScopedRefAction ScopedRefLambda() + { + return delegate (scoped ref int x) { + x++; + }; + } + + public RefAction AttributedRefLambda() + { +#if EXPECTED_OUTPUT + return ([ParamMod] ref int x) => { + x += 10; + }; +#elif CS140 + // C# 14 also allows attributes on simple lambda parameters + return ([ParamMod] ref x) => x += 10; +#else + return ([ParamMod] ref int x) => x += 10; +#endif + } + + public RefAction UnusedRefParameter() + { + return delegate { + }; + } + + public RefAction LocalFunctionWithRef() + { + return LocalRef; + static void LocalRef(ref int x) + { + x -= 3; + } + } + + public int InvokeAll() + { + int x = 1; + RefBlockBody()(ref x); + RefExpressionBody()(ref x); + OutLambda()("42", out var result); + DateTime d = DateTime.MinValue; + int num = InLambda()(in d) + RefReadonlyLambda()(in x); + string item = "a"; + GenericRefLambda()(ref item); + CaptureAlongsideRefParameter(2)(ref x); + LocalFunctionWithRef()(ref x); + return x + result + num + item.Length; + } + } + + [AttributeUsage(AttributeTargets.Parameter)] + internal class ParamModAttribute : Attribute + { + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ScopedLambdaParameters.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ScopedLambdaParameters.cs new file mode 100644 index 000000000..dedc97b42 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ScopedLambdaParameters.cs @@ -0,0 +1,19 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class ScopedLambdaParameters + { + public delegate Span ScopedRefFunc(scoped ref int x); + + public ScopedRefFunc UnusedScopedRefParameter() + { + // The parameter list must not be dropped here: an anonymous function + // without a parameter list has unscoped implicit parameters, which do + // not match the delegate's "scoped ref" parameter (CS8986). + return delegate (scoped ref int x) { + return default(Span); + }; + } + } +}