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,