diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs index f3a827cc8..e03c5468e 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs @@ -58,7 +58,43 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty private static void Test(ref S byRef) { } + + public void CallOnThis() + { + // distinguish calls on 'this' from calls on a copy of 'this' + SetField(); + S s = this; + s.SetField(); + } + } + +#if CS72 + public readonly struct R + { + public readonly int Field; + + public int Property { + get { + return Field; + } + set { + Console.WriteLine("Setter on readonly struct"); + } + } + + public void Method() + { + } + + public void CallOnThis() + { + // distinguish calls on 'this' from calls on a copy of 'this' + Method(); + R r = this; + r.Method(); + } } +#endif #if ROSLYN && OPT // Roslyn optimizes out the explicit default-initialization @@ -69,6 +105,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty private static S MutableS = default(S); #endif private static volatile int VolatileInt; +#if CS72 + private static readonly R ReadOnlyR; + private static R MutableR; +#endif public static void CallMethodViaField() { @@ -76,6 +116,14 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty MutableS.SetField(); S mutableS = MutableS; mutableS.SetField(); + +#if CS72 + ReadOnlyR.Method(); + R readOnlyR = ReadOnlyR; + readOnlyR.Method(); + R mutableR = MutableR; + mutableR.Method(); +#endif } #if !(ROSLYN && OPT) || COPY_PROPAGATION_FIXED @@ -194,15 +242,26 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } + public static T Get() + { + return default(T); + } + public static void CallOnTemporary() { // Method can be called directly on temporaries - //InitObj2().MethodCalls(); + Get().MethodCalls(); // Setting a property requires a temporary to avoid // CS1612 Cannot modify the return value of 'InitObj2()' because it is not a variable - S s = InitObj2(); + S s = Get(); s.Property = 1; + +#if CS72 + Get().Method(); + R r = Get(); + r.Property = 2; +#endif } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index a51ebf7a2..de131891b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -264,7 +264,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms // Thus, we have to ensure we're operating on an r-value. // Additionally, we cannot inline in cases where the C# compiler prohibits the direct use // of the rvalue (e.g. M(ref (MyStruct)obj); is invalid). - return IsUsedAsThisPointerInCall(loadInst) && !IsLValue(inlinedExpression); + if (!IsUsedAsThisPointerInCall(loadInst)) + return false; + switch (ClassifyExpression(inlinedExpression)) { + case ExpressionClassification.RValue: + // For struct method calls on rvalues, the C# compiler always generates temporaries. + return true; + case ExpressionClassification.MutableLValue: + // For struct method calls on mutable lvalues, the C# compiler never generates temporaries. + return false; + case ExpressionClassification.ReadonlyLValue: + // For struct method calls on readonly lvalues, the C# compiler + // only generates a temporary if it isn't a "readonly struct" + return !(v.Type.GetDefinition()?.IsReadOnly == true); + default: + throw new InvalidOperationException("invalid expression classification"); + } } internal static bool IsUsedAsThisPointerInCall(LdLoca ldloca) @@ -289,37 +304,87 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + internal enum ExpressionClassification + { + RValue, + MutableLValue, + ReadonlyLValue, + } + /// /// Gets whether the instruction, when converted into C#, turns into an l-value that can /// be used to mutate a value-type. /// If this function returns false, the C# compiler would introduce a temporary copy /// when calling a method on a value-type (and any mutations performed by the method will be lost) /// - static bool IsLValue(ILInstruction inst) + internal static ExpressionClassification ClassifyExpression(ILInstruction inst) { switch (inst.OpCode) { case OpCode.LdLoc: case OpCode.StLoc: - return true; + if (IsReadonlyRefLocal(((IInstructionWithVariableOperand)inst).Variable)) { + return ExpressionClassification.ReadonlyLValue; + } else { + return ExpressionClassification.MutableLValue; + } case OpCode.LdObj: // ldobj typically refers to a storage location, // but readonly fields are an exception. - IField f = (((LdObj)inst).Target as IInstructionWithFieldOperand)?.Field; - return !(f != null && f.IsReadOnly); + if (IsReadonlyReference(((LdObj)inst).Target)) { + return ExpressionClassification.ReadonlyLValue; + } else { + return ExpressionClassification.MutableLValue; + } case OpCode.StObj: // stobj is the same as ldobj. - f = (((StObj)inst).Target as IInstructionWithFieldOperand)?.Field; - return !(f != null && f.IsReadOnly); + if (IsReadonlyReference(((StObj)inst).Target)) { + return ExpressionClassification.ReadonlyLValue; + } else { + return ExpressionClassification.MutableLValue; + } case OpCode.Call: var m = ((CallInstruction)inst).Method; // multi-dimensional array getters are lvalues, // everything else is an rvalue. - return m.DeclaringType.Kind == TypeKind.Array; + if (m.DeclaringType.Kind == TypeKind.Array) { + return ExpressionClassification.MutableLValue; + } else { + return ExpressionClassification.RValue; + } + default: + return ExpressionClassification.RValue; // most instructions result in an rvalue + } + } + + private static bool IsReadonlyReference(ILInstruction addr) + { + switch (addr) { + case LdFlda ldflda: + return ldflda.Field.IsReadOnly; + case LdsFlda ldsflda: + return ldsflda.Field.IsReadOnly; + case LdLoc ldloc: + return IsReadonlyRefLocal(ldloc.Variable); + case Call call: + // TODO: calls with 'readonly ref' return default: - return false; // most instructions result in an rvalue + return false; } } + private static bool IsReadonlyRefLocal(ILVariable variable) + { + if (variable.Kind == VariableKind.Parameter) { + if (variable.Index == -1) { + // this parameter in readonly struct + return variable.Function.Method?.DeclaringTypeDefinition?.IsReadOnly == true; + } else { + return variable.Function.Parameters[variable.Index.Value].IsIn; + } + } + return false; + } + /// /// Determines whether a variable should be inlined in non-aggressive mode, even though it is not a generated variable. ///