diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index e71a1cb71..a116a04cd 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -321,6 +321,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers if (!flags.HasFlag(CompilerOptions.TargetNet40)) { preprocessorSymbols.Add("NETCORE"); + preprocessorSymbols.Add("NET60"); } preprocessorSymbols.Add("ROSLYN"); preprocessorSymbols.Add("CS60"); diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 57cea8eba..bc5b895f8 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -532,12 +532,6 @@ namespace ICSharpCode.Decompiler.Tests [Test] public async Task StringInterpolation([ValueSource(nameof(roslynOnlyWithNet40Options))] CompilerOptions cscOptions) { - if (!cscOptions.HasFlag(CompilerOptions.TargetNet40) && cscOptions.HasFlag(CompilerOptions.UseRoslynLatest)) - { - Assert.Ignore("DefaultInterpolatedStringHandler is not yet supported!"); - return; - } - await Run(cscOptions: cscOptions); } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs index 01d15eccd..ffc6a16fe 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness { @@ -114,12 +115,28 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness Console.WriteLine(a[0].ToString() + a[1].ToString()); } +#if NET60 && ROSLYN2 + static void TestManualDefaultStringInterpolationHandler() + { + Console.WriteLine("TestManualDefaultStringInterpolationHandler:"); + C c = new C(42); + DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(0, 1); + defaultInterpolatedStringHandler.AppendFormatted(c); + M2(Space(), defaultInterpolatedStringHandler.ToStringAndClear()); + } + + static void M2(object x, string y) { } +#endif + static void Main() { TestClass(); TestStruct(); TestStructMutation(); TestCharPlusChar("ab"); +#if NET60 && ROSLYN2 + TestManualDefaultStringInterpolationHandler(); +#endif } } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 0e9b6ad54..dca1897f8 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -150,7 +150,8 @@ namespace ICSharpCode.Decompiler.CSharp new IndexRangeTransform(), new DeconstructionTransform(), new NamedArgumentTransform(), - new UserDefinedLogicTransform() + new UserDefinedLogicTransform(), + new InterpolatedStringTransform() ), } }, diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index cd3918ff6..fb86ca411 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -22,6 +22,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Reflection.Metadata; +using System.Runtime.CompilerServices; using System.Threading; using ICSharpCode.Decompiler.CSharp.Resolver; @@ -3116,11 +3117,47 @@ namespace ICSharpCode.Decompiler.CSharp return TranslateSetterCallAssignment(block); case BlockKind.CallWithNamedArgs: return TranslateCallWithNamedArgs(block); + case BlockKind.InterpolatedString: + return TranslateInterpolatedString(block); default: return ErrorExpression("Unknown block type: " + block.Kind); } } + private TranslatedExpression TranslateInterpolatedString(Block block) + { + var content = new List(); + + for (int i = 1; i < block.Instructions.Count; i++) + { + var call = (Call)block.Instructions[i]; + switch (call.Method.Name) + { + case "AppendLiteral": + content.Add(new InterpolatedStringText(((LdStr)call.Arguments[1]).Value.Replace("{", "{{").Replace("}", "}}"))); + break; + case "AppendFormatted" when call.Arguments.Count == 2: + content.Add(new Interpolation(Translate(call.Arguments[1]))); + break; + case "AppendFormatted" when call.Arguments.Count == 3 && call.Arguments[2] is LdStr ldstr: + content.Add(new Interpolation(Translate(call.Arguments[1]), suffix: ldstr.Value)); + break; + case "AppendFormatted" when call.Arguments.Count == 3 && call.Arguments[2] is LdcI4 ldci4: + content.Add(new Interpolation(Translate(call.Arguments[1]), alignment: ldci4.Value)); + break; + case "AppendFormatted" when call.Arguments.Count == 4 && call.Arguments[2] is LdcI4 ldci4 && call.Arguments[3] is LdStr ldstr: + content.Add(new Interpolation(Translate(call.Arguments[1]), ldci4.Value, ldstr.Value)); + break; + default: + throw new NotSupportedException(); + } + } + + return new InterpolatedStringExpression(content) + .WithILInstruction(block) + .WithRR(new ResolveResult(compilation.FindType(KnownTypeCode.String))); + } + private TranslatedExpression TranslateCallWithNamedArgs(Block block) { return WrapInRef( diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index b17df4ce6..4f66c233f 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -27,7 +27,7 @@ False false - 9.0 + 10 true True ICSharpCode.Decompiler.snk @@ -114,6 +114,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index cd0b43d4c..e8c486dba 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -194,6 +194,20 @@ namespace ICSharpCode.Decompiler.IL case BlockKind.DeconstructionAssignments: Debug.Assert(this.SlotInfo == DeconstructInstruction.AssignmentsSlot); break; + case BlockKind.InterpolatedString: + Debug.Assert(FinalInstruction is Call { Method: { Name: "ToStringAndClear" }, Arguments: { Count: 1 } }); + var interpolInit = Instructions[0] as StLoc; + DebugAssert(interpolInit != null + && interpolInit.Variable.Kind == VariableKind.InitializerTarget + && interpolInit.Variable.AddressCount == Instructions.Count + && interpolInit.Variable.StoreCount == 1); + for (int i = 1; i < Instructions.Count; i++) + { + Call? inst = Instructions[i] as Call; + DebugAssert(inst != null); + DebugAssert(inst.Arguments.Count >= 1 && inst.Arguments[0].MatchLdLoca(interpolInit.Variable)); + } + break; } } @@ -465,5 +479,17 @@ namespace ICSharpCode.Decompiler.IL /// DeconstructionAssignments, WithInitializer, + /// + /// String interpolation using DefaultInterpolatedStringHandler. + /// + /// + /// Block { + /// stloc I_0 = newobj DefaultInterpolatedStringHandler(...) + /// call AppendXXX(I_0, ...) + /// ... + /// final: call ToStringAndClear(ldloc I_0) + /// } + /// + InterpolatedString, } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs new file mode 100644 index 000000000..6efe01f26 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs @@ -0,0 +1,132 @@ +// Copyright (c) 2021 Siegfried Pammer +// +// 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; +using System.Diagnostics; + +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + public class InterpolatedStringTransform : IStatementTransform + { + void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) + { + if (!context.Settings.StringInterpolation) + return; + int interpolationStart = pos; + int interpolationEnd; + ILInstruction insertionPoint; + // stloc v(newobj DefaultInterpolatedStringHandler..ctor(ldc.i4 literalLength, ldc.i4 formattedCount)) + if (block.Instructions[pos] is StLoc + { + Variable: ILVariable { Kind: VariableKind.Local } v, + Value: NewObj { Arguments: { Count: 2 } } newObj + } stloc + && v.Type.IsKnownType(KnownTypeCode.DefaultInterpolatedStringHandler) + && newObj.Method.DeclaringType.IsKnownType(KnownTypeCode.DefaultInterpolatedStringHandler) + && newObj.Arguments[0].MatchLdcI4(out _) + && newObj.Arguments[1].MatchLdcI4(out _)) + { + // { call MethodName(ldloca v, ...) } + do + { + pos++; + } + while (IsKnownCall(block, pos, v)); + interpolationEnd = pos; + // ... call ToStringAndClear(ldloca v) ... + if (!FindToStringAndClear(block, pos, interpolationStart, interpolationEnd, v, out insertionPoint)) + { + return; + } + if (!(v.StoreCount == 1 && v.AddressCount == interpolationEnd - interpolationStart && v.LoadCount == 0)) + { + return; + } + } + else + { + return; + } + context.Step($"Transform DefaultInterpolatedStringHandler {v.Name}", stloc); + v.Kind = VariableKind.InitializerTarget; + var replacement = new Block(BlockKind.InterpolatedString); + for (int i = interpolationStart; i < interpolationEnd; i++) + { + replacement.Instructions.Add(block.Instructions[i]); + } + var callToStringAndClear = insertionPoint; + insertionPoint.ReplaceWith(replacement); + replacement.FinalInstruction = callToStringAndClear; + block.Instructions.RemoveRange(interpolationStart, interpolationEnd - interpolationStart); + } + + private bool IsKnownCall(Block block, int pos, ILVariable v) + { + if (pos >= block.Instructions.Count - 1) + return false; + if (!(block.Instructions[pos] is Call call)) + return false; + if (!(call.Arguments.Count > 1)) + return false; + if (!call.Arguments[0].MatchLdLoca(v)) + return false; + if (call.Method.IsStatic) + return false; + if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.DefaultInterpolatedStringHandler)) + return false; + switch (call.Method.Name) + { + case "AppendLiteral" when call.Arguments.Count == 2 && call.Arguments[1] is LdStr: + case "AppendFormatted" when call.Arguments.Count == 2: + case "AppendFormatted" when call.Arguments.Count == 3 && call.Arguments[2] is LdStr: + case "AppendFormatted" when call.Arguments.Count == 3 && call.Arguments[2] is LdcI4: + case "AppendFormatted" when call.Arguments.Count == 4 && call.Arguments[2] is LdcI4 && call.Arguments[3] is LdStr: + break; + default: + return false; + } + return true; + } + + private bool FindToStringAndClear(Block block, int pos, int interpolationStart, int interpolationEnd, ILVariable v, out ILInstruction insertionPoint) + { + insertionPoint = null; + if (pos >= block.Instructions.Count) + return false; + // find + // ... call ToStringAndClear(ldloca v) ... + // in block.Instructions[pos] + for (int i = interpolationStart; i < interpolationEnd; i++) + { + var result = ILInlining.FindLoadInNext(block.Instructions[pos], v, block.Instructions[i], InliningOptions.None); + if (result.Type != ILInlining.FindResultType.Found) + return false; + insertionPoint ??= result.LoadInst.Parent; + Debug.Assert(insertionPoint == result.LoadInst.Parent); + } + + return insertionPoint is Call + { + Arguments: { Count: 1 }, + Method: { Name: "ToStringAndClear", IsStatic: false } + }; + } + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs b/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs index 06ba9d6cd..1c8a31674 100644 --- a/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs +++ b/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs @@ -137,6 +137,8 @@ namespace ICSharpCode.Decompiler.TypeSystem IFormattable, /// System.FormattableString FormattableString, + /// System.Runtime.CompilerServices.DefaultInterpolatedStringHandler + DefaultInterpolatedStringHandler, /// System.Span{T} SpanOfT, /// System.ReadOnlySpan{T} @@ -218,6 +220,7 @@ namespace ICSharpCode.Decompiler.TypeSystem new KnownTypeReference(KnownTypeCode.TypedReference, TypeKind.Struct, "System", "TypedReference"), new KnownTypeReference(KnownTypeCode.IFormattable, TypeKind.Interface, "System", "IFormattable"), new KnownTypeReference(KnownTypeCode.FormattableString, TypeKind.Class, "System", "FormattableString", baseType: KnownTypeCode.IFormattable), + new KnownTypeReference(KnownTypeCode.DefaultInterpolatedStringHandler, TypeKind.Struct, "System.Runtime.CompilerServices", "DefaultInterpolatedStringHandler"), new KnownTypeReference(KnownTypeCode.SpanOfT, TypeKind.Struct, "System", "Span", 1), new KnownTypeReference(KnownTypeCode.ReadOnlySpanOfT, TypeKind.Struct, "System", "ReadOnlySpan", 1), new KnownTypeReference(KnownTypeCode.MemoryOfT, TypeKind.Struct, "System", "Memory", 1),