Browse Source

#2552: Initial support for null coalescing assignment (`??=`).

null-coalescing-assignment
Daniel Grunwald 4 months ago
parent
commit
71d3120b94
  1. 1
      .vscode/settings.json
  2. 1
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  3. 6
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  4. 161
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullCoalescingAssign.cs
  5. 1
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  6. 23
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  7. 7
      ICSharpCode.Decompiler/CSharp/Syntax/Expressions/AssignmentExpression.cs
  8. 5
      ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs
  9. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  10. 19
      ICSharpCode.Decompiler/IL/ILVariable.cs
  11. 49
      ICSharpCode.Decompiler/IL/Instructions.cs
  12. 10
      ICSharpCode.Decompiler/IL/Instructions.tt
  13. 57
      ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs
  14. 6
      ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
  15. 5
      ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
  16. 285
      ICSharpCode.Decompiler/IL/Transforms/NullCoalescingAssignTransform.cs
  17. 4
      ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs

1
.vscode/settings.json vendored

@ -1,5 +1,4 @@
{ {
"dotnet-test-explorer.testProjectPath": "*.Tests/*Tests.csproj",
"files.exclude": { "files.exclude": {
"ILSpy-tests/**": true "ILSpy-tests/**": true
}, },

1
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -152,6 +152,7 @@
<Compile Include="TestCases\ILPretty\Issue3524.cs" /> <Compile Include="TestCases\ILPretty\Issue3524.cs" />
<Compile Include="TestCases\Pretty\ExpandParamsArgumentsDisabled.cs" /> <Compile Include="TestCases\Pretty\ExpandParamsArgumentsDisabled.cs" />
<Compile Include="TestCases\Pretty\ExtensionProperties.cs" /> <Compile Include="TestCases\Pretty\ExtensionProperties.cs" />
<Compile Include="TestCases\Pretty\NullCoalescingAssign.cs" />
<None Include="TestCases\ILPretty\Issue3504.cs" /> <None Include="TestCases\ILPretty\Issue3504.cs" />
<Compile Include="TestCases\ILPretty\MonoFixed.cs" /> <Compile Include="TestCases\ILPretty\MonoFixed.cs" />
<Compile Include="TestCases\Pretty\Comparisons.cs" /> <Compile Include="TestCases\Pretty\Comparisons.cs" />

6
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -539,6 +539,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions); await RunForLibrary(cscOptions: cscOptions);
} }
[Test]
public async Task NullCoalescingAssign([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test] [Test]
public async Task StringInterpolation([ValueSource(nameof(roslynOnlyWithNet40Options))] CompilerOptions cscOptions) public async Task StringInterpolation([ValueSource(nameof(roslynOnlyWithNet40Options))] CompilerOptions cscOptions)
{ {

161
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<T>()
{
return default(T);
}
private static ref T GetRef<T>()
{
throw null;
}
private static void Use<T>(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<MyClass>().InstanceField1 ??= "Hello";
Get<MyClass>().InstanceField2 ??= 42;
Use(Get<MyClass>().InstanceField1 ??= "World");
Use(Get<MyClass>().InstanceField2 ??= 42);
}
public static void ArrayElements()
{
Get<string[]>()[Get<int>()] ??= "Hello";
Get<int?[]>()[Get<int>()] ??= 42;
Use(Get<string[]>()[Get<int>()] ??= "World");
Use(Get<int?[]>()[Get<int>()] ??= 42);
}
public static void InstanceProperties()
{
Get<MyClass>().InstanceProperty1 ??= "Hello";
Get<MyClass>().InstanceProperty2 ??= 42;
Use(Get<MyClass>().InstanceProperty1 ??= "World");
Use(Get<MyClass>().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<string>() ??= "Hello";
GetRef<int?>() ??= 42;
Use(GetRef<string>() ??= "World");
Use(GetRef<int?>() ??= 42);
}
public static void Dynamic()
{
Get<dynamic>().X ??= "Hello";
Get<dynamic>().Y ??= 42;
Use(Get<dynamic>().X ??= "Hello");
Use(Get<dynamic>().Y ??= 42);
}
}
}

1
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -142,6 +142,7 @@ namespace ICSharpCode.Decompiler.CSharp
new DynamicIsEventAssignmentTransform(), new DynamicIsEventAssignmentTransform(),
new TransformAssignment(), // inline and compound assignments new TransformAssignment(), // inline and compound assignments
new NullCoalescingTransform(), new NullCoalescingTransform(),
new NullCoalescingAssignTransform(),
new NullableLiftingStatementTransform(), new NullableLiftingStatementTransform(),
new NullPropagationStatementTransform(), new NullPropagationStatementTransform(),
new TransformArrayInitializers(), new TransformArrayInitializers(),

23
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -3877,6 +3877,29 @@ namespace ICSharpCode.Decompiler.CSharp
.WithRR(rr); .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) protected internal override TranslatedExpression VisitIfInstruction(IfInstruction inst, TranslationContext context)
{ {
var condition = TranslateCondition(inst.Condition); var condition = TranslateCondition(inst.Condition);

7
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 BitwiseAndRole = new TokenRole("&=");
public readonly static TokenRole BitwiseOrRole = new TokenRole("|="); public readonly static TokenRole BitwiseOrRole = new TokenRole("|=");
public readonly static TokenRole ExclusiveOrRole = new TokenRole("^="); public readonly static TokenRole ExclusiveOrRole = new TokenRole("^=");
public readonly static TokenRole NullCoalescingRole = new TokenRole("??=");
public AssignmentExpression() public AssignmentExpression()
{ {
@ -138,6 +139,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return BitwiseOrRole; return BitwiseOrRole;
case AssignmentOperatorType.ExclusiveOr: case AssignmentOperatorType.ExclusiveOr:
return ExclusiveOrRole; return ExclusiveOrRole;
case AssignmentOperatorType.NullCoalescing:
return NullCoalescingRole;
default: default:
throw new NotSupportedException("Invalid value for AssignmentOperatorType"); throw new NotSupportedException("Invalid value for AssignmentOperatorType");
} }
@ -175,6 +178,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return BinaryOperatorType.BitwiseOr; return BinaryOperatorType.BitwiseOr;
case AssignmentOperatorType.ExclusiveOr: case AssignmentOperatorType.ExclusiveOr:
return BinaryOperatorType.ExclusiveOr; return BinaryOperatorType.ExclusiveOr;
case AssignmentOperatorType.NullCoalescing:
return BinaryOperatorType.NullCoalescing;
default: default:
throw new NotSupportedException("Invalid value for AssignmentOperatorType"); throw new NotSupportedException("Invalid value for AssignmentOperatorType");
} }
@ -275,6 +280,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
BitwiseOr, BitwiseOr,
/// <summary>left ^= right</summary> /// <summary>left ^= right</summary>
ExclusiveOr, ExclusiveOr,
/// <summary>left ??= right</summary>
NullCoalescing,
/// <summary>Any operator (for pattern matching)</summary> /// <summary>Any operator (for pattern matching)</summary>
Any Any

5
ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs

@ -754,6 +754,11 @@ namespace ICSharpCode.Decompiler.FlowAnalysis
HandleBinaryWithOptionalEvaluation(inst, inst.ValueInst, inst.FallbackInst); 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) protected internal override void VisitDynamicLogicOperatorInstruction(DynamicLogicOperatorInstruction inst)
{ {
HandleBinaryWithOptionalEvaluation(inst, inst.Left, inst.Right); HandleBinaryWithOptionalEvaluation(inst, inst.Left, inst.Right);

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -110,6 +110,7 @@
<Compile Include="Disassembler\SortByNameProcessor.cs" /> <Compile Include="Disassembler\SortByNameProcessor.cs" />
<Compile Include="Humanizer\StringHumanizeExtensions.cs" /> <Compile Include="Humanizer\StringHumanizeExtensions.cs" />
<Compile Include="IL\Transforms\InlineArrayTransform.cs" /> <Compile Include="IL\Transforms\InlineArrayTransform.cs" />
<Compile Include="IL\Transforms\NullCoalescingAssignTransform.cs" />
<Compile Include="IL\Transforms\RemoveUnconstrainedGenericReferenceTypeCheck.cs" /> <Compile Include="IL\Transforms\RemoveUnconstrainedGenericReferenceTypeCheck.cs" />
<Compile Include="Metadata\MetadataFile.cs" /> <Compile Include="Metadata\MetadataFile.cs" />
<Compile Include="Metadata\ModuleReferenceMetadata.cs" /> <Compile Include="Metadata\ModuleReferenceMetadata.cs" />

19
ICSharpCode.Decompiler/IL/ILVariable.cs

@ -574,6 +574,7 @@ namespace ICSharpCode.Decompiler.IL
/// <summary> /// <summary>
/// Gets whether this variable occurs within the specified instruction. /// 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.
/// </summary> /// </summary>
internal bool IsUsedWithin(ILInstruction inst) internal bool IsUsedWithin(ILInstruction inst)
{ {
@ -588,6 +589,24 @@ namespace ICSharpCode.Decompiler.IL
} }
return false; return false;
} }
/// <summary>
/// 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.
/// </summary>
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 public interface IInstructionWithVariableOperand

49
ICSharpCode.Decompiler/IL/Instructions.cs

@ -54,6 +54,8 @@ namespace ICSharpCode.Decompiler.IL
UserDefinedCompoundAssign, UserDefinedCompoundAssign,
/// <summary>Common instruction for dynamic compound assignments.</summary> /// <summary>Common instruction for dynamic compound assignments.</summary>
DynamicCompoundAssign, DynamicCompoundAssign,
/// <summary>Null coalescing compound assignment (??= in C#).</summary>
NullCoalescingCompoundAssign,
/// <summary>Bitwise NOT</summary> /// <summary>Bitwise NOT</summary>
BitNot, BitNot,
/// <summary>Retrieves the RuntimeArgumentHandle.</summary> /// <summary>Retrieves the RuntimeArgumentHandle.</summary>
@ -64,7 +66,7 @@ namespace ICSharpCode.Decompiler.IL
Leave, Leave,
/// <summary>If statement / conditional expression. <c>if (condition) trueExpr else falseExpr</c></summary> /// <summary>If statement / conditional expression. <c>if (condition) trueExpr else falseExpr</c></summary>
IfInstruction, IfInstruction,
/// <summary>Null coalescing operator expression. <c>if.notnull(valueInst, fallbackInst)</c></summary> /// <summary>Null coalescing operator expression (?? in C#). <c>if.notnull(valueInst, fallbackInst)</c></summary>
NullCoalescingInstruction, NullCoalescingInstruction,
/// <summary>Switch statement</summary> /// <summary>Switch statement</summary>
SwitchInstruction, SwitchInstruction,
@ -1172,6 +1174,36 @@ namespace ICSharpCode.Decompiler.IL
} }
} }
namespace ICSharpCode.Decompiler.IL namespace ICSharpCode.Decompiler.IL
{
/// <summary>Null coalescing compound assignment (??= in C#).</summary>
public sealed partial class NullCoalescingCompoundAssign : CompoundAssignmentInstruction
{
IType type;
/// <summary>Returns the type operand.</summary>
public IType Type {
get { return type; }
set { type = value; InvalidateFlags(); }
}
public override void AcceptVisitor(ILVisitor visitor)
{
visitor.VisitNullCoalescingCompoundAssign(this);
}
public override T AcceptVisitor<T>(ILVisitor<T> visitor)
{
return visitor.VisitNullCoalescingCompoundAssign(this);
}
public override T AcceptVisitor<C, T>(ILVisitor<C, T> 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
{ {
/// <summary>Bitwise NOT</summary> /// <summary>Bitwise NOT</summary>
public sealed partial class BitNot : UnaryInstruction public sealed partial class BitNot : UnaryInstruction
@ -1443,7 +1475,7 @@ namespace ICSharpCode.Decompiler.IL
} }
namespace ICSharpCode.Decompiler.IL namespace ICSharpCode.Decompiler.IL
{ {
/// <summary>Null coalescing operator expression. <c>if.notnull(valueInst, fallbackInst)</c></summary> /// <summary>Null coalescing operator expression (?? in C#). <c>if.notnull(valueInst, fallbackInst)</c></summary>
public sealed partial class NullCoalescingInstruction : ILInstruction public sealed partial class NullCoalescingInstruction : ILInstruction
{ {
public static readonly SlotInfo ValueInstSlot = new SlotInfo("ValueInst", canInlineInto: true); public static readonly SlotInfo ValueInstSlot = new SlotInfo("ValueInst", canInlineInto: true);
@ -7102,6 +7134,10 @@ namespace ICSharpCode.Decompiler.IL
{ {
Default(inst); Default(inst);
} }
protected internal virtual void VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst)
{
Default(inst);
}
protected internal virtual void VisitBitNot(BitNot inst) protected internal virtual void VisitBitNot(BitNot inst)
{ {
Default(inst); Default(inst);
@ -7512,6 +7548,10 @@ namespace ICSharpCode.Decompiler.IL
{ {
return Default(inst); return Default(inst);
} }
protected internal virtual T VisitNullCoalescingCompoundAssign(NullCoalescingCompoundAssign inst)
{
return Default(inst);
}
protected internal virtual T VisitBitNot(BitNot inst) protected internal virtual T VisitBitNot(BitNot inst)
{ {
return Default(inst); return Default(inst);
@ -7922,6 +7962,10 @@ namespace ICSharpCode.Decompiler.IL
{ {
return Default(inst, context); 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) protected internal virtual T VisitBitNot(BitNot inst, C context)
{ {
return Default(inst, context); return Default(inst, context);
@ -8294,6 +8338,7 @@ namespace ICSharpCode.Decompiler.IL
"numeric.compound", "numeric.compound",
"user.compound", "user.compound",
"dynamic.compound", "dynamic.compound",
"if.notnull.compound",
"bit.not", "bit.not",
"arglist", "arglist",
"br", "br",

10
ICSharpCode.Decompiler/IL/Instructions.tt

@ -97,6 +97,14 @@
MatchCondition("this.TargetKind == o.TargetKind"), MatchCondition("this.TargetKind == o.TargetKind"),
MatchCondition("Target.PerformMatch(o.Target, ref match)"), MatchCondition("Target.PerformMatch(o.Target, ref match)"),
MatchCondition("Value.PerformMatch(o.Value, 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("bit.not", "Bitwise NOT", Unary, CustomConstructor, MatchCondition("IsLifted == o.IsLifted && UnderlyingResultType == o.UnderlyingResultType")),
new OpCode("arglist", "Retrieves the RuntimeArgumentHandle.", NoArguments, ResultType("O")), new OpCode("arglist", "Retrieves the RuntimeArgumentHandle.", NoArguments, ResultType("O")),
new OpCode("br", "Unconditional branch. <c>goto target;</c>", new OpCode("br", "Unconditional branch. <c>goto target;</c>",
@ -112,7 +120,7 @@
new ChildInfo("trueInst"), new ChildInfo("trueInst"),
new ChildInfo("falseInst"), new ChildInfo("falseInst"),
}), CustomConstructor, CustomComputeFlags, CustomWriteTo), }), CustomConstructor, CustomComputeFlags, CustomWriteTo),
new OpCode("if.notnull", "Null coalescing operator expression. <c>if.notnull(valueInst, fallbackInst)</c>", new OpCode("if.notnull", "Null coalescing operator expression (?? in C#). <c>if.notnull(valueInst, fallbackInst)</c>",
CustomClassName("NullCoalescingInstruction"), CustomClassName("NullCoalescingInstruction"),
CustomChildren(new []{ CustomChildren(new []{
new ChildInfo("valueInst") { CanInlineInto = true }, new ChildInfo("valueInst") { CanInlineInto = true },

57
ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs

@ -20,6 +20,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Security.Claims;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
@ -114,6 +115,9 @@ namespace ICSharpCode.Decompiler.IL
case CompoundTargetKind.Property: case CompoundTargetKind.Property:
output.Write(".property"); output.Write(".property");
break; break;
case CompoundTargetKind.Dynamic:
output.Write(".dynamic");
break;
} }
switch (EvalMode) switch (EvalMode)
{ {
@ -376,7 +380,7 @@ namespace ICSharpCode.Decompiler.IL
{ {
WriteILRange(output, options); WriteILRange(output, options);
output.Write(OpCode); output.Write(OpCode);
output.Write("." + Operation.ToString().ToLower()); output.Write("." + Operation.ToString().ToLowerInvariant());
DynamicInstruction.WriteBinderFlags(BinderFlags, output, options); DynamicInstruction.WriteBinderFlags(BinderFlags, output, options);
base.WriteSuffix(output); base.WriteSuffix(output);
output.Write(' '); 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(')');
}
}
}

6
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) protected internal override void VisitTryCatchHandler(TryCatchHandler inst)
{ {
base.VisitTryCatchHandler(inst); base.VisitTryCatchHandler(inst);

5
ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs

@ -678,7 +678,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return true; // inline into dynamic compound assignments return true; // inline into dynamic compound assignments
break; break;
case OpCode.DynamicCompoundAssign: case OpCode.DynamicCompoundAssign:
case OpCode.NullCoalescingCompoundAssign:
return true; return true;
case OpCode.LdFlda:
if (parent.Parent.OpCode == OpCode.NullCoalescingCompoundAssign)
return true;
break;
case OpCode.GetPinnableReference: case OpCode.GetPinnableReference:
case OpCode.LocAllocSpan: case OpCode.LocAllocSpan:
return true; // inline size-expressions into localloc.span return true; // inline size-expressions into localloc.span

285
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
{
/// <summary>
/// Transform for constructing the NullCoalescingCompoundAssign (if.notnull.compound(a,b), or in C#: ??=)
/// </summary>
public class NullCoalescingAssignTransform : IStatementTransform
{
/// <summary>
/// Run as a expression-transform for a NullCoalescingInstruction that was already detected.
///
/// if.notnull(load(x), store(x, fallback))
/// =>
/// if.notnull.compound(x, fallback)
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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)
/// </summary>
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;
}
}
}

4
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. /// Every IsCompoundStore() call should be followed by an IsMatchingCompoundLoad() call.
/// </remarks> /// </remarks>
static bool IsCompoundStore(ILInstruction inst, out IType storeType, internal static bool IsCompoundStore(ILInstruction inst, out IType storeType,
out ILInstruction value, ICompilation compilation) out ILInstruction value, ICompilation compilation)
{ {
value = null; value = null;
@ -766,7 +766,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// <param name="previousInstruction"> /// <param name="previousInstruction">
/// Instruction preceding the load. /// Instruction preceding the load.
/// </param> /// </param>
static bool IsMatchingCompoundLoad(ILInstruction load, ILInstruction store, internal static bool IsMatchingCompoundLoad(ILInstruction load, ILInstruction store,
out ILInstruction target, out CompoundTargetKind targetKind, out ILInstruction target, out CompoundTargetKind targetKind,
out Action<ILTransformContext> finalizeMatch, out Action<ILTransformContext> finalizeMatch,
ILVariable forbiddenVariable = null, ILVariable forbiddenVariable = null,

Loading…
Cancel
Save