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
This commit is contained in:
Siegfried Pammer
2026-07-29 07:19:30 +02:00
parent d7bbf17e2e
commit 9615764746
3 changed files with 329 additions and 0 deletions
@@ -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)
{
@@ -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");
}
}
}
}
@@ -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
}
}
}