Skip to content

Commit

Permalink
Add support for DefaultInterpolatedStringHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
siegfriedpammer committed Jul 15, 2022
1 parent 5078796 commit f695bbc
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 8 deletions.
1 change: 1 addition & 0 deletions ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ public static List<string> GetPreprocessorSymbols(CompilerOptions flags)
if (!flags.HasFlag(CompilerOptions.TargetNet40))
{
preprocessorSymbols.Add("NETCORE");
preprocessorSymbols.Add("NET60");
}
preprocessorSymbols.Add("ROSLYN");
preprocessorSymbols.Add("CS60");
Expand Down
6 changes: 0 additions & 6 deletions ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -532,12 +532,6 @@ public async Task NullPropagation([ValueSource(nameof(roslynOnlyOptions))] Compi
[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);
}

Expand Down
17 changes: 17 additions & 0 deletions ICSharpCode.Decompiler.Tests/TestCases/Correctness/StringConcat.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;

namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
{
Expand Down Expand Up @@ -114,12 +115,28 @@ static void TestCharPlusChar(string a)
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
}
}
}
3 changes: 2 additions & 1 deletion ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ public static List<IILTransform> GetILTransforms()
new IndexRangeTransform(),
new DeconstructionTransform(),
new NamedArgumentTransform(),
new UserDefinedLogicTransform()
new UserDefinedLogicTransform(),
new InterpolatedStringTransform()
),
}
},
Expand Down
37 changes: 37 additions & 0 deletions ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Threading;

using ICSharpCode.Decompiler.CSharp.Resolver;
Expand Down Expand Up @@ -3116,11 +3117,47 @@ protected internal override TranslatedExpression VisitBlock(Block block, Transla
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<InterpolatedStringContent>();

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(
Expand Down
3 changes: 2 additions & 1 deletion ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<GenerateAssemblyInformationalVersionAttribute>False</GenerateAssemblyInformationalVersionAttribute>

<EnableDefaultItems>false</EnableDefaultItems>
<LangVersion>9.0</LangVersion>
<LangVersion>10</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>ICSharpCode.Decompiler.snk</AssemblyOriginatorKeyFile>
Expand Down Expand Up @@ -114,6 +114,7 @@
<Compile Include="Disassembler\DisassemblerSignatureTypeProvider.cs" />
<Compile Include="Documentation\XmlDocumentationElement.cs" />
<Compile Include="IL\ControlFlow\AwaitInFinallyTransform.cs" />
<Compile Include="IL\Transforms\InterpolatedStringTransform.cs" />
<Compile Include="IL\Transforms\IntroduceNativeIntTypeOnLocals.cs" />
<Compile Include="IL\Transforms\LdLocaDupInitObjTransform.cs" />
<Compile Include="IL\Transforms\ParameterNullCheckTransform.cs" />
Expand Down
26 changes: 26 additions & 0 deletions ICSharpCode.Decompiler/IL/Instructions/Block.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ internal override void CheckInvariant(ILPhase phase)
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;
}
}

Expand Down Expand Up @@ -465,5 +479,17 @@ public enum BlockKind
/// </summary>
DeconstructionAssignments,
WithInitializer,
/// <summary>
/// String interpolation using DefaultInterpolatedStringHandler.
/// </summary>
/// <example>
/// Block {
/// stloc I_0 = newobj DefaultInterpolatedStringHandler(...)
/// call AppendXXX(I_0, ...)
/// ...
/// final: call ToStringAndClear(ldloc I_0)
/// }
/// </example>
InterpolatedString,
}
}
132 changes: 132 additions & 0 deletions ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs
Original file line number Diff line number Diff line change
@@ -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 }
};
}
}
}
3 changes: 3 additions & 0 deletions ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ public enum KnownTypeCode
IFormattable,
/// <summary><c>System.FormattableString</c></summary>
FormattableString,
/// <summary><c>System.Runtime.CompilerServices.DefaultInterpolatedStringHandler</c></summary>
DefaultInterpolatedStringHandler,
/// <summary><c>System.Span{T}</c></summary>
SpanOfT,
/// <summary><c>System.ReadOnlySpan{T}</c></summary>
Expand Down Expand Up @@ -218,6 +220,7 @@ public sealed class KnownTypeReference : ITypeReference
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),
Expand Down

0 comments on commit f695bbc

Please sign in to comment.