diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs
index a00fc4425..0e898a8f5 100644
--- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs
@@ -145,6 +145,12 @@ namespace ICSharpCode.Decompiler.Tests
CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest,
}, executesCompiledOutput: true);
+ static readonly CompilerOptions[] roslyn5OrNewerOptions = Tester.SupportedOnCurrentPlatform(new[]
+ {
+ CompilerOptions.UseRoslynLatest,
+ CompilerOptions.Optimize | CompilerOptions.UseRoslynLatest,
+ });
+
static readonly CompilerOptions[] roslyn2OrNewerOptions = Tester.SupportedOnCurrentPlatform(new[]
{
CompilerOptions.UseRoslyn2_10_0 | CompilerOptions.TargetNet40,
@@ -211,6 +217,18 @@ namespace ICSharpCode.Decompiler.Tests
await RunCS(options: options);
}
+ [Test]
+ public async Task UserDefinedCompoundAssignment([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions options)
+ {
+ await RunCS(options: options);
+ }
+
+ [Test]
+ public async Task UserDefinedCompoundAssignmentInheritance([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions options)
+ {
+ await RunCS(options: options);
+ }
+
[Test]
public async Task PropertiesAndEvents([ValueSource(nameof(defaultOptions))] CompilerOptions options)
{
diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index af25b8827..ca125f4bb 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -209,6 +209,9 @@
+
+
+
diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
index 7e9f8df94..44e1d8e8a 100644
--- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
@@ -345,6 +345,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
+ [Test]
+ public async Task CompoundAssignmentOperatorEdgeCases()
+ {
+ await Run();
+ }
+
[Test]
public async Task FSharpLoops_Debug()
{
diff --git a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs
index 1c7db3227..f39402e68 100644
--- a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs
+++ b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs
@@ -59,7 +59,8 @@ namespace ICSharpCode.Decompiler.Tests.Output
return foundType;
}
- const ConversionFlags ILSpyMainTreeViewTypeFlags = ShowTypeParameterList | PlaceReturnTypeAfterParameterList;
+ const ConversionFlags ILSpyMainTreeViewTypeFlags = ShowTypeParameterList | PlaceReturnTypeAfterParameterList
+ | SupportOperatorChecked | SupportUnsignedRightShift | SupportUserDefinedCompoundAssignmentOperators;
const ConversionFlags ILSpyMainTreeViewMemberFlags = ILSpyMainTreeViewTypeFlags | ShowParameterList | ShowReturnType | ShowParameterModifiers;
#region ITypeDefinition tests
@@ -311,6 +312,41 @@ namespace ICSharpCode.Decompiler.Tests.Output
Assert.That(ambience.ConvertSymbol(prop), Is.EqualTo(expectedOutput));
}
+ const ConversionFlags TooltipFlags = All & ~(ShowBody | PlaceReturnTypeAfterParameterList);
+
+ [TestCase(StandardConversionFlags, "public void operator op_AdditionAssignment(int rhs);")]
+ [TestCase(StandardConversionFlags | SupportUserDefinedCompoundAssignmentOperators, "public void operator +=(int rhs);")]
+ [TestCase(ILSpyMainTreeViewMemberFlags, "operator +=(int) : void")]
+ [TestCase(TooltipFlags, "public void ICSharpCode.Decompiler.Tests.Output.CSharpAmbienceTests.Program.operator +=(int rhs)")]
+ public void CompoundAssignmentOperatorTests(ConversionFlags flags, string expectedOutput)
+ {
+ var op = compilation.FindType(typeof(CSharpAmbienceTests.Program))
+ .GetMethods(m => m.Name == "op_AdditionAssignment").Single();
+ ambience.ConversionFlags = flags;
+
+ Assert.That(ambience.ConvertSymbol(op), Is.EqualTo(expectedOutput));
+ }
+
+ [TestCase(ILSpyMainTreeViewMemberFlags, "operator checked +=(int) : void")]
+ public void CheckedCompoundAssignmentOperatorTests(ConversionFlags flags, string expectedOutput)
+ {
+ var op = compilation.FindType(typeof(CSharpAmbienceTests.Program))
+ .GetMethods(m => m.Name == "op_CheckedAdditionAssignment").Single();
+ ambience.ConversionFlags = flags;
+
+ Assert.That(ambience.ConvertSymbol(op), Is.EqualTo(expectedOutput));
+ }
+
+ [TestCase(ILSpyMainTreeViewMemberFlags, "operator ++() : void")]
+ public void InstanceIncrementOperatorTests(ConversionFlags flags, string expectedOutput)
+ {
+ var op = compilation.FindType(typeof(CSharpAmbienceTests.Program))
+ .GetMethods(m => m.Name == "op_IncrementAssignment").Single();
+ ambience.ConversionFlags = flags;
+
+ Assert.That(ambience.ConvertSymbol(op), Is.EqualTo(expectedOutput));
+ }
+
[TestCase(StandardConversionFlags, "~Program();")]
[TestCase(ILSpyMainTreeViewMemberFlags, "~Program()")]
public void DestructorTests(ConversionFlags flags, string expectedOutput)
@@ -441,6 +477,21 @@ namespace ICSharpCode.Decompiler.Tests.Output
throw new NotImplementedException();
}
+ public void operator +=(int rhs)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void operator checked +=(int rhs)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void operator ++()
+ {
+ throw new NotImplementedException();
+ }
+
public static implicit operator Test(Program lhs)
{
throw new NotImplementedException();
diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
index 01b6228c9..45afed5cb 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -836,6 +836,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
+ [Test]
+ public async Task UserDefinedCompoundAssignment([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
+ {
+ await RunForLibrary(cscOptions: cscOptions);
+ }
+
[Test]
public async Task ExpandParamsArgumentsDisabled([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignment.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignment.cs
new file mode 100644
index 000000000..f906572e2
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignment.cs
@@ -0,0 +1,329 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+
+using System;
+using System.Collections.Generic;
+
+namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
+{
+ class UserDefinedCompoundAssignment
+ {
+ static void Main()
+ {
+ InstanceOperator();
+ InheritedOperator();
+ ShadowedOperator();
+ StaticOperatorStaysStatic();
+ StaticOperatorOnRefLocalStaysStatic();
+ StaticTypeSelectsTheOperator();
+ IncrementDependsOnWhetherTheResultIsUsed();
+ StructOperator();
+ GenericOperator();
+ ForeachTarget();
+ UsingTarget();
+ InParameterTarget(new Base { Value = 40 });
+ ReadonlyFieldTarget();
+ }
+
+ class Base
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine("Base.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+
+ public void operator ++()
+ {
+ Console.WriteLine("Base.op_IncrementAssignment()");
+ Value++;
+ }
+ }
+
+ class Derived : Base
+ {
+ }
+
+ // Declares the same signature as Base, so "s += n" must bind here and not to Base.
+ class Shadowing : Base
+ {
+ public new void operator +=(int rhs)
+ {
+ Console.WriteLine("Shadowing.op_AdditionAssignment({0})", rhs);
+ Value += rhs * 100;
+ }
+ }
+
+ // Declaring both forms means "b = b + n" and "b += n" do different things: the static
+ // operator produces a new instance, the instance one mutates the receiver.
+ class Both
+ {
+ public int Value;
+
+ public static Both operator +(Both lhs, int rhs)
+ {
+ Console.WriteLine("Both.op_Addition({0})", rhs);
+ return new Both { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine("Both.op_AdditionAssignment({0})", rhs);
+ Value += rhs * 1000;
+ }
+
+ public static Both operator ++(Both x)
+ {
+ Console.WriteLine("Both.op_Increment()");
+ return new Both { Value = x.Value + 1 };
+ }
+
+ public void operator ++()
+ {
+ Console.WriteLine("Both.op_IncrementAssignment()");
+ Value += 1000;
+ }
+ }
+
+ // Only the derived type declares an instance operator. Which operator "x += y" binds to
+ // therefore depends on the declared type of x, not on the runtime type of the object.
+ class StaticBase
+ {
+ public int Value;
+
+ public static StaticBase operator +(StaticBase lhs, int rhs)
+ {
+ Console.WriteLine("StaticBase.op_Addition({0})", rhs);
+ return new StaticBase { Value = lhs.Value + rhs };
+ }
+ }
+
+ class InstanceDerived : StaticBase
+ {
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine("InstanceDerived.op_AdditionAssignment({0})", rhs);
+ Value += rhs * 10;
+ }
+ }
+
+ struct Counter
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine("Counter.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ interface ICompound
+ {
+ void operator +=(int rhs);
+ }
+
+ class ViaInterface : ICompound
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine("ViaInterface.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ static void InstanceOperator()
+ {
+ Base b = new Base { Value = 1 };
+ b += 2;
+ b++;
+ Console.WriteLine("InstanceOperator: " + b.Value);
+ }
+
+ static void InheritedOperator()
+ {
+ Derived d = new Derived { Value = 1 };
+ d += 2;
+ d++;
+ Console.WriteLine("InheritedOperator: " + d.Value);
+ }
+
+ static void StaticOperatorOnRefLocalStaysStatic()
+ {
+ Both[] arr = new Both[1] { new Both { Value = 1 } };
+ ref Both r = ref arr[0];
+ r = r + 5;
+ Console.WriteLine("StaticOperatorOnRefLocalStaysStatic: " + arr[0].Value);
+ }
+
+ static void ShadowedOperator()
+ {
+ Shadowing s = new Shadowing { Value = 1 };
+ s += 2;
+ Console.WriteLine("ShadowedOperator: " + s.Value);
+ }
+
+ static void StaticOperatorStaysStatic()
+ {
+ Both b = new Both { Value = 1 };
+ Both original = b;
+ b = b + 2;
+ Console.WriteLine("StaticOperatorStaysStatic: {0} {1} {2}", b.Value, original.Value, ReferenceEquals(b, original));
+
+ b += 2;
+ Console.WriteLine("InstanceOperatorMutatesInPlace: " + b.Value);
+ }
+
+ static void StaticTypeSelectsTheOperator()
+ {
+ StaticBase asBase = new InstanceDerived { Value = 1 };
+ asBase += 2;
+ Console.WriteLine("AsBase: " + asBase.Value);
+
+ InstanceDerived asDerived = new InstanceDerived { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine("AsDerived: " + asDerived.Value);
+ }
+
+ // A postfix increment whose result is used cannot call an operator that mutates in place,
+ // so it takes the static one even though an instance operator is declared. Discarding the
+ // result, or writing the prefix form, reaches the instance operator instead.
+ static void IncrementDependsOnWhetherTheResultIsUsed()
+ {
+ Both a = new Both { Value = 1 };
+ Both old = a++;
+ Console.WriteLine("PostfixResultUsed: {0} {1} {2}", a.Value, old.Value, ReferenceEquals(a, old));
+
+ Both b = new Both { Value = 1 };
+ b++;
+ Console.WriteLine("PostfixDiscarded: " + b.Value);
+
+ Both c = new Both { Value = 1 };
+ Both n = ++c;
+ Console.WriteLine("PrefixResultUsed: {0} {1} {2}", c.Value, n.Value, ReferenceEquals(c, n));
+ }
+
+ static void StructOperator()
+ {
+ Counter c = default(Counter);
+ c += 3;
+ c += 4;
+ Console.WriteLine("StructOperator: " + c.Value);
+ }
+
+ static void UseGeneric(T x, int n) where T : ICompound
+ {
+ x += n;
+ }
+
+ static void GenericOperator()
+ {
+ ViaInterface v = new ViaInterface { Value = 1 };
+ UseGeneric(v, 5);
+ Console.WriteLine("GenericOperator: " + v.Value);
+ }
+
+ sealed class DisposableHolder : IDisposable
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine("DisposableHolder.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+
+ public void Dispose()
+ {
+ Console.WriteLine("Dispose({0})", Value);
+ }
+ }
+
+ static readonly Base readonlyField = new Base { Value = 10 };
+ readonly Base readonlyInstanceField;
+
+ static UserDefinedCompoundAssignment()
+ {
+ // A readonly field is a variable inside the matching constructor, so the
+ // operator form is legal here and has to stay.
+ readonlyField += 1;
+ }
+
+ UserDefinedCompoundAssignment()
+ {
+ readonlyInstanceField = new Base { Value = 20 };
+ readonlyInstanceField += 2;
+ }
+
+ static void ForeachTarget()
+ {
+ // The iteration variable is read-only, so the operator can only be applied
+ // to a copy; the copy has to survive decompilation.
+ List list = new List { new Base(), new Base() };
+ foreach (Base b in list)
+ {
+ Base copy = b;
+ copy += 3;
+ }
+ foreach (Base b in list)
+ {
+ Console.WriteLine("ForeachTarget: {0}", b.Value);
+ }
+ Base[] array = { new Base { Value = 30 } };
+ foreach (Base b in array)
+ {
+ Base copy = b;
+ copy += 4;
+ }
+ Console.WriteLine("ForeachTargetArray: {0}", array[0].Value);
+ }
+
+ static void UsingTarget()
+ {
+ // Same for a using variable.
+ using (DisposableHolder d = new DisposableHolder())
+ {
+ DisposableHolder copy = d;
+ copy += 5;
+ }
+ }
+
+ static void InParameterTarget(in Base b)
+ {
+ // And for an "in" parameter.
+ Base copy = b;
+ copy += 6;
+ Console.WriteLine("InParameterTarget: {0}", b.Value);
+ }
+
+ static void ReadonlyFieldTarget()
+ {
+ // Outside the constructor the readonly field is not a variable, so the
+ // copy has to survive here as well.
+ Base copy = readonlyField;
+ copy += 7;
+ Console.WriteLine("ReadonlyFieldTarget: {0}", readonlyField.Value);
+ Console.WriteLine("ReadonlyInstanceField: {0}", new UserDefinedCompoundAssignment().readonlyInstanceField.Value);
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignmentInheritance.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignmentInheritance.cs
new file mode 100644
index 000000000..ae7239c0e
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/UserDefinedCompoundAssignmentInheritance.cs
@@ -0,0 +1,681 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+
+using System;
+
+namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
+{
+ // Every combination of where a static "operator +" and an instance "operator +=" are
+ // declared across a two-level hierarchy, used through both a base-typed and a
+ // derived-typed variable. The class name suffix says what each level declares:
+ // N nothing, S static only, I instance only, X both - so Base_IS declares an instance
+ // operator and its derived class declares a static one.
+ //
+ // Which operator runs depends on the declared type of the left operand, not on the
+ // runtime type, and an instance operator reachable from that type wins over any static
+ // one. "x = x + n" therefore has to survive decompilation as-is wherever both forms
+ // exist: folding it to "x += n" would call a different operator.
+ class UserDefinedCompoundAssignmentInheritance
+ {
+ static void Main()
+ {
+ Combo_NS();
+ Combo_NI();
+ Combo_NX();
+ Combo_SN();
+ Combo_SS();
+ Combo_SI();
+ Combo_SX();
+ Combo_IN();
+ Combo_IS();
+ Combo_II();
+ Combo_IX();
+ Combo_XN();
+ Combo_XS();
+ Combo_XI();
+ Combo_XX();
+ Combo_BaseStaticReturnsDerived();
+ Combo_InstanceSignatureDoesNotApply();
+ Combo_DerivedOverloadHidesBase();
+ }
+
+ static void Combo_NS()
+ {
+ Console.WriteLine("NS derived-typed +=:");
+ Derived_NS asDerived = new Derived_NS { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_NI()
+ {
+ Console.WriteLine("NI derived-typed +=:");
+ Derived_NI asDerived = new Derived_NI { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_NX()
+ {
+ Console.WriteLine("NX derived-typed +=:");
+ Derived_NX asDerived = new Derived_NX { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("NX derived-typed x = x + n:");
+ Derived_NX sDerived = new Derived_NX { Value = 1 };
+ sDerived = sDerived + 4;
+ Console.WriteLine(" -> " + sDerived.Value);
+ }
+
+ static void Combo_SN()
+ {
+ Console.WriteLine("SN base-typed +=:");
+ Base_SN asBase = new Derived_SN { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ }
+
+ static void Combo_SS()
+ {
+ Console.WriteLine("SS base-typed +=:");
+ Base_SS asBase = new Derived_SS { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("SS derived-typed +=:");
+ Derived_SS asDerived = new Derived_SS { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_SI()
+ {
+ Console.WriteLine("SI base-typed +=:");
+ Base_SI asBase = new Derived_SI { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("SI derived-typed +=:");
+ Derived_SI asDerived = new Derived_SI { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_SX()
+ {
+ Console.WriteLine("SX base-typed +=:");
+ Base_SX asBase = new Derived_SX { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("SX derived-typed +=:");
+ Derived_SX asDerived = new Derived_SX { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("SX derived-typed x = x + n:");
+ Derived_SX sDerived = new Derived_SX { Value = 1 };
+ sDerived = sDerived + 4;
+ Console.WriteLine(" -> " + sDerived.Value);
+ }
+
+ // The static operator is declared on the base but returns the derived type, so
+ // "x = x + n" is expressible on a derived-typed variable. Binary "+" only ever
+ // considers static operators, but "x += n" would find the derived class's instance
+ // operator first, so the assignment has to stay spelled out.
+ static void Combo_BaseStaticReturnsDerived()
+ {
+ Console.WriteLine("base static returning derived, derived-typed x = x + n:");
+ Derived_SR asDerived = new Derived_SR { Value = 1 };
+ asDerived = asDerived + 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("base static returning derived, array element x = x + n:");
+ Derived_SR[] arr = new Derived_SR[1] { new Derived_SR { Value = 1 } };
+ arr[0] = arr[0] + 4;
+ Console.WriteLine(" -> " + arr[0].Value);
+ }
+
+ // Overload resolution removes base-type candidates when an applicable candidate exists
+ // in a more derived type: on a derived-typed target even an int argument binds the
+ // derived operator taking long. The base-typed variable is what makes the int overload
+ // reachable, so it has to survive decompilation.
+ static void Combo_DerivedOverloadHidesBase()
+ {
+ Console.WriteLine("overload base-typed +=:");
+ Derived_Overload target = new Derived_Overload { Value = 1 };
+ Base_Overload asBase = target;
+ asBase += 2;
+ Console.WriteLine(" -> " + target.Value);
+ Console.WriteLine("overload derived-typed +=:");
+ Derived_Overload asDerived = new Derived_Overload { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ // The instance operator takes an int, so it is not a candidate for a long argument
+ // and "x += n" binds the static operator: the assignment can be folded.
+ static void Combo_InstanceSignatureDoesNotApply()
+ {
+ Console.WriteLine("instance operator not applicable, long argument:");
+ MixedSignature asLocal = new MixedSignature { Value = 1 };
+ asLocal += 4L;
+ Console.WriteLine(" -> " + asLocal.Value);
+ Console.WriteLine("instance operator applicable, int argument:");
+ MixedSignature asInt = new MixedSignature { Value = 1 };
+ asInt += 4;
+ Console.WriteLine(" -> " + asInt.Value);
+ }
+
+ static void Combo_IN()
+ {
+ Console.WriteLine("IN base-typed +=:");
+ Base_IN asBase = new Derived_IN { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("IN derived-typed +=:");
+ Derived_IN asDerived = new Derived_IN { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_IS()
+ {
+ Console.WriteLine("IS base-typed +=:");
+ Base_IS asBase = new Derived_IS { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("IS derived-typed +=:");
+ Derived_IS asDerived = new Derived_IS { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("IS derived-typed x = x + n:");
+ Derived_IS sDerived = new Derived_IS { Value = 1 };
+ sDerived = sDerived + 4;
+ Console.WriteLine(" -> " + sDerived.Value);
+ }
+
+ static void Combo_II()
+ {
+ Console.WriteLine("II base-typed +=:");
+ Base_II asBase = new Derived_II { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("II derived-typed +=:");
+ Derived_II asDerived = new Derived_II { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_IX()
+ {
+ Console.WriteLine("IX base-typed +=:");
+ Base_IX asBase = new Derived_IX { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("IX derived-typed +=:");
+ Derived_IX asDerived = new Derived_IX { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("IX derived-typed x = x + n:");
+ Derived_IX sDerived = new Derived_IX { Value = 1 };
+ sDerived = sDerived + 4;
+ Console.WriteLine(" -> " + sDerived.Value);
+ }
+
+ static void Combo_XN()
+ {
+ Console.WriteLine("XN base-typed +=:");
+ Base_XN asBase = new Derived_XN { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("XN base-typed x = x + n:");
+ Base_XN sBase = new Derived_XN { Value = 1 };
+ sBase = sBase + 2;
+ Console.WriteLine(" -> " + sBase.Value);
+ Console.WriteLine("XN derived-typed +=:");
+ Derived_XN asDerived = new Derived_XN { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_XS()
+ {
+ Console.WriteLine("XS base-typed +=:");
+ Base_XS asBase = new Derived_XS { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("XS base-typed x = x + n:");
+ Base_XS sBase = new Derived_XS { Value = 1 };
+ sBase = sBase + 2;
+ Console.WriteLine(" -> " + sBase.Value);
+ Console.WriteLine("XS derived-typed +=:");
+ Derived_XS asDerived = new Derived_XS { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("XS derived-typed x = x + n:");
+ Derived_XS sDerived = new Derived_XS { Value = 1 };
+ sDerived = sDerived + 4;
+ Console.WriteLine(" -> " + sDerived.Value);
+ }
+
+ static void Combo_XI()
+ {
+ Console.WriteLine("XI base-typed +=:");
+ Base_XI asBase = new Derived_XI { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("XI base-typed x = x + n:");
+ Base_XI sBase = new Derived_XI { Value = 1 };
+ sBase = sBase + 2;
+ Console.WriteLine(" -> " + sBase.Value);
+ Console.WriteLine("XI derived-typed +=:");
+ Derived_XI asDerived = new Derived_XI { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ }
+
+ static void Combo_XX()
+ {
+ Console.WriteLine("XX base-typed +=:");
+ Base_XX asBase = new Derived_XX { Value = 1 };
+ asBase += 2;
+ Console.WriteLine(" -> " + asBase.Value);
+ Console.WriteLine("XX base-typed x = x + n:");
+ Base_XX sBase = new Derived_XX { Value = 1 };
+ sBase = sBase + 2;
+ Console.WriteLine(" -> " + sBase.Value);
+ Console.WriteLine("XX derived-typed +=:");
+ Derived_XX asDerived = new Derived_XX { Value = 1 };
+ asDerived += 4;
+ Console.WriteLine(" -> " + asDerived.Value);
+ Console.WriteLine("XX derived-typed x = x + n:");
+ Derived_XX sDerived = new Derived_XX { Value = 1 };
+ sDerived = sDerived + 4;
+ Console.WriteLine(" -> " + sDerived.Value);
+ }
+
+ class Base_NS
+ {
+ public int Value;
+ }
+
+ class Derived_NS : Base_NS
+ {
+ public static Derived_NS operator +(Derived_NS lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_NS.op_Addition({0})", rhs);
+ return new Derived_NS { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Base_NI
+ {
+ public int Value;
+ }
+
+ class Derived_NI : Base_NI
+ {
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_NI.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_NX
+ {
+ public int Value;
+ }
+
+ class Derived_NX : Base_NX
+ {
+ public static Derived_NX operator +(Derived_NX lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_NX.op_Addition({0})", rhs);
+ return new Derived_NX { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_NX.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_SN
+ {
+ public int Value;
+
+ public static Base_SN operator +(Base_SN lhs, int rhs)
+ {
+ Console.WriteLine(" Base_SN.op_Addition({0})", rhs);
+ return new Base_SN { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Derived_SN : Base_SN
+ {
+ }
+
+ class Base_SS
+ {
+ public int Value;
+
+ public static Base_SS operator +(Base_SS lhs, int rhs)
+ {
+ Console.WriteLine(" Base_SS.op_Addition({0})", rhs);
+ return new Base_SS { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Derived_SS : Base_SS
+ {
+ public static Derived_SS operator +(Derived_SS lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_SS.op_Addition({0})", rhs);
+ return new Derived_SS { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Base_SI
+ {
+ public int Value;
+
+ public static Base_SI operator +(Base_SI lhs, int rhs)
+ {
+ Console.WriteLine(" Base_SI.op_Addition({0})", rhs);
+ return new Base_SI { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Derived_SI : Base_SI
+ {
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_SI.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_SX
+ {
+ public int Value;
+
+ public static Base_SX operator +(Base_SX lhs, int rhs)
+ {
+ Console.WriteLine(" Base_SX.op_Addition({0})", rhs);
+ return new Base_SX { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Derived_SX : Base_SX
+ {
+ public static Derived_SX operator +(Derived_SX lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_SX.op_Addition({0})", rhs);
+ return new Derived_SX { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_SX.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_IN
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_IN.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_IN : Base_IN
+ {
+ }
+
+ class Base_IS
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_IS.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_IS : Base_IS
+ {
+ public static Derived_IS operator +(Derived_IS lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_IS.op_Addition({0})", rhs);
+ return new Derived_IS { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Base_II
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_II.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_II : Base_II
+ {
+ public new void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_II.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_IX
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_IX.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_IX : Base_IX
+ {
+ public static Derived_IX operator +(Derived_IX lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_IX.op_Addition({0})", rhs);
+ return new Derived_IX { Value = lhs.Value + rhs };
+ }
+
+ public new void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_IX.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_XN
+ {
+ public int Value;
+
+ public static Base_XN operator +(Base_XN lhs, int rhs)
+ {
+ Console.WriteLine(" Base_XN.op_Addition({0})", rhs);
+ return new Base_XN { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_XN.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_XN : Base_XN
+ {
+ }
+
+ class Base_XS
+ {
+ public int Value;
+
+ public static Base_XS operator +(Base_XS lhs, int rhs)
+ {
+ Console.WriteLine(" Base_XS.op_Addition({0})", rhs);
+ return new Base_XS { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_XS.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_XS : Base_XS
+ {
+ public static Derived_XS operator +(Derived_XS lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_XS.op_Addition({0})", rhs);
+ return new Derived_XS { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Base_XI
+ {
+ public int Value;
+
+ public static Base_XI operator +(Base_XI lhs, int rhs)
+ {
+ Console.WriteLine(" Base_XI.op_Addition({0})", rhs);
+ return new Base_XI { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_XI.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_XI : Base_XI
+ {
+ public new void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_XI.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_XX
+ {
+ public int Value;
+
+ public static Base_XX operator +(Base_XX lhs, int rhs)
+ {
+ Console.WriteLine(" Base_XX.op_Addition({0})", rhs);
+ return new Base_XX { Value = lhs.Value + rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_XX.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_XX : Base_XX
+ {
+ public static Derived_XX operator +(Derived_XX lhs, int rhs)
+ {
+ Console.WriteLine(" Derived_XX.op_Addition({0})", rhs);
+ return new Derived_XX { Value = lhs.Value + rhs };
+ }
+
+ public new void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_XX.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Base_SR
+ {
+ public int Value;
+
+ public static Derived_SR operator +(Base_SR lhs, int rhs)
+ {
+ Console.WriteLine(" Base_SR.op_Addition({0})", rhs);
+ return new Derived_SR { Value = lhs.Value + rhs };
+ }
+ }
+
+ class Derived_SR : Base_SR
+ {
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Derived_SR.op_AdditionAssignment({0})", rhs);
+ Value += rhs * 100;
+ }
+ }
+
+ class MixedSignature
+ {
+ public int Value;
+
+ public static MixedSignature operator +(MixedSignature lhs, long rhs)
+ {
+ Console.WriteLine(" MixedSignature.op_Addition({0})", rhs);
+ return new MixedSignature { Value = lhs.Value + (int)rhs };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" MixedSignature.op_AdditionAssignment({0})", rhs);
+ Value += rhs * 100;
+ }
+ }
+
+ class Base_Overload
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Console.WriteLine(" Base_Overload.op_AdditionAssignment({0})", rhs);
+ Value += rhs;
+ }
+ }
+
+ class Derived_Overload : Base_Overload
+ {
+ public void operator +=(long rhs)
+ {
+ Console.WriteLine(" Derived_Overload.op_AdditionAssignment({0}L)", rhs);
+ Value += (int)rhs * 100;
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs
new file mode 100644
index 000000000..7540202bc
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.cs
@@ -0,0 +1,243 @@
+using System.Runtime.CompilerServices;
+
+public class BadArityTarget
+{
+ public int Value;
+
+ // C# 14 compound assignment operators take exactly one parameter and the instance
+ // increment operators none; any other arity stays a plain method.
+ [SpecialName]
+ public void op_AdditionAssignment(int a, int b)
+ {
+ }
+
+ [SpecialName]
+ public void op_SubtractionAssignment()
+ {
+ }
+
+ [SpecialName]
+ public void op_IncrementAssignment(int a)
+ {
+ }
+}
+public class BadParameterShapes
+{
+ // C# allows only value, "in" and "ref readonly" parameters on a compound assignment
+ // operator; ref and params stay plain methods.
+ [SpecialName]
+ public void op_SubtractionAssignment(ref int rhs)
+ {
+ }
+
+ [SpecialName]
+ public void op_MultiplicationAssignment(params int[] rhs)
+ {
+ }
+}
+public class CompoundTarget
+{
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ // Not "operator -=": C# 14 only recognizes void-returning instance methods as compound
+ // assignment operators, so a value-returning one stays a plain method.
+ [SpecialName]
+ public CompoundTarget op_SubtractionAssignment(int rhs)
+ {
+ return this;
+ }
+
+ [SpecialName]
+ public static CompoundTarget op_MultiplicationAssignment(CompoundTarget lhs, int rhs)
+ {
+ return lhs;
+ }
+}
+public class CovariantBase
+{
+ public int Value;
+
+ public static CovariantDerived operator ++(CovariantBase x)
+ {
+ return (CovariantDerived)x;
+ }
+}
+public class CovariantDerived : CovariantBase
+{
+ public void operator ++()
+ {
+ }
+}
+public class DerivedCompoundTarget : CompoundTarget
+{
+ public void CallBaseOperator(int n)
+ {
+ // "this" cannot be the target of "x op= y", so the receiver keeps a copy of it;
+ // the copy binds the same inherited operator the IL calls.
+ DerivedCompoundTarget derivedCompoundTarget = this;
+ derivedCompoundTarget += n;
+ }
+}
+public class EdgeCases
+{
+ public static int Sink;
+
+ public static ShadowTarget Shared;
+
+ // A statement-level "x++" considers the instance operator first; the only form that always
+ // binds the static operator is a postfix increment whose result is used, so the result goes
+ // to a discard.
+ public static void StatementLevelPostIncrement()
+ {
+ _ = Shared++;
+ }
+
+ // A pre-increment whose result is used prefers the instance operator too, so the increment
+ // becomes a statement of its own before the uses of the new value.
+ public static void ValueUsedPreIncrement()
+ {
+ ShadowTarget shared = Shared;
+ _ = shared++;
+ UseValue(Shared = shared);
+ }
+
+ public static void UseValue(ShadowTarget t)
+ {
+ }
+
+ // The same holds for the shape that stores the old value inside the operator call.
+ public static void InlineStorePostIncrement()
+ {
+ _ = Shared++;
+ }
+
+ // An array element is a variable, so a statement-level "++arr[0]" would bind the instance
+ // operator too.
+ public static void ArrayElementIncrement(ShadowTarget[] arr)
+ {
+ ShadowTarget shadowTarget = arr[0];
+ _ = shadowTarget++;
+ arr[0] = shadowTarget;
+ }
+
+ public static CompoundTarget GetTarget()
+ {
+ return new CompoundTarget();
+ }
+
+ public static void NonVariableReceiver(int n)
+ {
+ CompoundTarget target = GetTarget();
+ target += n;
+ }
+
+ public static void UnconstrainedGenericReceiver(T x, int n) where T : IOther
+ {
+ ICompound compound = (ICompound)(object)x;
+ compound += n;
+ }
+
+ // Not "x += n": C# requires a user-defined operator to be public, so an operator that
+ // is not cannot be bound by the operator form and the call has to stay a call.
+ public static void InaccessibleOperator(InaccessibleOperatorTarget x, int n)
+ {
+ x.op_AdditionAssignment(n);
+ }
+
+ public static void MismatchedReceiverType(object o, int n)
+ {
+ ((CompoundTarget)o).op_AdditionAssignment(n);
+ }
+
+ // A prefix increment prefers the instance "operator ++()" even where its result is used, so
+ // the increment is a statement with a discarded postfix result; the copy keeps the
+ // parameter unchanged, as in the IL.
+ public static ShadowTarget ShadowedIncrement(ShadowTarget x)
+ {
+ ShadowTarget result = x;
+ _ = result++;
+ return result;
+ }
+
+ public static void CallNonCSharpOperators(CompoundTarget t, int n)
+ {
+ t.op_SubtractionAssignment(n);
+ CompoundTarget.op_MultiplicationAssignment(t, n);
+ }
+
+ // The static operator's covariant return lets the result be stored in a derived-typed
+ // local, and on that type "++x" would bind the instance operator; a postfix increment with
+ // its result discarded binds the static one.
+ public static CovariantDerived CovariantIncrement(CovariantDerived d)
+ {
+ CovariantDerived covariantDerived = d;
+ _ = covariantDerived++;
+ Sink = covariantDerived.Value;
+ return covariantDerived;
+ }
+
+ // Not "x += n": the "in" overload is what the IL calls, but "x += n" binds the by-value
+ // overload, so the call has to stay a call.
+ public static void CallInOverload(InOverloads x, int n)
+ {
+ int rhs = n;
+ x.op_AdditionAssignment(in rhs);
+ }
+}
+public interface ICompound
+{
+ void operator +=(int rhs);
+}
+public class InaccessibleOperatorTarget
+{
+ public int Value;
+
+ // Not "operator +=": a non-public operator is not declarable as one in C#.
+ private void op_AdditionAssignment(int rhs)
+ {
+ Value += rhs;
+ }
+}
+public class InOverloads
+{
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ }
+
+ public void operator +=(in int rhs)
+ {
+ }
+}
+public interface IOther
+{
+ void M();
+}
+public class ShadowTarget
+{
+ public int Value;
+
+ public static ShadowTarget operator ++(ShadowTarget x)
+ {
+ return x;
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+}
+public static class StaticClassOps
+{
+ // a static class cannot contain operators
+ [SpecialName]
+ public void op_AdditionAssignment(int rhs)
+ {
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il
new file mode 100644
index 000000000..6e2ec1b15
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/CompoundAssignmentOperatorEdgeCases.il
@@ -0,0 +1,522 @@
+// Metadata version: v4.0.30319
+.assembly extern System.Runtime
+{
+ .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
+ .ver 11:0:0:0
+}
+.assembly CompoundAssignmentOperatorEdgeCases
+{
+ .ver 1:0:0:0
+}
+.module CompoundAssignmentOperatorEdgeCases.dll
+.imagebase 0x10000000
+.file alignment 0x00000200
+.stackreserve 0x00100000
+.subsystem 0x0003 // WINDOWS_CUI
+.corflags 0x00000001 // ILONLY
+
+.class public auto ansi beforefieldinit CompoundTarget
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ // A C# 14 user-defined compound assignment operator.
+ .method public hidebysig specialname instance void
+ op_AdditionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.0
+ IL_0002: ldfld int32 CompoundTarget::Value
+ IL_0007: ldarg.1
+ IL_0008: add
+ IL_0009: stfld int32 CompoundTarget::Value
+ IL_000e: ret
+ }
+
+ // C++/CLI emits value-returning instance operators; C# has no syntax for those.
+ .method public hidebysig specialname instance class CompoundTarget
+ op_SubtractionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ret
+ }
+
+ // F# mangles "static member (*=)" to a static, value-returning op_MultiplicationAssignment.
+ .method public hidebysig specialname static class CompoundTarget
+ op_MultiplicationAssignment(class CompoundTarget lhs,
+ int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit InaccessibleOperatorTarget
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ // C# requires a user-defined operator to be public, so no C# expression can bind to
+ // this one and it cannot be declared as an operator either.
+ .method private hidebysig specialname instance void
+ op_AdditionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.0
+ IL_0002: ldfld int32 InaccessibleOperatorTarget::Value
+ IL_0007: ldarg.1
+ IL_0008: add
+ IL_0009: stfld int32 InaccessibleOperatorTarget::Value
+ IL_000e: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class interface public abstract auto ansi beforefieldinit ICompound
+{
+ .method public hidebysig newslot specialname abstract virtual
+ instance void op_AdditionAssignment(int32 rhs) cil managed
+ {
+ }
+}
+
+.class interface public abstract auto ansi beforefieldinit IOther
+{
+ .method public hidebysig newslot abstract virtual
+ instance void M() cil managed
+ {
+ }
+}
+
+.class public auto ansi beforefieldinit DerivedCompoundTarget
+ extends CompoundTarget
+{
+ .method public hidebysig instance void CallBaseOperator(int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: call instance void CompoundTarget::op_AdditionAssignment(int32)
+ IL_0007: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void CompoundTarget::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit ShadowTarget
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ // A static increment operator on a type that also declares the C# 14 instance one.
+ .method public hidebysig specialname static class ShadowTarget
+ op_Increment(class ShadowTarget x) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ret
+ }
+
+ .method public hidebysig specialname instance void
+ op_IncrementAssignment() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.0
+ IL_0002: ldfld int32 ShadowTarget::Value
+ IL_0007: ldc.i4.1
+ IL_0008: add
+ IL_0009: stfld int32 ShadowTarget::Value
+ IL_000e: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit EdgeCases
+ extends [System.Runtime]System.Object
+{
+ .field public static int32 Sink
+ .field public static class ShadowTarget Shared
+
+ // The post-increment shape legacy csc emits: the old value goes to a temp that nothing reads,
+ // so the increment is written as a statement, where C# considers the instance operator first.
+ .method public hidebysig static void StatementLevelPostIncrement() cil managed
+ {
+ .maxstack 2
+ .locals init (class ShadowTarget V_0)
+ IL_0000: ldsfld class ShadowTarget EdgeCases::Shared
+ IL_0005: stloc.0
+ IL_0006: ldloc.0
+ IL_0007: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget)
+ IL_000c: stsfld class ShadowTarget EdgeCases::Shared
+ IL_0011: ret
+ }
+
+ // A pre-increment whose result is used: a prefix increment would bind the instance operator,
+ // so the increment has to become a statement of its own before the uses of the new value.
+ .method public hidebysig static void ValueUsedPreIncrement() cil managed
+ {
+ .maxstack 2
+ IL_0000: ldsfld class ShadowTarget EdgeCases::Shared
+ IL_0005: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget)
+ IL_000a: dup
+ IL_000b: stsfld class ShadowTarget EdgeCases::Shared
+ IL_0010: call void EdgeCases::UseValue(class ShadowTarget)
+ IL_0015: ret
+ }
+
+ .method public hidebysig static void UseValue(class ShadowTarget t) cil managed
+ {
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ // The other legacy post-increment shape, storing the old value inside the operator call: it is
+ // written as a statement too where nothing reads that value.
+ .method public hidebysig static void InlineStorePostIncrement() cil managed
+ {
+ .maxstack 3
+ .locals init (class ShadowTarget V_0)
+ IL_0000: ldsfld class ShadowTarget EdgeCases::Shared
+ IL_0005: dup
+ IL_0006: stloc.0
+ IL_0007: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget)
+ IL_000c: stsfld class ShadowTarget EdgeCases::Shared
+ IL_0011: ret
+ }
+
+ // An array element is a variable, and "ldelem.ref" leaves the reader with the stack type
+ // rather than with the element type.
+ .method public hidebysig static void ArrayElementIncrement(class ShadowTarget[] arr) cil managed
+ {
+ .maxstack 3
+ .locals init (class ShadowTarget V_0)
+ IL_0000: ldarg.0
+ IL_0001: dup
+ IL_0002: ldc.i4.0
+ IL_0003: ldelem.ref
+ IL_0004: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget)
+ IL_0009: stloc.0
+ IL_000a: ldc.i4.0
+ IL_000b: ldloc.0
+ IL_000c: stelem.ref
+ IL_000d: ret
+ }
+
+ .method public hidebysig static class CompoundTarget
+ GetTarget() cil managed
+ {
+ .maxstack 8
+ IL_0000: newobj instance void CompoundTarget::.ctor()
+ IL_0005: ret
+ }
+
+ // The receiver is not a variable, so it has to be given one before it can become the
+ // target of "x += n".
+ .method public hidebysig static void NonVariableReceiver(int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: call class CompoundTarget EdgeCases::GetTarget()
+ IL_0005: ldarg.0
+ IL_0006: callvirt instance void CompoundTarget::op_AdditionAssignment(int32)
+ IL_000b: ret
+ }
+
+ // T is not constrained to ICompound, so the cast selects the operator and has to stay; it
+ // is the variable holding the cast value that becomes the assignment target.
+ .method public hidebysig static void UnconstrainedGenericReceiver<(IOther) T>(!!T x,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: box !!T
+ IL_0006: castclass ICompound
+ IL_000b: ldarg.1
+ IL_000c: callvirt instance void ICompound::op_AdditionAssignment(int32)
+ IL_0011: ret
+ }
+
+ // The operator is not public, so the call cannot become "x += n".
+ .method public hidebysig static void InaccessibleOperator(class InaccessibleOperatorTarget x,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: callvirt instance void InaccessibleOperatorTarget::op_AdditionAssignment(int32)
+ IL_0007: ret
+ }
+
+ // The receiver's static type does not declare the operator, so overload resolution has no
+ // candidate to pick.
+ .method public hidebysig static void MismatchedReceiverType(object o,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: callvirt instance void CompoundTarget::op_AdditionAssignment(int32)
+ IL_0007: ret
+ }
+
+ // "x++" would bind the instance operator, so the call to the static one has to stay a call.
+ .method public hidebysig static class ShadowTarget
+ ShadowedIncrement(class ShadowTarget x) cil managed
+ {
+ .maxstack 8
+ .locals init (class ShadowTarget V_0)
+ IL_0000: ldarg.0
+ IL_0001: call class ShadowTarget ShadowTarget::op_Increment(class ShadowTarget)
+ IL_0006: stloc.0
+ IL_0007: ldloc.0
+ IL_0008: ret
+ }
+
+ .method public hidebysig static void CallNonCSharpOperators(class CompoundTarget t,
+ int32 n) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: ldarg.1
+ IL_0002: callvirt instance class CompoundTarget CompoundTarget::op_SubtractionAssignment(int32)
+ IL_0007: pop
+ IL_0008: ldarg.0
+ IL_0009: ldarg.1
+ IL_000a: call class CompoundTarget CompoundTarget::op_MultiplicationAssignment(class CompoundTarget,
+ int32)
+ IL_000f: pop
+ IL_0010: ret
+ }
+
+ // Not "++x": the static operator's covariant return lets the result be stored in a
+ // derived-typed local, and on that type "++x" would bind the instance operator.
+ .method public hidebysig static class CovariantDerived
+ CovariantIncrement(class CovariantDerived d) cil managed
+ {
+ .maxstack 8
+ .locals init (class CovariantDerived V_0)
+ IL_0000: ldarg.0
+ IL_0001: call class CovariantDerived CovariantBase::op_Increment(class CovariantBase)
+ IL_0006: stloc.0
+ IL_0007: ldloc.0
+ IL_0008: ldfld int32 CovariantBase::Value
+ IL_000d: stsfld int32 EdgeCases::Sink
+ IL_0012: ldloc.0
+ IL_0013: ret
+ }
+
+ // Not "x += n": the "in" overload is what the IL calls, but "x += n" binds the by-value
+ // overload, so the call has to stay a call.
+ .method public hidebysig static void CallInOverload(class InOverloads x, int32 n) cil managed
+ {
+ .maxstack 8
+ .locals init (int32 V_0)
+ IL_0000: ldarg.1
+ IL_0001: stloc.0
+ IL_0002: ldarg.0
+ IL_0003: ldloca.s V_0
+ IL_0005: callvirt instance void InOverloads::op_AdditionAssignment(int32&)
+ IL_000a: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit BadArityTarget
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ // C# 14 compound assignment operators take exactly one parameter and the instance
+ // increment operators none; any other arity stays a plain method.
+ .method public hidebysig specialname instance void
+ op_AdditionAssignment(int32 a,
+ int32 b) cil managed
+ {
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ .method public hidebysig specialname instance void
+ op_SubtractionAssignment() cil managed
+ {
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ .method public hidebysig specialname instance void
+ op_IncrementAssignment(int32 a) cil managed
+ {
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit CovariantBase
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ .method public hidebysig specialname static class CovariantDerived
+ op_Increment(class CovariantBase x) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: castclass CovariantDerived
+ IL_0006: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit CovariantDerived
+ extends CovariantBase
+{
+ .method public hidebysig specialname instance void
+ op_IncrementAssignment() cil managed
+ {
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void CovariantBase::.ctor()
+ IL_0006: ret
+ }
+}
+
+.class public auto ansi beforefieldinit BadParameterShapes
+ extends [System.Runtime]System.Object
+{
+ // C# allows only value, "in" and "ref readonly" parameters on a compound assignment
+ // operator; ref and params stay plain methods.
+ .method public hidebysig specialname instance void
+ op_SubtractionAssignment(int32& rhs) cil managed
+ {
+ .maxstack 8
+ ret
+ }
+
+ .method public hidebysig specialname instance void
+ op_MultiplicationAssignment(int32[] rhs) cil managed
+ {
+ .param [1]
+ .custom instance void [System.Runtime]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 )
+ .maxstack 8
+ ret
+ }
+
+ .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
+ {
+ .maxstack 8
+ ldarg.0
+ call instance void [System.Runtime]System.Object::.ctor()
+ ret
+ }
+}
+
+// a static class cannot contain operators
+.class public abstract auto ansi sealed beforefieldinit StaticClassOps
+ extends [System.Runtime]System.Object
+{
+ .method public hidebysig specialname instance void
+ op_AdditionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ ret
+ }
+}
+
+.class public auto ansi beforefieldinit InOverloads
+ extends [System.Runtime]System.Object
+{
+ .field public int32 Value
+
+ .method public hidebysig specialname instance void
+ op_AdditionAssignment(int32 rhs) cil managed
+ {
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ .method public hidebysig specialname instance void
+ op_AdditionAssignment([in] int32& rhs) cil managed
+ {
+ .param [1]
+ .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 )
+ .maxstack 8
+ IL_0000: ret
+ }
+
+ .method public hidebysig specialname rtspecialname
+ instance void .ctor() cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call instance void [System.Runtime]System.Object::.ctor()
+ IL_0006: ret
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs
new file mode 100644
index 000000000..52d9f1713
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/UserDefinedCompoundAssignment.cs
@@ -0,0 +1,628 @@
+// Copyright (c) 2026 Siegfried Pammer
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Collections.Generic;
+
+namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
+{
+ internal class UserDefinedCompoundAssignment
+ {
+ public class CompoundClass
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public void operator checked +=(int rhs)
+ {
+ checked
+ {
+ Value += rhs;
+ }
+ }
+
+ public void operator -=(int rhs)
+ {
+ Value -= rhs;
+ }
+
+ public void operator checked -=(int rhs)
+ {
+ checked
+ {
+ Value -= rhs;
+ }
+ }
+
+ public void operator *=(int rhs)
+ {
+ Value *= rhs;
+ }
+
+ public void operator checked *=(int rhs)
+ {
+ checked
+ {
+ Value *= rhs;
+ }
+ }
+
+ public void operator /=(int rhs)
+ {
+ Value /= rhs;
+ }
+
+ public void operator checked /=(int rhs)
+ {
+ Value /= rhs;
+ }
+
+ public void operator %=(int rhs)
+ {
+ Value %= rhs;
+ }
+
+ public void operator &=(int rhs)
+ {
+ Value &= rhs;
+ }
+
+ public void operator |=(int rhs)
+ {
+ Value |= rhs;
+ }
+
+ public void operator ^=(int rhs)
+ {
+ Value ^= rhs;
+ }
+
+ public void operator <<=(int rhs)
+ {
+ Value <<= rhs;
+ }
+
+ public void operator >>=(int rhs)
+ {
+ Value >>= rhs;
+ }
+
+ public void operator >>>=(int rhs)
+ {
+ Value >>>= rhs;
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+
+ public void operator checked ++()
+ {
+ checked
+ {
+ Value++;
+ }
+ }
+
+ public void operator --()
+ {
+ Value--;
+ }
+
+ public void operator checked --()
+ {
+ checked
+ {
+ Value--;
+ }
+ }
+
+ public virtual void operator +=(long rhs)
+ {
+ Value += (int)rhs;
+ }
+
+ // "this" is not an assignable variable in a class, so the copy is what makes the
+ // operator form legal; it has to survive decompilation.
+ public void AddViaThisCopy(int n)
+ {
+ CompoundClass compoundClass = this;
+ compoundClass += n;
+ }
+
+ public void IncrementViaThisCopy()
+ {
+ CompoundClass compoundClass = this;
+ compoundClass++;
+ }
+ }
+
+ public struct CompoundStruct
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public readonly void operator -=(int rhs)
+ {
+ Console.WriteLine(Value - rhs);
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+
+ public void AddViaThis()
+ {
+ this += 10;
+ }
+ }
+
+ public class DerivedCompoundClass : CompoundClass
+ {
+ }
+
+ public class OverridingCompoundClass : CompoundClass
+ {
+ public override void operator +=(long rhs)
+ {
+ Value += (int)(rhs * 2);
+ }
+ }
+
+ public class ShadowingCompoundClass : CompoundClass
+ {
+ public new void operator +=(int rhs)
+ {
+ Value += rhs * 2;
+ }
+ }
+
+ public class InParameterOperators
+ {
+ public int Value;
+
+ public static InParameterOperators operator +(InParameterOperators lhs, in InParameterOperators rhs)
+ {
+ return new InParameterOperators {
+ Value = lhs.Value + rhs.Value
+ };
+ }
+
+ public void operator +=(in InParameterOperators rhs)
+ {
+ Value += rhs.Value;
+ }
+ }
+
+ public class ShadowingInParameterOperators : InParameterOperators
+ {
+ public new void operator +=(in InParameterOperators rhs)
+ {
+ Value += rhs.Value * 2;
+ }
+ }
+
+ public sealed class DisposableCompound : IDisposable
+ {
+ public int Value;
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+
+ public class ReadonlyFieldHolder
+ {
+ public static readonly CompoundClass Shared;
+
+ private readonly CompoundClass instance = new CompoundClass();
+
+ static ReadonlyFieldHolder()
+ {
+ Shared = new CompoundClass();
+ // A readonly field is a variable inside the matching constructor, so the
+ // operator form stays legal here.
+ Shared += 1;
+ }
+
+ public ReadonlyFieldHolder()
+ {
+ instance += 2;
+ }
+ }
+
+ // A value-type element makes csc address the storage once with ldelema, which is a
+ // different shape again from the reference-type case.
+ public struct StaticOnlyStruct
+ {
+ public int Value;
+
+ public static StaticOnlyStruct operator +(StaticOnlyStruct lhs, int rhs)
+ {
+ return new StaticOnlyStruct {
+ Value = lhs.Value + rhs
+ };
+ }
+ }
+
+ // No instance operator, so "x += n" reaches the static one and csc emits the compound
+ // assignment shape rather than a load and a store.
+ public class StaticOnlyOperators
+ {
+ public int Value;
+
+ public static StaticOnlyOperators operator +(StaticOnlyOperators lhs, int rhs)
+ {
+ return new StaticOnlyOperators {
+ Value = lhs.Value + rhs
+ };
+ }
+
+ public static StaticOnlyOperators operator ++(StaticOnlyOperators x)
+ {
+ return new StaticOnlyOperators {
+ Value = x.Value + 1
+ };
+ }
+ }
+
+ public class BothOperators
+ {
+ public int Value;
+
+ public static BothOperators operator +(BothOperators lhs, int rhs)
+ {
+ return new BothOperators {
+ Value = lhs.Value + rhs
+ };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ public static BothOperators operator ++(BothOperators x)
+ {
+ return new BothOperators {
+ Value = x.Value + 1
+ };
+ }
+
+ public void operator ++()
+ {
+ Value++;
+ }
+ }
+
+ public class MixedSignatureOperators
+ {
+ public int Value;
+
+ public static MixedSignatureOperators operator +(MixedSignatureOperators lhs, long rhs)
+ {
+ return new MixedSignatureOperators {
+ Value = lhs.Value + (int)rhs
+ };
+ }
+
+ public void operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+ }
+
+ public interface ICompound
+ {
+ void operator +=(T rhs);
+ void operator ++();
+ }
+
+ public class ExplicitCompound : ICompound
+ {
+ public int Value;
+
+ void ICompound.operator +=(int rhs)
+ {
+ Value += rhs;
+ }
+
+ void ICompound.operator ++()
+ {
+ Value++;
+ }
+ }
+
+ private static CompoundClass staticField = new CompoundClass();
+
+ private CompoundClass instanceField = new CompoundClass();
+
+ private BothOperators bothField = new BothOperators();
+
+ private BothOperators[] bothArray = new BothOperators[4];
+
+ private static CompoundClass refReturnTarget = new CompoundClass();
+
+ private static CompoundStruct refReturnStructTarget;
+
+ public BothOperators BothProperty { get; set; }
+
+ public BothOperators this[int index] {
+ get {
+ return bothArray[index];
+ }
+ set {
+ bothArray[index] = value;
+ }
+ }
+
+ public static void UseClass(CompoundClass c, int n)
+ {
+ c += n;
+ c += 1;
+ c -= n;
+ c *= n;
+ c /= n;
+ c %= n;
+ c &= n;
+ c |= n;
+ c ^= n;
+ c <<= n;
+ c >>= n;
+ c >>>= n;
+ c += 2L;
+ c++;
+ c--;
+ checked
+ {
+ c += n;
+ c -= n;
+ c *= n;
+ c /= n;
+ c++;
+ c--;
+ }
+ }
+
+ public static void UseStruct(CompoundStruct s, int n)
+ {
+ s += n;
+ s -= n;
+ s++;
+ }
+
+ public static void UseOtherTargets(CompoundClass[] arr, ref CompoundClass rc, ref CompoundStruct rs, UserDefinedCompoundAssignment inst, int n)
+ {
+ staticField += n;
+ inst.instanceField += n;
+ arr[0] += n;
+ rc += n;
+ rs += n;
+ rs++;
+ }
+
+ public static void UseGeneric(T x, int n) where T : ICompound
+ {
+ x += n;
+ x++;
+ }
+
+ public static void UseInheritedOperator(DerivedCompoundClass d, int n)
+ {
+ d += n;
+ d++;
+ }
+
+ public static void UseShadowedOperator(ShadowingCompoundClass s, int n)
+ {
+ s += n;
+ }
+
+ public static void UseOverriddenOperator(OverridingCompoundClass o, long n)
+ {
+ o += n;
+ }
+
+ public static BothOperators UsePostfixIncrementResultUsed(BothOperators b)
+ {
+ return b++;
+ }
+
+ public static BothOperators UseStaticOperator(BothOperators b, int n)
+ {
+ // The IL calls the static operator, so the decompiled code has to keep calling it:
+ // "b += n" and "b++" would bind to the instance operators instead.
+ b = b + n;
+ b = b + 1;
+ return b;
+ }
+
+ public static void UseCompoundAssignmentOnArrayElement(StaticOnlyOperators[] arr, int n)
+ {
+ arr[0] += n;
+ arr[0]++;
+ }
+
+ public static void UseCompoundAssignmentOnStructArrayElement(StaticOnlyStruct[] arr, int n)
+ {
+ arr[0] += n;
+ }
+
+ public static void UseStaticOperatorOnArrayElement(BothOperators[] arr, int n)
+ {
+ // The element type declares an instance operator too, so the static call has to stay
+ // spelled out; array elements take a separate path through the decompiler to locals.
+ arr[0] = arr[0] + n;
+ arr[0] = arr[0] + 1;
+ }
+
+ public static void UseStaticOperatorOnField(UserDefinedCompoundAssignment inst, int n)
+ {
+ inst.bothField = inst.bothField + n;
+ }
+
+ public static void UseStaticOperatorOnRefLocal(BothOperators[] arr, int n)
+ {
+ // A ref local is a variable, so "r += n" would bind the instance operator here too.
+ ref BothOperators reference = ref arr[0];
+ reference = reference + n;
+ reference = reference + 1;
+ }
+
+ public static void UseBaseTypedLocalOfShadowingClass(ShadowingCompoundClass s, int n)
+ {
+ // The local is what makes "c += n" bind the base operator rather than the shadowing
+ // one, so it has to survive even where the compiler optimized it away.
+ CompoundClass compoundClass = s;
+ compoundClass += n;
+ }
+
+ public static void UseCheckedOperatorOfBaseTypedLocal(ShadowingCompoundClass s, int n)
+ {
+ // The derived type hides only the regular flavor, but a checked context does not
+ // exclude regular operators - it prefers a checked one within the type that wins the
+ // lookup. On a derived-typed receiver the hiding operator would take this call, so
+ // the base-typed local has to survive here too.
+ CompoundClass compoundClass = s;
+ checked
+ {
+ compoundClass += n;
+ }
+ }
+
+ public static void UseStaticOperatorOnPropertyAndIndexer(UserDefinedCompoundAssignment inst, int n)
+ {
+ // A property and an indexer are not variables, so the instance operator is never a
+ // candidate for them: "x += n" binds the static operator, which is the one the IL calls.
+ inst.BothProperty += n;
+ inst[0] += n;
+ }
+
+ public static void UseStaticOperatorWhenInstanceDoesNotApply(MixedSignatureOperators m, MixedSignatureOperators[] arr, long n)
+ {
+ // The instance operator takes an int, so it is not a candidate for a long argument:
+ // "x += n" binds the static operator here, which is the one the IL calls.
+ m += n;
+ arr[0] += n;
+ }
+
+ public static void UseInstanceOperator(BothOperators b, int n)
+ {
+ b += n;
+ b++;
+ }
+
+ public static ref CompoundClass GetRefTarget()
+ {
+ return ref refReturnTarget;
+ }
+
+ public static ref CompoundStruct GetRefStructTarget()
+ {
+ return ref refReturnStructTarget;
+ }
+
+ public static void UseRefReturnTarget(int n)
+ {
+ // A ref-returning invocation is a variable, so the operator form binds here.
+ GetRefTarget() += n;
+ GetRefTarget()++;
+ GetRefStructTarget() += n;
+ GetRefStructTarget()++;
+ }
+
+ public unsafe static void UsePointerReceiver(CompoundStruct* p, int n)
+ {
+ // A pointer indirection is a variable, so the operator form binds here.
+ *p += n;
+ (*p)++;
+ }
+
+ public static void UseForeachVariableCopy(List list, int n)
+ {
+ // The iteration variable is read-only, so the operator is applied to a copy; the
+ // copy has to survive even where the compiler optimized it away.
+ foreach (CompoundClass item in list)
+ {
+ CompoundClass compoundClass = item;
+ compoundClass += n;
+ }
+ }
+
+ public static void UseUsingVariableCopy(int n)
+ {
+ // The using variable is read-only, so the operator is applied to a copy; the copy has
+ // to survive even where the compiler optimized it away.
+ using DisposableCompound disposableCompound = new DisposableCompound();
+ DisposableCompound disposableCompound2 = disposableCompound;
+ disposableCompound2 += n;
+ }
+
+ public static void UseExplicitInterfaceOperator(ExplicitCompound e, int n)
+ {
+ // The interface-typed variable is the only receiver that can bind an explicitly
+ // implemented operator, so the copy has to survive decompilation.
+ ICompound compound = e;
+ compound += n;
+ ICompound compound2 = e;
+ compound2++;
+ }
+
+ public static void KeepStaticOperatorWithInParameter(InParameterOperators x, InParameterOperators y)
+ {
+ // "x += y" binds the instance operator, so folding the static call the IL makes into
+ // the operator form would change which operator runs.
+ x = x + y;
+ }
+
+ public static void UseCopyForShadowedInParameterOperator(ShadowingInParameterOperators s, InParameterOperators y)
+ {
+ // The receiver has to keep the base type: the derived type hides the operator with a
+ // "new" one, which the operator form would bind instead.
+ InParameterOperators inParameterOperators = s;
+ inParameterOperators += y;
+ }
+
+ public static void UseForeachDeconstructionCopy(List<(CompoundClass, int)> list, int n)
+ {
+ // The variables a foreach deconstruction designation introduces are read-only, so the
+ // deconstruction stays in the body, where the operator has an assignable target.
+ foreach (var item in list)
+ {
+ var (compoundClass, value) = item;
+ compoundClass += n;
+ Console.WriteLine(value);
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index ed0668734..0c872d55e 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -827,6 +827,7 @@ namespace ICSharpCode.Decompiler.CSharp
typeSystemAstBuilder.SupportOperatorChecked = settings.CheckedOperators;
typeSystemAstBuilder.AlwaysUseGlobal = settings.AlwaysUseGlobal;
typeSystemAstBuilder.SupportExtensionDeclarations = settings.ExtensionMembers;
+ typeSystemAstBuilder.SupportUserDefinedCompoundAssignmentOperators = settings.UserDefinedCompoundAssignmentOperators;
return typeSystemAstBuilder;
}
@@ -1730,15 +1731,16 @@ namespace ICSharpCode.Decompiler.CSharp
return true;
}
}
- else if (entity.SymbolKind == SymbolKind.Method)
+ else if (entity.SymbolKind is SymbolKind.Method or SymbolKind.Operator)
{
// A method introduced in a class or struct hides all non-method base class members with the same name, and all
// base class methods with the same signature (method name and parameter count, modifiers, and types).
+ // Instance operators hide by those same rules.
if (baseType.GetMembers(m => m.SymbolKind != SymbolKind.Indexer
&& m.SymbolKind != SymbolKind.Constructor
&& m.SymbolKind != SymbolKind.Destructor
&& m.Name == entity.Name && lookup.IsAccessible(m, true))
- .Any(m => m.SymbolKind != SymbolKind.Method ||
+ .Any(m => m.SymbolKind is not (SymbolKind.Method or SymbolKind.Operator) ||
(((IMethod)entity).TypeParameters.Count == ((IMethod)m).TypeParameters.Count
&& parameterListComparer.Equals(((IMethod)entity).Parameters, ((IMethod)m).Parameters))))
{
@@ -2245,8 +2247,11 @@ namespace ICSharpCode.Decompiler.CSharp
methodDecl.Modifiers |= Modifiers.Extern;
}
// Accessors qualify only when they are emitted as ordinary methods (parameterized
- // properties); an Accessor node cannot carry the 'new' modifier.
- if ((method.SymbolKind == SymbolKind.Method || (method.SymbolKind == SymbolKind.Accessor && methodDecl is MethodDeclaration))
+ // properties); an Accessor node cannot carry the 'new' modifier. Instance operators
+ // hide by signature like ordinary methods do; static ones cannot carry 'new' at all.
+ if ((method.SymbolKind == SymbolKind.Method
+ || (method.SymbolKind == SymbolKind.Operator && !method.IsStatic)
+ || (method.SymbolKind == SymbolKind.Accessor && methodDecl is MethodDeclaration))
&& !method.IsExplicitInterfaceImplementation
&& methodDefinition.HasFlag(System.Reflection.MethodAttributes.Virtual) == methodDefinition.HasFlag(System.Reflection.MethodAttributes.NewSlot))
{
@@ -2272,6 +2277,10 @@ namespace ICSharpCode.Decompiler.CSharp
{
RemoveObsoleteAttribute(methodDecl, "Constructors of types with required members are not supported in this version of your compiler.");
}
+ if (methodDecl is OperatorDeclaration operatorDecl && OperatorDeclaration.IsCompoundAssignment(operatorDecl.OperatorType))
+ {
+ RemoveCompilerFeatureRequiredAttribute(methodDecl, "UserDefinedCompoundAssignmentOperators");
+ }
return methodDecl;
bool IsTypeHierarchyKnown(IType type)
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
index 509c3ea7c..12e4314f0 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
@@ -286,6 +286,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
astBuilder.SupportUnsignedRightShift = (ConversionFlags & ConversionFlags.SupportUnsignedRightShift) != 0;
astBuilder.SupportOperatorChecked = (ConversionFlags & ConversionFlags.SupportOperatorChecked) != 0;
astBuilder.SupportExtensionDeclarations = (ConversionFlags & ConversionFlags.SupportExtensionDeclarations) != 0;
+ astBuilder.SupportUserDefinedCompoundAssignmentOperators = (ConversionFlags & ConversionFlags.SupportUserDefinedCompoundAssignmentOperators) != 0;
return astBuilder;
}
@@ -394,7 +395,8 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
writer.WriteKeyword("operator");
writer.Space();
var operatorType = OperatorDeclaration.GetOperatorType(name);
- if (operatorType.HasValue && !((ConversionFlags & ConversionFlags.SupportOperatorChecked) == 0 && OperatorDeclaration.IsChecked(operatorType.Value)))
+ if (operatorType.HasValue && !((ConversionFlags & ConversionFlags.SupportOperatorChecked) == 0 && OperatorDeclaration.IsChecked(operatorType.Value))
+ && !((ConversionFlags & ConversionFlags.SupportUserDefinedCompoundAssignmentOperators) == 0 && OperatorDeclaration.IsCompoundAssignment(operatorType.Value)))
{
if (OperatorDeclaration.IsChecked(operatorType.Value))
{
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
index e3c66d95e..94e7028de 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
@@ -260,6 +260,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// Controls whether C# 14 "extension" declarations are supported.
///
public bool SupportExtensionDeclarations { get; set; }
+
+ ///
+ /// Controls whether C# 14 user-defined compound assignment operators ("operator +=") are supported.
+ ///
+ public bool SupportUserDefinedCompoundAssignmentOperators { get; set; }
#endregion
#region Convert Type
@@ -2556,7 +2561,15 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
OperatorType? opType = OperatorDeclaration.GetOperatorType(name);
if (opType == null)
return ConvertMethod(op);
- if (opType == OperatorType.UnsignedRightShift && !SupportUnsignedRightShift)
+ if (opType is OperatorType.UnsignedRightShift or OperatorType.UnsignedRightShiftAssignment && !SupportUnsignedRightShift)
+ return ConvertMethod(op);
+ if (!SupportUserDefinedCompoundAssignmentOperators && OperatorDeclaration.IsCompoundAssignment(opType.Value))
+ return ConvertMethod(op);
+ // C# requires a user-defined operator to be public, so a compound assignment operator
+ // that is not can only be written as a plain method. An explicit interface
+ // implementation is private in metadata but is still written in operator form.
+ if (OperatorDeclaration.IsCompoundAssignment(opType.Value)
+ && op.Accessibility != Accessibility.Public && !op.IsExplicitInterfaceImplementation)
return ConvertMethod(op);
if (!SupportOperatorChecked && OperatorDeclaration.IsChecked(opType.Value))
return ConvertMethod(op);
diff --git a/ICSharpCode.Decompiler/Output/IAmbience.cs b/ICSharpCode.Decompiler/Output/IAmbience.cs
index f88a8576e..aec5ff0e7 100644
--- a/ICSharpCode.Decompiler/Output/IAmbience.cs
+++ b/ICSharpCode.Decompiler/Output/IAmbience.cs
@@ -125,6 +125,10 @@ namespace ICSharpCode.Decompiler.Output
/// Support C# 14 extension declarations.
///
SupportExtensionDeclarations = 0x400000,
+ ///
+ /// Support C# 14 user-defined compound assignment operators (operator +=).
+ ///
+ SupportUserDefinedCompoundAssignmentOperators = 0x800000,
StandardConversionFlags = ShowParameterNames |
ShowAccessibility |
@@ -140,7 +144,7 @@ namespace ICSharpCode.Decompiler.Output
ShowDefinitionKeyword |
ShowBody,
- All = 0x1fffff,
+ All = 0xffffff,
}
///
diff --git a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
index 15e74dab4..bbe3dc2e0 100644
--- a/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
+++ b/ICSharpCode.ILSpyCmd/IlspyCmdProgram.cs
@@ -160,7 +160,7 @@ Examples:
[Option("-lv|--languageversion ", "C# Language version: CSharp1, CSharp2, CSharp3, " +
"CSharp4, CSharp5, CSharp6, CSharp7, CSharp7_1, CSharp7_2, CSharp7_3, CSharp8_0, CSharp9_0, " +
- "CSharp10_0, CSharp11_0, CSharp12_0, CSharp13_0, Preview or Latest", CommandOptionType.SingleValue)]
+ "CSharp10_0, CSharp11_0, CSharp12_0, CSharp13_0, CSharp14_0, CSharp15_0, Preview or Latest", CommandOptionType.SingleValue)]
public LanguageVersion LanguageVersion { get; } = LanguageVersion.Latest;
[FileExists]
diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs
index 10a46c120..ff5a373e2 100644
--- a/ILSpy/Languages/CSharpLanguage.cs
+++ b/ILSpy/Languages/CSharpLanguage.cs
@@ -86,7 +86,9 @@ namespace ICSharpCode.ILSpy.Languages
};
static CSharpAmbience CreateAmbience() => new() {
- ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList,
+ ConversionFlags = ConversionFlags.ShowTypeParameterList | ConversionFlags.PlaceReturnTypeAfterParameterList
+ | ConversionFlags.SupportOperatorChecked | ConversionFlags.SupportUnsignedRightShift
+ | ConversionFlags.SupportUserDefinedCompoundAssignmentOperators,
};
public override string TypeToString(IType type, ConversionFlags conversionFlags = ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.UseFullyQualifiedTypeNames)