From 961576474628142c4a7f8bfb708d17ef611ea788 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Mon, 6 Jul 2026 12:58:21 +0200 Subject: [PATCH] Add pretty tests for C# 7.3 tuple comparison operators Two fixtures cover tuple equality (#1822): TupleComparisonsExpanded is a green pin of the current element-wise expansion (evaluation-order temps, OPT vs debug shapes via #if, tuple literals decaying to plain element comparisons, mixed element types incl. string/user-defined operator/ lifted int?, Rest-based 8-element chains, single-evaluation temps for side-effecting operands, nullable-tuple null tests folding to HasValue). TupleComparisons is the Assert.Ignore'd implementation spec (#829): the desired tuple ==/!= reconstruction for variables, literals, nested and nullable tuples, element conversions, dynamic elements, and 8+ elements. All six Roslyn 3.11/4.14/latest debug/opt configs compile the ignored fixture and fail only at the output comparison. The green fixture uses roslyn3OrNewerOptions (not TupleTests' roslyn2OrNewerOptions) because Roslyn 2.10 only runs on Windows and its tuple-equality codegen could not be verified here; the ignored spec is compiler-agnostic and mirrors TupleTests. Nullable-tuple comparisons stay out of the green fixture: the current output carries HasValue machinery including a dead 'if (!hasValue) return true;' branch after 'if (!hasValue) return false;' when comparing a nullable tuple against a non-nullable operand. Dynamic elements decompile to '!(((dynamic)a == (dynamic)b) ? false : true)'; both shapes recompile but are left to the feature implementation. Assisted-by: Claude:claude-fable-5:Claude Code --- .../PrettyTestRunner.cs | 13 ++ .../TestCases/Pretty/TupleComparisons.cs | 118 +++++++++++ .../Pretty/TupleComparisonsExpanded.cs | 198 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisons.cs create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisonsExpanded.cs diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 5bb191410..3549c0fd9 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -747,6 +747,19 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task TupleComparisons([ValueSource(nameof(roslyn2OrNewerOptions))] CompilerOptions cscOptions) + { + Assert.Ignore("Tuple comparisons are not yet supported by the decompiler. See https://github.com/icsharpcode/ILSpy/issues/829 and https://github.com/icsharpcode/ILSpy/issues/1822"); + await RunForLibrary(cscOptions: cscOptions); + } + + [Test] + public async Task TupleComparisonsExpanded([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task NamedArguments([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisons.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisons.cs new file mode 100644 index 000000000..23f9709ae --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisons.cs @@ -0,0 +1,118 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + public class TupleComparisons + { + public class CustomEquality + { + public static bool operator ==(CustomEquality a, CustomEquality b) + { + return true; + } + + public static bool operator !=(CustomEquality a, CustomEquality b) + { + return false; + } + + public override bool Equals(object obj) + { + return false; + } + + public override int GetHashCode() + { + return 0; + } + } + + public bool TupleEquality((int, int) t1, (int, int) t2) + { + return t1 == t2; + } + + public bool TupleInequality((int, int) t1, (int, int) t2) + { + return t1 != t2; + } + + public bool ComparisonWithTupleLiteral((int a, int b) t1, (int a, int b) t2) + { + return t1 != t2 && t2 == (t1.a + t1.b, 0); + } + + public bool NestedTuples((int, int) ab, int c, (int, int) de, int f) + { + return (ab, c) == (de, f); + } + + public bool NullableTupleEquality((int, int)? t1, (int, int)? t2) + { + return t1 == t2; + } + + public bool NullableTupleInequality((int, int)? t1, (int, int)? t2) + { + return t1 != t2; + } + + public bool NullableTupleComparedToLiteral((int, int)? t1, int a, int b) + { + return t1 == (a, b); + } + + public bool ImplicitConversionsFromLiteral((int, long) t) + { + return t == (1, 2); + } + + public bool ImplicitElementConversions((byte a, int b) t1, (int a, long b) t2) + { + return t1 == t2; + } + + public bool StringElements((string, int) t1, (string, int) t2) + { + return t1 == t2; + } + + public bool CustomOperatorElements((CustomEquality, int) t1, (CustomEquality, int) t2) + { + return t1 == t2; + } + + public bool DynamicElements((dynamic, int) t1, (object, int) t2) + { + return t1 == t2; + } + + public bool ElementNamesDoNotMatter((int a, int b) t1, (int x, int y) t2) + { + return t1 == t2; + } + + public bool NullableElements((int?, int) t1, (int?, int) t2) + { + return t1 == t2; + } + + public bool EightElements((int, int, int, int, int, int, int, int) t1, (int, int, int, int, int, int, int, int) t2) + { + return t1 == t2; + } + + public bool OperandsEvaluatedOnlyOnce(Func<(int, int)> f1, Func<(int, int)> f2) + { + return f1() == f2(); + } + + public void UsedInCondition((int, int) t1, (int, int) t2) + { + if (t1 == t2) + { + Console.WriteLine("equal"); + } + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisonsExpanded.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisonsExpanded.cs new file mode 100644 index 000000000..e5a8e9d36 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/TupleComparisonsExpanded.cs @@ -0,0 +1,198 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + // Pins the current decompiled form of C# 7.3 tuple comparisons: the element-wise + // expansion with evaluation-order temporaries. Once the decompiler learns to + // reconstruct tuple comparisons (see TupleComparisons.cs), the EXPECTED_OUTPUT + // blocks in this file are expected to collapse to the tuple `==`/`!=` form. + public class TupleComparisonsExpanded + { + public class CustomEquality + { + public static bool operator ==(CustomEquality a, CustomEquality b) + { + return true; + } + + public static bool operator !=(CustomEquality a, CustomEquality b) + { + return false; + } + + public override bool Equals(object obj) + { + return false; + } + + public override int GetHashCode() + { + return 0; + } + } + + public bool TupleEquality((int, int) t1, (int, int) t2) + { +#if EXPECTED_OUTPUT && OPT + (int, int) tuple = t1; + (int, int) tuple2 = t2; + if (tuple.Item1 == tuple2.Item1) + { + return tuple.Item2 == tuple2.Item2; + } + return false; +#elif EXPECTED_OUTPUT + (int, int) tuple = t1; + (int, int) tuple2 = t2; + return tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2; +#else + return t1 == t2; +#endif + } + + public bool TupleInequality((int, int) t1, (int, int) t2) + { +#if EXPECTED_OUTPUT && OPT + (int, int) tuple = t1; + (int, int) tuple2 = t2; + if (tuple.Item1 == tuple2.Item1) + { + return tuple.Item2 != tuple2.Item2; + } + return true; +#elif EXPECTED_OUTPUT + (int, int) tuple = t1; + (int, int) tuple2 = t2; + return tuple.Item1 != tuple2.Item1 || tuple.Item2 != tuple2.Item2; +#else + return t1 != t2; +#endif + } + + public bool TupleLiteralsDecayToElementComparisons(int a, int b, int c, int d) + { +#if EXPECTED_OUTPUT && OPT + if (a == c) + { + return b == d; + } + return false; +#elif EXPECTED_OUTPUT + return a == c && b == d; +#else + return (a, b) == (c, d); +#endif + } + + public bool NestedTuples((int, int) ab, int c, (int, int) de, int f) + { +#if EXPECTED_OUTPUT && OPT + (int, int) tuple = ab; + (int, int) tuple2 = de; + if (tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2) + { + return c == f; + } + return false; +#elif EXPECTED_OUTPUT + (int, int) tuple = ab; + (int, int) tuple2 = de; + return tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2 && c == f; +#else + return (ab, c) == (de, f); +#endif + } + + public bool MixedElementTypes((string, CustomEquality, int?, double, float) t1, (string, CustomEquality, int?, double, float) t2) + { +#if EXPECTED_OUTPUT && OPT + (string, CustomEquality, int?, double, float) tuple = t1; + (string, CustomEquality, int?, double, float) tuple2 = t2; + if (tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2 && tuple.Item3 == tuple2.Item3 && tuple.Item4 == tuple2.Item4) + { + return tuple.Item5 == tuple2.Item5; + } + return false; +#elif EXPECTED_OUTPUT + (string, CustomEquality, int?, double, float) tuple = t1; + (string, CustomEquality, int?, double, float) tuple2 = t2; + return tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2 && tuple.Item3 == tuple2.Item3 && tuple.Item4 == tuple2.Item4 && tuple.Item5 == tuple2.Item5; +#else + return t1 == t2; +#endif + } + + public bool EightElements((int, int, int, int, int, int, int, int) t1, (int, int, int, int, int, int, int, int) t2) + { +#if EXPECTED_OUTPUT && OPT + (int, int, int, int, int, int, int, int) tuple = t1; + (int, int, int, int, int, int, int, int) tuple2 = t2; + if (tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2 && tuple.Item3 == tuple2.Item3 && tuple.Item4 == tuple2.Item4 && tuple.Item5 == tuple2.Item5 && tuple.Item6 == tuple2.Item6 && tuple.Item7 == tuple2.Item7) + { + return tuple.Rest.Item1 == tuple2.Rest.Item1; + } + return false; +#elif EXPECTED_OUTPUT + (int, int, int, int, int, int, int, int) tuple = t1; + (int, int, int, int, int, int, int, int) tuple2 = t2; + return tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2 && tuple.Item3 == tuple2.Item3 && tuple.Item4 == tuple2.Item4 && tuple.Item5 == tuple2.Item5 && tuple.Item6 == tuple2.Item6 && tuple.Item7 == tuple2.Item7 && tuple.Rest.Item1 == tuple2.Rest.Item1; +#else + return t1 == t2; +#endif + } + + public bool OperandsEvaluatedOnlyOnce(Func<(int, int, int)> f1, Func<(int, int, int)> f2) + { +#if EXPECTED_OUTPUT && OPT + (int, int, int) tuple = f1(); + (int, int, int) tuple2 = f2(); + if (tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2) + { + return tuple.Item3 == tuple2.Item3; + } + return false; +#elif EXPECTED_OUTPUT + (int, int, int) tuple = f1(); + (int, int, int) tuple2 = f2(); + return tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2 && tuple.Item3 == tuple2.Item3; +#else + return f1() == f2(); +#endif + } + + public bool NullableTupleComparedToNull((int, int)? t1) + { +#if EXPECTED_OUTPUT + return !t1.HasValue; +#else + return t1 == null; +#endif + } + + public bool NullComparedToNullableTuple((int, int)? t1) + { +#if EXPECTED_OUTPUT + return t1.HasValue; +#else + return null != t1; +#endif + } + + public void UsedInCondition((int, int) t1, (int, int) t2) + { +#if EXPECTED_OUTPUT + (int, int) tuple = t1; + (int, int) tuple2 = t2; + if (tuple.Item1 == tuple2.Item1 && tuple.Item2 == tuple2.Item2) + { + Console.WriteLine("equal"); + } +#else + if (t1 == t2) + { + Console.WriteLine("equal"); + } +#endif + } + } +}