diff --git a/.vscode/settings.json b/.vscode/settings.json
index 591102eca..50544dff6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,5 +1,4 @@
{
- "dotnet-test-explorer.testProjectPath": "*.Tests/*Tests.csproj",
"files.exclude": {
"ILSpy-tests/**": true
},
diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index c43c7db0d..e1c7f4529 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -152,6 +152,7 @@
+
diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
index f34b96b5b..a5a556480 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -539,6 +539,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
+ [Test]
+ public async Task NullCoalescingAssign([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
+ {
+ await RunForLibrary(cscOptions: cscOptions);
+ }
+
[Test]
public async Task StringInterpolation([ValueSource(nameof(roslynOnlyWithNet40Options))] CompilerOptions cscOptions)
{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullCoalescingAssign.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullCoalescingAssign.cs
new file mode 100644
index 000000000..20a2ffc87
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullCoalescingAssign.cs
@@ -0,0 +1,161 @@
+// Copyright (c) 2018 Daniel Grunwald
+//
+// 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.Pretty
+{
+ // Note: for null coalescing without assignment (??), see
+ // LiftedOperators.cs, NullPropagation.cs and ThrowExpressions.cs.
+ internal class NullCoalescingAssign
+ {
+ private class MyClass
+ {
+ public static string? StaticField1;
+ public static int? StaticField2;
+ public string? InstanceField1;
+ public int? InstanceField2;
+
+ public static string? StaticProperty1 { get; set; }
+ public static int? StaticProperty2 { get; set; }
+
+ public string? InstanceProperty1 { get; set; }
+ public int? InstanceProperty2 { get; set; }
+
+ public string? this[string name] {
+ get {
+ return null;
+ }
+ set {
+ }
+ }
+
+ public int? this[int index] {
+ get {
+ return null;
+ }
+ set {
+ }
+ }
+ }
+
+ private struct MyStruct
+ {
+ public string? InstanceField1;
+ public int? InstanceField2;
+ public string? InstanceProperty1 { get; set; }
+ public int? InstanceProperty2 { get; set; }
+
+ public string? this[string name] {
+ get {
+ return null;
+ }
+ set {
+ }
+ }
+
+ public int? this[int index] {
+ get {
+ return null;
+ }
+ set {
+ }
+ }
+ }
+
+ private static T Get()
+ {
+ return default(T);
+ }
+
+ private static ref T GetRef()
+ {
+ throw null;
+ }
+
+ private static void Use(T t)
+ {
+ }
+
+ public static void Locals()
+ {
+ string? local1 = null;
+ int? local2 = null;
+ local1 ??= "Hello";
+ local2 ??= 42;
+ Use(local1 ??= "World");
+ Use(local2 ??= 42);
+ }
+
+ public static void StaticFields()
+ {
+ MyClass.StaticField1 ??= "Hello";
+ MyClass.StaticField2 ??= 42;
+ Use(MyClass.StaticField1 ??= "World");
+ Use(MyClass.StaticField2 ??= 42);
+ }
+
+ public static void InstanceFields()
+ {
+ Get().InstanceField1 ??= "Hello";
+ Get().InstanceField2 ??= 42;
+ Use(Get().InstanceField1 ??= "World");
+ Use(Get().InstanceField2 ??= 42);
+ }
+
+ public static void ArrayElements()
+ {
+ Get()[Get()] ??= "Hello";
+ Get()[Get()] ??= 42;
+ Use(Get()[Get()] ??= "World");
+ Use(Get()[Get()] ??= 42);
+ }
+
+ public static void InstanceProperties()
+ {
+ Get().InstanceProperty1 ??= "Hello";
+ Get().InstanceProperty2 ??= 42;
+ Use(Get().InstanceProperty1 ??= "World");
+ Use(Get().InstanceProperty2 ??= 42);
+ }
+
+ public static void StaticProperties()
+ {
+ MyClass.StaticProperty1 ??= "Hello";
+ MyClass.StaticProperty2 ??= 42;
+ Use(MyClass.StaticProperty1 ??= "World");
+ Use(MyClass.StaticProperty2 ??= 42);
+ }
+
+ public static void RefReturn()
+ {
+ GetRef() ??= "Hello";
+ GetRef() ??= 42;
+ Use(GetRef() ??= "World");
+ Use(GetRef() ??= 42);
+ }
+
+ public static void Dynamic()
+ {
+ Get().X ??= "Hello";
+ Get().Y ??= 42;
+ Use(Get().X ??= "Hello");
+ Use(Get().Y ??= 42);
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index 65849d73d..afe54be91 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -142,6 +142,7 @@ namespace ICSharpCode.Decompiler.CSharp
new DynamicIsEventAssignmentTransform(),
new TransformAssignment(), // inline and compound assignments
new NullCoalescingTransform(),
+ new NullCoalescingAssignTransform(),
new NullableLiftingStatementTransform(),
new NullPropagationStatementTransform(),
new TransformArrayInitializers(),
diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
index 4c038c621..9abc1444f 100644
--- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
@@ -3877,6 +3877,29 @@ namespace ICSharpCode.Decompiler.CSharp
.WithRR(rr);
}
+ protected internal override TranslatedExpression VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst, TranslationContext context)
+ {
+ ExpressionWithResolveResult target;
+ if (inst.TargetKind == CompoundTargetKind.Address)
+ {
+ target = LdObj(inst.Target, inst.Type);
+ }
+ else
+ {
+ target = Translate(inst.Target, inst.Type);
+ }
+ var fallback = Translate(inst.Value);
+ fallback = AdjustConstantExpressionToType(fallback, inst.Type);
+ var resultType = target.Type;
+ if (inst.CoalescingKind == NullCoalescingKind.NullableWithValueFallback)
+ {
+ resultType = NullableType.GetUnderlyingType(resultType);
+ }
+ return new AssignmentExpression(target, AssignmentOperatorType.NullCoalescing, fallback)
+ .WithILInstruction(inst)
+ .WithRR(new ResolveResult(resultType));
+ }
+
protected internal override TranslatedExpression VisitIfInstruction(IfInstruction inst, TranslationContext context)
{
var condition = TranslateCondition(inst.Condition);
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/AssignmentExpression.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/AssignmentExpression.cs
index 378b33bfa..c813813b5 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/AssignmentExpression.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/Expressions/AssignmentExpression.cs
@@ -51,6 +51,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public readonly static TokenRole BitwiseAndRole = new TokenRole("&=");
public readonly static TokenRole BitwiseOrRole = new TokenRole("|=");
public readonly static TokenRole ExclusiveOrRole = new TokenRole("^=");
+ public readonly static TokenRole NullCoalescingRole = new TokenRole("??=");
public AssignmentExpression()
{
@@ -138,6 +139,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return BitwiseOrRole;
case AssignmentOperatorType.ExclusiveOr:
return ExclusiveOrRole;
+ case AssignmentOperatorType.NullCoalescing:
+ return NullCoalescingRole;
default:
throw new NotSupportedException("Invalid value for AssignmentOperatorType");
}
@@ -175,6 +178,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return BinaryOperatorType.BitwiseOr;
case AssignmentOperatorType.ExclusiveOr:
return BinaryOperatorType.ExclusiveOr;
+ case AssignmentOperatorType.NullCoalescing:
+ return BinaryOperatorType.NullCoalescing;
default:
throw new NotSupportedException("Invalid value for AssignmentOperatorType");
}
@@ -275,6 +280,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
BitwiseOr,
/// left ^= right
ExclusiveOr,
+ /// left ??= right
+ NullCoalescing,
/// Any operator (for pattern matching)
Any
diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs
index 06d4a24d3..d6d61de3d 100644
--- a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs
+++ b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs
@@ -754,6 +754,11 @@ namespace ICSharpCode.Decompiler.FlowAnalysis
HandleBinaryWithOptionalEvaluation(inst, inst.ValueInst, inst.FallbackInst);
}
+ protected internal override void VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst)
+ {
+ HandleBinaryWithOptionalEvaluation(inst, inst.Target, inst.Value);
+ }
+
protected internal override void VisitDynamicLogicOperatorInstruction(DynamicLogicOperatorInstruction inst)
{
HandleBinaryWithOptionalEvaluation(inst, inst.Left, inst.Right);
diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
index 00cb53e3c..44636c37d 100644
--- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
+++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
@@ -110,6 +110,7 @@
+
diff --git a/ICSharpCode.Decompiler/IL/ILVariable.cs b/ICSharpCode.Decompiler/IL/ILVariable.cs
index 96ddf5c2b..a605a2297 100644
--- a/ICSharpCode.Decompiler/IL/ILVariable.cs
+++ b/ICSharpCode.Decompiler/IL/ILVariable.cs
@@ -574,6 +574,7 @@ namespace ICSharpCode.Decompiler.IL
///
/// Gets whether this variable occurs within the specified instruction.
+ /// Only detects direct usages, may incorrectly return false for variables that have aliases via ref-locals/pointers.
///
internal bool IsUsedWithin(ILInstruction inst)
{
@@ -588,6 +589,24 @@ namespace ICSharpCode.Decompiler.IL
}
return false;
}
+
+ ///
+ /// Gets whether this variable is used for a store within the specified instruction.
+ /// Only detects direct stores, may incorrect return false for variables that have their addresses taken.
+ ///
+ internal bool IsWrittenWithin(ILInstruction inst)
+ {
+ if (inst is IStoreInstruction iwvo && iwvo.Variable == this)
+ {
+ return true;
+ }
+ foreach (var child in inst.Children)
+ {
+ if (IsWrittenWithin(child))
+ return true;
+ }
+ return false;
+ }
}
public interface IInstructionWithVariableOperand
diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs
index 3bdf53ecd..e86233f04 100644
--- a/ICSharpCode.Decompiler/IL/Instructions.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions.cs
@@ -54,6 +54,8 @@ namespace ICSharpCode.Decompiler.IL
UserDefinedCompoundAssign,
/// Common instruction for dynamic compound assignments.
DynamicCompoundAssign,
+ /// Null coalescing compound assignment (??= in C#).
+ NullCoalescingCompoundAssign,
/// Bitwise NOT
BitNot,
/// Retrieves the RuntimeArgumentHandle.
@@ -64,7 +66,7 @@ namespace ICSharpCode.Decompiler.IL
Leave,
/// If statement / conditional expression. if (condition) trueExpr else falseExpr
IfInstruction,
- /// Null coalescing operator expression. if.notnull(valueInst, fallbackInst)
+ /// Null coalescing operator expression (?? in C#). if.notnull(valueInst, fallbackInst)
NullCoalescingInstruction,
/// Switch statement
SwitchInstruction,
@@ -1172,6 +1174,36 @@ namespace ICSharpCode.Decompiler.IL
}
}
namespace ICSharpCode.Decompiler.IL
+{
+ /// Null coalescing compound assignment (??= in C#).
+ public sealed partial class NullCoalescingCompoundAssign : CompoundAssignmentInstruction
+ {
+ IType type;
+ /// Returns the type operand.
+ public IType Type {
+ get { return type; }
+ set { type = value; InvalidateFlags(); }
+ }
+ public override void AcceptVisitor(ILVisitor visitor)
+ {
+ visitor.VisitNullCoalescingCompoundAssign(this);
+ }
+ public override T AcceptVisitor(ILVisitor visitor)
+ {
+ return visitor.VisitNullCoalescingCompoundAssign(this);
+ }
+ public override T AcceptVisitor(ILVisitor visitor, C context)
+ {
+ return visitor.VisitNullCoalescingCompoundAssign(this, context);
+ }
+ protected internal override bool PerformMatch(ILInstruction? other, ref Patterns.Match match)
+ {
+ var o = other as NullCoalescingCompoundAssign;
+ return o != null && type.Equals(o.type) && CoalescingKind == o.CoalescingKind && this.EvalMode == o.EvalMode && this.TargetKind == o.TargetKind && Target.PerformMatch(o.Target, ref match) && Value.PerformMatch(o.Value, ref match);
+ }
+ }
+}
+namespace ICSharpCode.Decompiler.IL
{
/// Bitwise NOT
public sealed partial class BitNot : UnaryInstruction
@@ -1443,7 +1475,7 @@ namespace ICSharpCode.Decompiler.IL
}
namespace ICSharpCode.Decompiler.IL
{
- /// Null coalescing operator expression. if.notnull(valueInst, fallbackInst)
+ /// Null coalescing operator expression (?? in C#). if.notnull(valueInst, fallbackInst)
public sealed partial class NullCoalescingInstruction : ILInstruction
{
public static readonly SlotInfo ValueInstSlot = new SlotInfo("ValueInst", canInlineInto: true);
@@ -7102,6 +7134,10 @@ namespace ICSharpCode.Decompiler.IL
{
Default(inst);
}
+ protected internal virtual void VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst)
+ {
+ Default(inst);
+ }
protected internal virtual void VisitBitNot(BitNot inst)
{
Default(inst);
@@ -7512,6 +7548,10 @@ namespace ICSharpCode.Decompiler.IL
{
return Default(inst);
}
+ protected internal virtual T VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst)
+ {
+ return Default(inst);
+ }
protected internal virtual T VisitBitNot(BitNot inst)
{
return Default(inst);
@@ -7922,6 +7962,10 @@ namespace ICSharpCode.Decompiler.IL
{
return Default(inst, context);
}
+ protected internal virtual T VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst, C context)
+ {
+ return Default(inst, context);
+ }
protected internal virtual T VisitBitNot(BitNot inst, C context)
{
return Default(inst, context);
@@ -8294,6 +8338,7 @@ namespace ICSharpCode.Decompiler.IL
"numeric.compound",
"user.compound",
"dynamic.compound",
+ "if.notnull.compound",
"bit.not",
"arglist",
"br",
diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt
index 071548ab3..f915ef41b 100644
--- a/ICSharpCode.Decompiler/IL/Instructions.tt
+++ b/ICSharpCode.Decompiler/IL/Instructions.tt
@@ -97,6 +97,14 @@
MatchCondition("this.TargetKind == o.TargetKind"),
MatchCondition("Target.PerformMatch(o.Target, ref match)"),
MatchCondition("Value.PerformMatch(o.Value, ref match)")),
+ new OpCode("if.notnull.compound", "Null coalescing compound assignment (??= in C#).",
+ CustomClassName("NullCoalescingCompoundAssign"), BaseClass("CompoundAssignmentInstruction"),
+ HasTypeOperand, CustomComputeFlags, CustomWriteTo, CustomConstructor,
+ MatchCondition("CoalescingKind == o.CoalescingKind"),
+ MatchCondition("this.EvalMode == o.EvalMode"),
+ MatchCondition("this.TargetKind == o.TargetKind"),
+ MatchCondition("Target.PerformMatch(o.Target, ref match)"),
+ MatchCondition("Value.PerformMatch(o.Value, ref match)")),
new OpCode("bit.not", "Bitwise NOT", Unary, CustomConstructor, MatchCondition("IsLifted == o.IsLifted && UnderlyingResultType == o.UnderlyingResultType")),
new OpCode("arglist", "Retrieves the RuntimeArgumentHandle.", NoArguments, ResultType("O")),
new OpCode("br", "Unconditional branch. goto target;",
@@ -112,7 +120,7 @@
new ChildInfo("trueInst"),
new ChildInfo("falseInst"),
}), CustomConstructor, CustomComputeFlags, CustomWriteTo),
- new OpCode("if.notnull", "Null coalescing operator expression. if.notnull(valueInst, fallbackInst)",
+ new OpCode("if.notnull", "Null coalescing operator expression (?? in C#). if.notnull(valueInst, fallbackInst)",
CustomClassName("NullCoalescingInstruction"),
CustomChildren(new []{
new ChildInfo("valueInst") { CanInlineInto = true },
diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
index 4e3a504f5..2faf22639 100644
--- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
@@ -20,6 +20,7 @@
using System;
using System.Diagnostics;
using System.Linq.Expressions;
+using System.Security.Claims;
using ICSharpCode.Decompiler.TypeSystem;
@@ -114,6 +115,9 @@ namespace ICSharpCode.Decompiler.IL
case CompoundTargetKind.Property:
output.Write(".property");
break;
+ case CompoundTargetKind.Dynamic:
+ output.Write(".dynamic");
+ break;
}
switch (EvalMode)
{
@@ -376,7 +380,7 @@ namespace ICSharpCode.Decompiler.IL
{
WriteILRange(output, options);
output.Write(OpCode);
- output.Write("." + Operation.ToString().ToLower());
+ output.Write("." + Operation.ToString().ToLowerInvariant());
DynamicInstruction.WriteBinderFlags(BinderFlags, output, options);
base.WriteSuffix(output);
output.Write(' ');
@@ -416,6 +420,57 @@ namespace ICSharpCode.Decompiler.IL
}
}
}
-}
+ public partial class NullCoalescingCompoundAssign : CompoundAssignmentInstruction
+ {
+ public readonly NullCoalescingKind CoalescingKind;
+
+ public NullCoalescingCompoundAssign(ILInstruction target,
+ CompoundTargetKind targetKind, NullCoalescingKind coalescingKind, ILInstruction value, IType type)
+ : base(OpCode.NullCoalescingCompoundAssign, CompoundEvalMode.EvaluatesToNewValue, target, targetKind, value)
+ {
+ this.CoalescingKind = coalescingKind;
+ this.type = type;
+ }
+
+ internal override bool CanInlineIntoSlot(int childIndex, ILInstruction expressionBeingMoved)
+ {
+ // Can inline into target slot, but not into the conditionally-executed value slot.
+ Debug.Assert(GetChildSlot(0) == TargetSlot);
+ if (childIndex == 0)
+ return base.CanInlineIntoSlot(childIndex, expressionBeingMoved);
+ else
+ return false;
+ }
+
+ public override StackType ResultType => Value.ResultType;
+
+ protected override InstructionFlags ComputeFlags()
+ {
+ // targetInst is always executed; valueInst (and the implicit store) only sometimes
+ return InstructionFlags.ControlFlow | Target.Flags
+ | SemanticHelper.CombineBranches(InstructionFlags.None, InstructionFlags.SideEffect | Value.Flags);
+ }
+
+ public override InstructionFlags DirectFlags {
+ get {
+ return InstructionFlags.SideEffect | InstructionFlags.ControlFlow;
+ }
+ }
+
+ public override void WriteTo(ITextOutput output, ILAstWritingOptions options)
+ {
+ WriteILRange(output, options);
+ output.Write("if.notnull.");
+ output.Write(CoalescingKind.ToString().ToLowerInvariant());
+ output.Write(".compound");
+ base.WriteSuffix(output);
+ output.Write('(');
+ Target.WriteTo(output, options);
+ output.Write(", ");
+ Value.WriteTo(output, options);
+ output.Write(')');
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
index c6ce6443a..2da5cbb9a 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
@@ -856,6 +856,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
}
+ protected internal override void VisitNullCoalescingInstruction(NullCoalescingInstruction inst)
+ {
+ base.VisitNullCoalescingInstruction(inst);
+ NullCoalescingAssignTransform.RunForExpression(inst, context);
+ }
+
protected internal override void VisitTryCatchHandler(TryCatchHandler inst)
{
base.VisitTryCatchHandler(inst);
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
index e66b993db..b2fb06182 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
@@ -678,7 +678,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true; // inline into dynamic compound assignments
break;
case OpCode.DynamicCompoundAssign:
+ case OpCode.NullCoalescingCompoundAssign:
return true;
+ case OpCode.LdFlda:
+ if (parent.Parent.OpCode == OpCode.NullCoalescingCompoundAssign)
+ return true;
+ break;
case OpCode.GetPinnableReference:
case OpCode.LocAllocSpan:
return true; // inline size-expressions into localloc.span
diff --git a/ICSharpCode.Decompiler/IL/Transforms/NullCoalescingAssignTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NullCoalescingAssignTransform.cs
new file mode 100644
index 000000000..ef196842f
--- /dev/null
+++ b/ICSharpCode.Decompiler/IL/Transforms/NullCoalescingAssignTransform.cs
@@ -0,0 +1,285 @@
+// Copyright (c) 2025 Daniel Grunwald
+//
+// 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.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using ICSharpCode.Decompiler.TypeSystem;
+
+namespace ICSharpCode.Decompiler.IL.Transforms
+{
+ ///
+ /// Transform for constructing the NullCoalescingCompoundAssign (if.notnull.compound(a,b), or in C#: ??=)
+ ///
+ public class NullCoalescingAssignTransform : IStatementTransform
+ {
+ ///
+ /// Run as a expression-transform for a NullCoalescingInstruction that was already detected.
+ ///
+ /// if.notnull(load(x), store(x, fallback))
+ /// =>
+ /// if.notnull.compound(x, fallback)
+ ///
+ public static bool RunForExpression(NullCoalescingInstruction nci, StatementTransformContext context)
+ {
+ if (nci.Kind != NullCoalescingKind.Ref)
+ return false;
+ ILInstruction load = nci.ValueInst;
+ ILInstruction store = nci.FallbackInst;
+
+ if (!TransformAssignment.IsCompoundStore(store, out var storeType, out var rhsValue, context.TypeSystem))
+ return false;
+ if (!TransformAssignment.IsMatchingCompoundLoad(load, store, out var target, out var targetKind, out var finalizeMatch))
+ return false;
+
+ context.Step("Null coalescing assignment: ref types (expression transform)", nci);
+ nci.ReplaceWith(new NullCoalescingCompoundAssign(target, targetKind, nci.Kind, rhsValue, storeType).WithILRange(nci));
+ return true;
+ }
+
+ public void Run(Block block, int pos, StatementTransformContext context)
+ {
+ if (TransformRefTypes(block, pos, context))
+ return;
+ if (TransformValueTypes(block, pos, context))
+ return;
+ }
+
+ ///
+ /// case 1: ref local of class type, return value of ??= not used.
+ ///
+ /// if (comp.o(ldobj System.Object(ldloc reference) == ldnull)) Block {
+ /// stobj System.Object(ldloc reference, ldstr "Hello")
+ /// }
+ /// =>
+ /// if.notnull.ref.compound.address.new(ldloc reference, ldstr "Hello")
+ ///
+ /// Note: case 3 (class type and return value of ??= is used) is handled as ExpressionTransform.
+ ///
+ static bool TransformRefTypes(Block block, int pos, StatementTransformContext context)
+ {
+ if (!block.Instructions[pos].MatchIfInstruction(out var condition, out var trueInst))
+ return false;
+ trueInst = Block.Unwrap(trueInst);
+ if (!condition.MatchCompEqualsNull(out var loadInCondition))
+ return false;
+ if (!TransformAssignment.IsCompoundStore(trueInst, out var storeType, out var rhsValue, context.TypeSystem))
+ return false;
+ if (!TransformAssignment.IsMatchingCompoundLoad(loadInCondition, trueInst, out var target, out var targetKind, out var finalizeMatch))
+ return false;
+ context.Step("Null coalescing assignment: ref types", condition);
+ var result = new NullCoalescingCompoundAssign(target, targetKind, NullCoalescingKind.Ref, rhsValue, storeType);
+ result.AddILRange(block.Instructions[pos]);
+ result.AddILRange(trueInst);
+ block.Instructions[pos] = result;
+ finalizeMatch?.Invoke(context);
+ return true;
+ }
+
+ ///
+ /// case 2: ref local of value type, return value of ??= not used.
+ ///
+ /// stloc temp1(call GetValueOrDefault(ldloc reference))
+ /// if (logic.not(call get_HasValue(ldloc reference))) Block {
+ /// stloc temp2(ldc.i4 42)
+ /// stobj System.Nullable`1[[System.Int32]](ldloc reference, newobj Nullable..ctor(ldloc temp2))
+ /// }
+ /// =>
+ /// if.notnull.nullablewithvaluefallback.compound.address.new(ldloc reference, ldc.i4 42)
+ ///
+ ///
+ /// case 4: ref local of value type, return value of ??= used.
+ ///
+ /// stloc temp1(call GetValueOrDefault(ldloc reference))
+ /// if (logic.not(call get_HasValue(ldloc reference))) Block {
+ /// stloc temp2(ldc.i4 42)
+ /// stobj System.Nullable`1[[System.Int32]](ldloc reference, newobj Nullable..ctor(ldloc temp2))
+ /// stloc resultVar(ldloc temp2)
+ /// } else Block {
+ /// stloc resultVar(ldloc temp1)
+ /// }
+ /// =>
+ /// if.notnull.nullablewithvaluefallback.compound.address.new(ldloc reference, ldc.i4 42)
+ ///
+ static bool TransformValueTypes(Block block, int pos, StatementTransformContext context)
+ {
+ // stloc valueOrDefault(call GetValueOrDefault(ldloc reference))
+ if (!block.Instructions[pos].MatchStLoc(out var temp1, out var getValueOrDefaultCall))
+ return false;
+ if (!(temp1.IsSingleDefinition && temp1.LoadCount <= 1))
+ return false;
+ if (!NullableLiftingTransform.MatchGetValueOrDefault(getValueOrDefaultCall, out ILInstruction load1))
+ return false;
+
+ // if (logic.not(call get_HasValue(ldloc reference)))
+ if (!block.Instructions[pos + 1].MatchIfInstructionPositiveCondition(out var condition, out var hasValueInst, out var noValueInst))
+ return false;
+ if (!NullableLiftingTransform.MatchHasValueCall(condition, out ILInstruction load2))
+ return false;
+
+ // Check the else block (HasValue returned true)
+ hasValueInst = Block.Unwrap(hasValueInst);
+ ILVariable? resultVar = null;
+ if (temp1.LoadCount == 0)
+ {
+ // resulting value not used: hasValueInst must no Nop
+ if (!hasValueInst.MatchNop())
+ return false;
+ }
+ else if (temp1.LoadCount == 1)
+ {
+ // stloc resultVar(ldloc temp1)
+ if (!hasValueInst.MatchStLoc(out resultVar, out var temp1Load))
+ return false;
+ if (!temp1Load.MatchLdLoc(temp1))
+ return false;
+ }
+
+ // Check the then block (HasValue returned false)
+ if (!CheckNoValueBlock(noValueInst as Block, resultVar, context, out var storeInst, out var storeType, out var rhsValue))
+ return false;
+ // Checks loads for consistency with the storeInst:
+ if (!IsMatchingCompoundLdloca(load1, storeInst, rhsValue, out var target, out var targetKind, out var needToRecombineLocals1))
+ return false;
+ if (!IsMatchingCompoundLdloca(load2, storeInst, rhsValue, out target, out targetKind, out var needToRecombineLocals2))
+ return false;
+ Debug.Assert(needToRecombineLocals1 == needToRecombineLocals2);
+
+ context.Step("Null coalescing assignment: value types", condition);
+ ILInstruction result = new NullCoalescingCompoundAssign(target, targetKind, NullCoalescingKind.NullableWithValueFallback, rhsValue, storeType);
+ result.AddILRange(block.Instructions[pos]);
+ result.AddILRange(block.Instructions[pos + 1]);
+ if (resultVar != null)
+ {
+ result = new StLoc(resultVar, result);
+ context.RequestRerun(); // The new StLoc could allow inlining.
+ }
+ block.Instructions[pos] = result;
+ block.Instructions.RemoveAt(pos + 1);
+ if (needToRecombineLocals1 || needToRecombineLocals2)
+ {
+ Debug.Assert(((LdLoca)load1).Variable == ((LdLoca)load2).Variable);
+ Debug.Assert(needToRecombineLocals1 == needToRecombineLocals2);
+ context.Function.RecombineVariables(((LdLoca)load1).Variable, ((StLoc)storeInst).Variable);
+ }
+ return true;
+ }
+
+ /// Called with a block like: Block {
+ /// stloc temp2(ldc.i4 42)
+ /// stobj Nullable[T](ldloc reference, newobj Nullable..ctor(ldloc temp2))
+ /// stloc resultVar(ldloc temp2)
+ /// }
+ static bool CheckNoValueBlock(Block? block, ILVariable? resultVar, StatementTransformContext context,
+ [NotNullWhen(true)] out ILInstruction? storeInst, [NotNullWhen(true)] out IType? storeType, [NotNullWhen(true)] out ILInstruction? rhsValue)
+ {
+ storeInst = null;
+ storeType = null;
+ rhsValue = null;
+ if (block == null)
+ return false;
+
+ int pos = 0;
+ // stloc temp2(ldc.i4 42)
+ if (block.Instructions.ElementAtOrDefault(pos) is not StLoc temp2Store)
+ return false;
+ if (!(temp2Store.Variable.IsSingleDefinition && temp2Store.Variable.LoadCount == (resultVar != null ? 2 : 1)))
+ return false;
+ rhsValue = temp2Store.Value;
+ pos += 1;
+
+ // stobj Nullable[T](ldloc reference, newobj Nullable..ctor(ldloc temp2))
+ storeInst = block.Instructions.ElementAtOrDefault(pos);
+ if (!TransformAssignment.IsCompoundStore(storeInst, out storeType, out var storeValue, context.TypeSystem))
+ return false;
+ if (!NullableLiftingTransform.MatchNullableCtor(storeValue, out _, out var temp2Load))
+ return false;
+ if (!temp2Load.MatchLdLoc(temp2Store.Variable))
+ return false;
+ pos += 1;
+
+ if (resultVar != null)
+ {
+ // stloc resultVar(ldloc temp2)
+ var resultStore = block.Instructions.ElementAtOrDefault(pos);
+ if (!(resultStore != null && resultStore.MatchStLoc(resultVar, out temp2Load)))
+ return false;
+ if (!temp2Load.MatchLdLoc(temp2Store.Variable))
+ return false;
+ pos++;
+ }
+ return pos == block.Instructions.Count;
+ }
+
+ static bool IsMatchingCompoundLdloca(ILInstruction load, ILInstruction store, ILInstruction reorderedInst, [NotNullWhen(true)] out ILInstruction? target, out CompoundTargetKind targetKind, out bool needToRecombineLocals)
+ {
+ needToRecombineLocals = false;
+ // Store was already validated by TransformAssignment.IsCompoundStore().
+ // This function acts similar to TransformAssignment.IsMatchingCompoundLoad(),
+ // except that it tests that `load` loads the address that was used in `store`.
+ // 'reorderedInst' is the rhsValue, which originally occurs between the load and store, but
+ // our transform will move the store's address-load in front of 'reorderedInst'.
+ if (store is StObj stobj)
+ {
+ target = stobj.Target;
+ targetKind = CompoundTargetKind.Address;
+ return IsSameAddress(load, target, reorderedInst);
+ }
+ else if (store is StLoc stloc && load is LdLoca ldloca && ILVariableEqualityComparer.Instance.Equals(ldloca.Variable, stloc.Variable))
+ {
+ target = load;
+ targetKind = CompoundTargetKind.Address;
+ needToRecombineLocals = ldloca.Variable != stloc.Variable;
+ return true;
+ }
+ target = null;
+ targetKind = default;
+ return false;
+ }
+
+ static bool IsSameAddress(ILInstruction addr1, ILInstruction addr2, ILInstruction reorderedInst)
+ {
+ if (addr1.MatchLdLoc(out var refVar))
+ {
+ return refVar.AddressCount == 0
+ && addr2.MatchLdLoc(refVar)
+ && !refVar.IsWrittenWithin(reorderedInst);
+ }
+ else if (addr1.MatchLdLoca(out var targetVar))
+ {
+ return addr2.MatchLdLoca(targetVar);
+ }
+ else if (addr1.MatchLdFlda(out var instance1, out var field1) && addr2.MatchLdFlda(out var instance2, out var field2))
+ {
+ return field1.Equals(field2) && IsSameAddress(instance1, instance2, reorderedInst);
+ }
+ else if (addr1.MatchLdsFlda(out field1) && addr2.MatchLdsFlda(out field2))
+ {
+ return field1.Equals(field2);
+ }
+ return false;
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
index 1b6b1b44f..9cdf87b65 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
@@ -678,7 +678,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
///
/// Every IsCompoundStore() call should be followed by an IsMatchingCompoundLoad() call.
///
- static bool IsCompoundStore(ILInstruction inst, out IType storeType,
+ internal static bool IsCompoundStore(ILInstruction inst, out IType storeType,
out ILInstruction value, ICompilation compilation)
{
value = null;
@@ -766,7 +766,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
///
/// Instruction preceding the load.
///
- static bool IsMatchingCompoundLoad(ILInstruction load, ILInstruction store,
+ internal static bool IsMatchingCompoundLoad(ILInstruction load, ILInstruction store,
out ILInstruction target, out CompoundTargetKind targetKind,
out Action finalizeMatch,
ILVariable forbiddenVariable = null,