From 20cb82db202efe2e53fc1ece508434ba2f895d57 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 6 Jul 2026 12:26:58 +0200 Subject: [PATCH] Add pretty tests for C# 10 improved definite assignment The decompiler's stake in the C# 10 "improved definite assignment" rules (dotnet/csharplang#4465) is the mirror image of the compiler feature: decompiled output must stay provably assigned when recompiled, and ideally should not need dummy initializers where the C# 10 rules prove assignment. OutVariableFlows (green) pins that out variables flowing through short-circuit operators, conditional access, ternaries, while/for conditions, exception filters, closures and generic/struct out parameters decompile to recompilable code in all four Roslyn 4 debug/opt configs. Probing these flow shapes found no CS0165-broken output: the decompiler either rewrites conditional access into explicit null-check chains or falls back to a dummy initializer. The lifted-negation shape '(!(x)) ?? true' documents a trap: the C# 10 rules do not cover it, so its dummy initializer is required. ImprovedDefiniteAssignment (Assert.Ignore'd, #829) is the desired output spec for the shapes where the decompiler keeps the conditional access and currently hoists the out variable with a 'default(T)' initializer that the C# 10 rules make redundant: '?? false' on int, generic and struct out parameters, and '== true' on chained conditional access in if and while conditions. All four configs compile the fixture and fail only at the output comparison; the EXPECTED_OUTPUT branches carry IL-equivalent inputs where the desired form lowers to different IL. Assisted-by: Claude:claude-fable-5:Claude Code --- .../PrettyTestRunner.cs | 13 + .../Pretty/ImprovedDefiniteAssignment.cs | 129 +++++++++ .../TestCases/Pretty/OutVariableFlows.cs | 259 ++++++++++++++++++ 3 files changed, 401 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/ImprovedDefiniteAssignment.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariableFlows.cs diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 5bb191410..88d56b786 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -482,6 +482,19 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task OutVariableFlows([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + + [Test] + public async Task ImprovedDefiniteAssignment([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions) + { + Assert.Ignore("Improved definite assignment (C# 10) is not yet supported by the decompiler. See https://github.com/icsharpcode/ILSpy/issues/829"); + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task PatternMatching([ValueSource(nameof(roslyn2OrNewerOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ImprovedDefiniteAssignment.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ImprovedDefiniteAssignment.cs new file mode 100644 index 000000000..b3a07fe8c --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ImprovedDefiniteAssignment.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + // Desired output for out variables whose assignment is only provable via the C# 10 + // "improved definite assignment" rules (https://github.com/dotnet/csharplang/issues/4465): + // a conditional-access invocation compared with a bool constant ('== true') or coalesced + // with 'false' guarantees assignment on the true branch, so no dummy initializer and no + // hoisted declaration are needed. The decompiler currently emits + // 'T value = default(T);' before each of these statements and passes 'out value' instead + // of declaring the variable inline. The EXPECTED_OUTPUT branches exist where the input + // must use a different (but IL-equivalent) expression form to produce the nullable-lifted + // IL that keeps the conditional access in the decompiled output. + public class ImprovedDefiniteAssignment + { + public class Container + { + private readonly Dictionary map = new Dictionary(); + + public bool TryGet(string key, out int value) + { + return map.TryGetValue(key, out value); + } + } + + public class GenericSource + { + public bool TryGet(string key, out T value) + { + value = default(T); + return key != null; + } + } + + public struct StructValue + { + public int A; + + public string B; + } + + public class Provider + { + public bool TryGetStruct(out StructValue value) + { + value = new StructValue { + A = 1, + B = "x" + }; + return true; + } + } + + public class Wrapper + { + public Container Inner; + + public Dictionary Data; + } + + public static int CoalesceOutVar(Container c, string key) + { +#if EXPECTED_OUTPUT + if (c?.TryGet(key, out var value) ?? false) +#else + if (c?.TryGet(key, out var value) is true) +#endif + { + return value; + } + return -1; + } + + public static T GenericCoalesceOutVar(GenericSource s, string key) + { +#if EXPECTED_OUTPUT + if (s?.TryGet(key, out var value) ?? false) +#else + if (s?.TryGet(key, out var value) is true) +#endif + { + return value; + } + return default(T); + } + + public static string StructCoalesceOutVar(Provider p) + { +#if EXPECTED_OUTPUT + if (p?.TryGetStruct(out var value) ?? false) +#else + if (p?.TryGetStruct(out var value) is true) +#endif + { + return value.B + value.A; + } + return null; + } + + public static int ChainedConditionalOutVar(Wrapper w, string key) + { +#if EXPECTED_OUTPUT + if (w != null && w.Inner?.TryGet(key, out var value) == true) +#else + if (w?.Inner?.TryGet(key, out var value) == true) +#endif + { + return value; + } + return -1; + } + + public static int WhileChainedOutVar(Wrapper w) + { + int num = 0; + int num2 = 0; +#if EXPECTED_OUTPUT + while (w != null && w.Data?.TryGetValue(num2, out var value) == true) +#else + while (w?.Data?.TryGetValue(num2, out var value) == true) +#endif + { + num += value; + num2++; + } + return num; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariableFlows.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariableFlows.cs new file mode 100644 index 000000000..f178d2cdd --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariableFlows.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + // Pins that out variables flowing through short-circuit operators, conditional access, + // loops and exception filters decompile to recompilable code. The C# 10 "improved + // definite assignment" rules (https://github.com/dotnet/csharplang/issues/4465) made + // several of these flow shapes legal without dummy initializers; the decompiler must + // never produce output where an out variable's assignment is unprovable (CS0165). + public class OutVariableFlows + { + public class Container + { + private readonly Dictionary map = new Dictionary(); + + public bool TryGet(string key, out int value) + { + return map.TryGetValue(key, out value); + } + } + + public class GenericSource + { + public bool TryGet(string key, out T value) + { + value = default(T); + return key != null; + } + } + + public struct StructValue + { + public int A; + + public string B; + } + + public class Provider + { + public bool TryGetStruct(out StructValue value) + { + value = new StructValue { + A = 1, + B = "x" + }; + return true; + } + } + + public static int GuardedUse(Container c, string key) + { + if (c != null && c.TryGet(key, out var value)) + { + return value; + } + return -1; + } + + public static int NegatedGuardEarlyReturn(Container c, string key) + { + if (c == null || !c.TryGet(key, out var value)) + { + return -1; + } + return value; + } + + public static int TernaryUse(Container c, string key) + { +#if OPT + if (c == null || !c.TryGet(key, out var value)) + { + return -1; + } + return value; +#else + int value; + return (c != null && c.TryGet(key, out value)) ? value : (-1); +#endif + } + + public static int WhileCondition(Container c) + { + int num = 0; + int num2 = 0; + int value; + while (c.TryGet(num2.ToString(), out value)) + { + num += value; + num2++; + } + return num; + } + + public static int ForCondition(Dictionary d) + { + int num = 0; + int value; + for (int i = 0; d.TryGetValue(i, out value); i++) + { + if (value <= 0) + { + break; + } + num += value; + } + return num; + } + + public static int NestedShortCircuit(Container a, Container b, string key) + { + if (a.TryGet(key, out var value) && b.TryGet(key, out var value2)) + { + return value + value2; + } + return 0; + } + + public static int MixedAndOr(Container a, Container b, string key) + { + if ((a != null && a.TryGet(key, out var value)) || (b != null && b.TryGet(key, out value))) + { + return value; + } + return -1; + } + + public static string StructGuardedUse(Provider p) + { + if (p != null && p.TryGetStruct(out var value)) + { + return value.B + value.A; + } + return null; + } + + public static int TryParseChain(string s1, string s2) + { + if (int.TryParse(s1, out var result) && int.TryParse(s2, out var result2)) + { + return result + result2; + } +#if OPT + if (!int.TryParse(s1, out var result3)) + { + return 0; + } + return result3; +#else + int result3; + return int.TryParse(s1, out result3) ? result3 : 0; +#endif + } + + public static string CoalesceAssignTernary(Dictionary d, string key) + { + string text = null; + if (text == null) + { + text = (d.TryGetValue(key, out var value) ? value : "missing"); + } + return text; + } + + public static int LiftedNotCoalesce(Container c, string key) + { + // The dummy initializer is required in the decompiled output: with this expression + // shape the C# 10 improved definite assignment rules do not cover '(!(x)) ?? true', + // so dropping the initializer would make this method uncompilable (CS0165). + // The input uses 'is not true' because recompiling the lifted-negation form does + // not reproduce the nullable-lifted IL this output shape comes from. +#if EXPECTED_OUTPUT + int value = default(int); + if ((!(c?.TryGet(key, out value))) ?? true) +#else + if (c?.TryGet(key, out var value) is not true) +#endif + { + return -1; + } + return value; + } + + public static int CatchFilter(Action a) + { + int result; + try + { + a(); + return 0; + } + catch (Exception ex) when (int.TryParse(ex.Message, out result)) + { + return result; + } + } + + public static int TryCatchEarlyReturn(string s) + { + try + { + if (!int.TryParse(s, out var result)) + { + return -1; + } + return result; + } + catch (Exception) + { + return -2; + } + } + + public static T GenericGuardedUse(GenericSource s, string key) + { + if (s != null && s.TryGet(key, out var value)) + { + return value; + } + return default(T); + } + + public static int IsPatternCombined(object o, string key) + { + if (o is Container container && container.TryGet(key, out var value)) + { + return value; + } + return -1; + } + + public static Func CapturedInClosure(Container c, string key) + { + if (c != null && c.TryGet(key, out var value)) + { + return () => value; + } + return null; + } + + public static Func LambdaConditional(Dictionary d, int key) + { +#if OPT +#if EXPECTED_OUTPUT + return () => (d != null && d.TryGetValue(key, out var value)) ? value : (-1); +#else + return () => (d == null || !d.TryGetValue(key, out var value)) ? (-1) : value; +#endif +#else + return delegate { + Dictionary dictionary = d; + int value; + return (dictionary != null && dictionary.TryGetValue(key, out value)) ? value : (-1); + }; +#endif + } + } +}