Render user-defined compound assignment operator declarations

TypeSystemAstBuilder writes the C# 14 "operator +=" declaration form, behind a
support flag like the other version-gated operator syntax; a non-public
operator has no legal operator declaration (CS9308) and falls back to a plain
method, except an explicit interface implementation, which is private in
metadata but still written in operator form. An instance operator hides by
signature like an ordinary method, so it can carry "new"; the
[CompilerFeatureRequired] marker the compiler emits is removed like the other
feature markers. Tooltips get the same rendering via a ConversionFlags bit;
widening ConversionFlags.All also turns on the existing checked-operator and
unsigned-right-shift flags, which the tooltip ambience now sets explicitly.

The test fixtures land here, where the pipeline is complete end to end: pretty
and IL round-trips, correctness runs against Roslyn's C# 14 binding (including
the operator-inheritance matrix and a ref-local target), and the ugly
configuration pins the output with the setting off.

Assisted-by: Claude:claude-opus-5:Claude Code
This commit is contained in:
Siegfried Pammer
2026-08-29 09:39:10 +02:00
parent 91dd944d29
commit e4fa167afc
16 changed files with 2527 additions and 10 deletions
@@ -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)
{
@@ -209,6 +209,9 @@
<None Include="TestCases\ILPretty\InstanceOperatorCall.il" />
<Compile Remove="TestCases\ILPretty\EnumArithmeticOutOfRange.cs" />
<None Include="TestCases\ILPretty\EnumArithmeticOutOfRange.cs" />
<Compile Remove="TestCases\ILPretty\CompoundAssignmentOperatorEdgeCases.cs" />
<None Include="TestCases\ILPretty\CompoundAssignmentOperatorEdgeCases.cs" />
<None Include="TestCases\ILPretty\CompoundAssignmentOperatorEdgeCases.il" />
<Compile Remove="TestCases\ILPretty\EmptyBodies.cs" />
<None Include="TestCases\ILPretty\EmptyBodies.cs" />
<Compile Remove="TestCases\ILPretty\TruncatedAccessorBody.cs" />
@@ -345,6 +345,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
[Test]
public async Task CompoundAssignmentOperatorEdgeCases()
{
await Run();
}
[Test]
public async Task FSharpLoops_Debug()
{
@@ -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();
@@ -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)
{
@@ -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>(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<Base> list = new List<Base> { 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);
}
}
}
@@ -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;
}
}
}
}
@@ -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>(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)
{
}
}
@@ -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
}
}
@@ -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<T>
{
void operator +=(T rhs);
void operator ++();
}
public class ExplicitCompound : ICompound<int>
{
public int Value;
void ICompound<int>.operator +=(int rhs)
{
Value += rhs;
}
void ICompound<int>.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>(T x, int n) where T : ICompound<int>
{
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<CompoundClass> 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<int> compound = e;
compound += n;
ICompound<int> 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);
}
}
}
}
@@ -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)
@@ -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))
{
@@ -260,6 +260,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// Controls whether C# 14 "extension" declarations are supported.
/// </summary>
public bool SupportExtensionDeclarations { get; set; }
/// <summary>
/// Controls whether C# 14 user-defined compound assignment operators ("operator +=") are supported.
/// </summary>
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);
+5 -1
View File
@@ -125,6 +125,10 @@ namespace ICSharpCode.Decompiler.Output
/// Support C# 14 <c>extension</c> declarations.
/// </summary>
SupportExtensionDeclarations = 0x400000,
/// <summary>
/// Support C# 14 user-defined compound assignment operators (<c>operator +=</c>).
/// </summary>
SupportUserDefinedCompoundAssignmentOperators = 0x800000,
StandardConversionFlags = ShowParameterNames |
ShowAccessibility |
@@ -140,7 +144,7 @@ namespace ICSharpCode.Decompiler.Output
ShowDefinitionKeyword |
ShowBody,
All = 0x1fffff,
All = 0xffffff,
}
/// <summary>
+1 -1
View File
@@ -160,7 +160,7 @@ Examples:
[Option("-lv|--languageversion <version>", "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]
+3 -1
View File
@@ -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)