Browse Source

Avoid making explicit ToString() implicit when doing so changes the semantics for mutable value types.

pull/1726/head
Daniel Grunwald 6 years ago
parent
commit
4b90e43187
  1. 18
      ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs
  2. 2
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/Using.cs
  3. 13
      ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
  4. 22
      ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs

18
ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs

@ -6,7 +6,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
{ {
private class C private class C
{ {
readonly int i; int i;
public C(int i) public C(int i)
{ {
@ -17,13 +17,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
public override string ToString() public override string ToString()
{ {
Console.WriteLine(" C(" + i + ").ToString()"); Console.WriteLine(" C(" + i + ").ToString()");
return i.ToString(); return (i++).ToString();
} }
} }
private struct S private struct S
{ {
readonly int i; int i;
public S(int i) public S(int i)
{ {
@ -34,7 +34,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
public override string ToString() public override string ToString()
{ {
Console.WriteLine(" S(" + i + ").ToString()"); Console.WriteLine(" S(" + i + ").ToString()");
return i.ToString(); return (i++).ToString();
} }
} }
@ -98,10 +98,20 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
Console.WriteLine(string.Concat(new S(30), Space()) + new S(31)); Console.WriteLine(string.Concat(new S(30), Space()) + new S(31));
} }
static void TestStructMutation()
{
Console.WriteLine("TestStructMutation:");
S s = new S(0);
Console.WriteLine(Space() + s);
Console.WriteLine(Space() + s.ToString());
Console.WriteLine(s);
}
static void Main() static void Main()
{ {
TestClass(); TestClass();
TestStruct(); TestStruct();
TestStructMutation();
} }
} }
} }

2
ICSharpCode.Decompiler.Tests/TestCases/Pretty/Using.cs

@ -90,7 +90,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
private void UsingStatementOnNullableStruct(UsingStruct? us) private void UsingStatementOnNullableStruct(UsingStruct? us)
{ {
using (us) { using (us) {
Console.WriteLine("using-body: " + us); Console.WriteLine("using-body: " + us.ToString());
} }
} }

13
ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs

@ -233,14 +233,15 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
static readonly Pattern ToStringCallPattern = new Choice { static readonly Pattern ToStringCallPattern = new Choice {
// target.ToString() // target.ToString()
new InvocationExpression(new MemberReferenceExpression(new AnyNode("target"), "ToString")), new InvocationExpression(new MemberReferenceExpression(new AnyNode("target"), "ToString")).WithName("call"),
// target?.ToString() // target?.ToString()
new UnaryOperatorExpression( new UnaryOperatorExpression(
UnaryOperatorType.NullConditionalRewrap, UnaryOperatorType.NullConditionalRewrap,
new InvocationExpression( new InvocationExpression(
new MemberReferenceExpression( new MemberReferenceExpression(
new UnaryOperatorExpression(UnaryOperatorType.NullConditional, new AnyNode("target")), new UnaryOperatorExpression(UnaryOperatorType.NullConditional, new AnyNode("target")),
"ToString")) "ToString")
).WithName("call")
).WithName("nullConditional") ).WithName("nullConditional")
}; };
@ -256,6 +257,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// generate additional ToString() calls in this case. // generate additional ToString() calls in this case.
return expr; return expr;
} }
var toStringMethod = m.Get<Expression>("call").Single().GetSymbol() as IMethod;
var target = m.Get<Expression>("target").Single(); var target = m.Get<Expression>("target").Single();
var type = target.GetResolveResult().Type; var type = target.GetResolveResult().Type;
if (!(isLastArgument || ToStringIsKnownEffectFree(type))) { if (!(isLastArgument || ToStringIsKnownEffectFree(type))) {
@ -266,6 +268,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// ToString() might throw NullReferenceException, but the builtin operator+ doesn't. // ToString() might throw NullReferenceException, but the builtin operator+ doesn't.
return expr; return expr;
} }
if (!ToStringIsKnownEffectFree(type) && toStringMethod != null && IL.Transforms.ILInlining.MethodRequiresCopyForReadonlyLValue(toStringMethod)) {
// ToString() on a struct may mutate the struct.
// For operator+ the C# compiler creates a temporary copy before implicitly calling ToString(),
// whereas an explicit ToString() call would mutate the original lvalue.
// So we can't remove the compiler-generated ToString() call in cases where this might make a difference.
return expr;
}
// All checks succeeded, we can eliminate the ToString() call. // All checks succeeded, we can eliminate the ToString() call.
// The C# compiler will generate an equivalent call if the code is recompiled. // The C# compiler will generate an equivalent call if the code is recompiled.

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

@ -249,7 +249,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// Thus, we have to ensure we're operating on an r-value. // 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 // 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). // of the rvalue (e.g. M(ref (MyStruct)obj); is invalid).
if (!IsUsedAsThisPointerInCall(loadInst)) if (!IsUsedAsThisPointerInCall(loadInst, out var method))
return false; return false;
switch (ClassifyExpression(inlinedExpression)) { switch (ClassifyExpression(inlinedExpression)) {
case ExpressionClassification.RValue: case ExpressionClassification.RValue:
@ -261,14 +261,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms
case ExpressionClassification.ReadonlyLValue: case ExpressionClassification.ReadonlyLValue:
// For struct method calls on readonly lvalues, the C# compiler // For struct method calls on readonly lvalues, the C# compiler
// only generates a temporary if it isn't a "readonly struct" // only generates a temporary if it isn't a "readonly struct"
return !(v.Type.GetDefinition()?.IsReadOnly == true); return MethodRequiresCopyForReadonlyLValue(method);
default: default:
throw new InvalidOperationException("invalid expression classification"); throw new InvalidOperationException("invalid expression classification");
} }
} }
internal static bool MethodRequiresCopyForReadonlyLValue(IMethod method)
{
var type = method.DeclaringType;
if (type.IsReferenceType == true)
return false; // reference types are never implicitly copied
if (type.GetDefinition()?.IsReadOnly == true)
return false; // readonly structs are never implicitly copied
return true;
}
internal static bool IsUsedAsThisPointerInCall(LdLoca ldloca) internal static bool IsUsedAsThisPointerInCall(LdLoca ldloca)
{ {
return IsUsedAsThisPointerInCall(ldloca, out _);
}
static bool IsUsedAsThisPointerInCall(LdLoca ldloca, out IMethod method)
{
method = null;
if (ldloca.ChildIndex != 0) if (ldloca.ChildIndex != 0)
return false; return false;
if (ldloca.Variable.Type.IsReferenceType ?? false) if (ldloca.Variable.Type.IsReferenceType ?? false)
@ -280,7 +296,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
switch (inst.Parent.OpCode) { switch (inst.Parent.OpCode) {
case OpCode.Call: case OpCode.Call:
case OpCode.CallVirt: case OpCode.CallVirt:
var method = ((CallInstruction)inst.Parent).Method; method = ((CallInstruction)inst.Parent).Method;
if (method.IsAccessor && method.AccessorKind != MethodSemanticsAttributes.Getter) { if (method.IsAccessor && method.AccessorKind != MethodSemanticsAttributes.Getter) {
// C# doesn't allow calling setters on temporary structs // C# doesn't allow calling setters on temporary structs
return false; return false;

Loading…
Cancel
Save