Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public enum CompilerOptions
CheckForOverflowUnderflow = 0x20000,
ProcessXmlDoc = 0x40000,
UseRoslyn4_14_0 = 0x80000,
EnableRuntimeAsync = 0x100000,
UseMcsMask = UseMcs2_6_4 | UseMcs5_23,
UseRoslynMask = UseRoslyn1_3_2 | UseRoslyn2_10_0 | UseRoslyn3_11_0 | UseRoslyn4_14_0 | UseRoslynLatest
}
Expand Down Expand Up @@ -605,13 +606,18 @@ public static async Task<CompilerResults> CompileCSharp(string sourceFileName, C
if (roslynVersion != "legacy")
{
otherOptions += "/shared ";
if (!targetNet40 && Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion)).Major > 2)
var version = Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion));
if (!targetNet40 && version.Major > 2)
{
if (flags.HasFlag(CompilerOptions.NullableEnable))
otherOptions += "/nullable+ ";
else
otherOptions += "/nullable- ";
}
if (!targetNet40 && roslynVersion == roslynLatestVersion && flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
otherOptions += "/features:runtime-async=on ";
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christophwille I think you found out, that this is no longer needed?

}
}

if (flags.HasFlag(CompilerOptions.Library))
Expand All @@ -627,6 +633,12 @@ public static async Task<CompilerResults> CompileCSharp(string sourceFileName, C
{
otherOptions += "-debug:full ";
}
else if (flags.HasFlag(CompilerOptions.EnableRuntimeAsync))
{
// Runtime-async loses the user's local variable names along with the state machine,
// so we have to fall back on PDB info to recover them.
otherOptions += "-debug:portable ";
}
else
{
otherOptions += "-debug- ";
Expand Down Expand Up @@ -842,6 +854,8 @@ internal static string GetSuffix(CompilerOptions cscOptions)
suffix += ".mcs2";
if ((cscOptions & CompilerOptions.UseMcs5_23) != 0)
suffix += ".mcs5";
if ((cscOptions & CompilerOptions.EnableRuntimeAsync) != 0)
suffix += ".runtimeasync";
return suffix;
}

Expand Down
40 changes: 40 additions & 0 deletions ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,46 @@ public async Task CustomTaskType([ValueSource(nameof(roslyn2OrNewerOptions))] Co
await RunForLibrary(cscOptions: cscOptions);
}

[Test]
public async Task RuntimeAsync([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("Async", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncForeach([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("AsyncForeach", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncMain([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await Run("AsyncMain", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncStreams([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("AsyncStreams", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task RuntimeAsyncUsing([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(
"AsyncUsing",
cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview,
configureDecompiler: settings => { settings.UseEnhancedUsing = false; }
);
}

[Test]
public async Task RuntimeAsyncCustomTaskType([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary("CustomTaskType", cscOptions: cscOptions | CompilerOptions.EnableRuntimeAsync | CompilerOptions.Preview);
}

[Test]
public async Task NullableRefTypes([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
Expand Down
3 changes: 2 additions & 1 deletion ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public enum LanguageVersion
CSharp12_0 = 1200,
CSharp13_0 = 1300,
CSharp14_0 = 1400,
Preview = 1400,
CSharp15_0 = 1500,
Preview = 1500,
Latest = 0x7FFFFFFF
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression)
invocationExpression.ReplaceWith(slicing);
}
break;
case "System.Runtime.CompilerServices.AsyncHelpers.Await":
if (arguments.Length == 1 && context.Settings.RuntimeAsync)
{
var awaitExpr = new UnaryOperatorExpression(UnaryOperatorType.Await, arguments[0].Detach());
awaitExpr.CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(awaitExpr);
}
break;
}

bool isChecked;
Expand Down
24 changes: 24 additions & 0 deletions ICSharpCode.Decompiler/DecompilerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,16 @@ public void SetLanguageVersion(CSharp.LanguageVersion languageVersion)
extensionMembers = false;
firstClassSpanTypes = false;
}
if (languageVersion < CSharp.LanguageVersion.CSharp15_0)
{
runtimeAsync = false;
}
}

public CSharp.LanguageVersion GetMinimumRequiredVersion()
{
if (runtimeAsync)
return CSharp.LanguageVersion.CSharp15_0;
if (extensionMembers || firstClassSpanTypes)
return CSharp.LanguageVersion.CSharp14_0;
if (paramsCollections)
Expand Down Expand Up @@ -2198,6 +2204,24 @@ public bool FirstClassSpanTypes {
}
}

bool runtimeAsync = true;

/// <summary>
/// Gets/Sets whether runtime async should be used.
/// </summary>
[Category("C# 15.0 / VS 202x.yy")]
[Description("DecompilerSettings.RuntimeAsync")]
public bool RuntimeAsync {
get { return runtimeAsync; }
set {
if (runtimeAsync != value)
{
runtimeAsync = value;
OnPropertyChanged();
}
}
}

bool separateLocalVariableDeclarations = false;

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
<Compile Include="Disassembler\DisassemblerSignatureTypeProvider.cs" />
<Compile Include="Documentation\XmlDocumentationElement.cs" />
<Compile Include="IL\ControlFlow\AwaitInFinallyTransform.cs" />
<Compile Include="IL\ControlFlow\RuntimeAsyncExceptionRewriteTransform.cs" />
<Compile Include="IL\Transforms\InterpolatedStringTransform.cs" />
<Compile Include="IL\Transforms\IntroduceNativeIntTypeOnLocals.cs" />
<Compile Include="IL\Transforms\LdLocaDupInitObjTransform.cs" />
Expand Down
4 changes: 4 additions & 0 deletions ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ public void Run(ILFunction function, ILTransformContext context)
awaitDebugInfos.Clear();
moveNextLeaves.Clear();
if (!MatchTaskCreationPattern(function) && !MatchAsyncEnumeratorCreationPattern(function))
{
if (function.Method?.IsAsync == true)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs a decompiler settings check, should only run if Runtime Async is enabled.

RuntimeAsyncExceptionRewriteTransform.Run(function, context);
return;
}
try
{
AnalyzeMoveNext();
Expand Down
136 changes: 80 additions & 56 deletions ICSharpCode.Decompiler/IL/ControlFlow/AwaitInFinallyTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,31 +78,11 @@ public static void Run(ILFunction function, ILTransformContext context)
// [stloc V_3(ldloc E_100) - copy exception variable to a temporary]
// stloc V_6(ldloc V_3) - store exception in 'global' object variable
// br IL_0075 - jump out of catch block to the head of the finallyBlock
var catchBlockEntry = catchBlockContainer.EntryPoint;
ILVariable objectVariable;
switch (catchBlockEntry.Instructions.Count)
if (!MatchObjectStoreCatchHandler(catchBlockContainer, exceptionVariable,
out var objectVariable, out var entryPointOfFinally))
{
case 2:
if (!catchBlockEntry.Instructions[0].MatchStLoc(out objectVariable, out var value))
continue;
if (!value.MatchLdLoc(exceptionVariable))
continue;
break;
case 3:
if (!catchBlockEntry.Instructions[0].MatchStLoc(out var temporaryVariable, out value))
continue;
if (!value.MatchLdLoc(exceptionVariable))
continue;
if (!catchBlockEntry.Instructions[1].MatchStLoc(out objectVariable, out value))
continue;
if (!value.MatchLdLoc(temporaryVariable))
continue;
break;
default:
continue;
}
if (!catchBlockEntry.Instructions[catchBlockEntry.Instructions.Count - 1].MatchBranch(out var entryPointOfFinally))
continue;
}
// globalCopyVar should only be used once, at the end of the finally-block
if (objectVariable.LoadCount != 1 || objectVariable.StoreCount > 2)
continue;
Expand Down Expand Up @@ -136,7 +116,7 @@ public static void Run(ILFunction function, ILTransformContext context)
tryCatch.ReplaceWith(new TryFinally(tryCatch.TryBlock, finallyContainer).WithILRange(tryCatch.TryBlock));

context.Step("Move blocks into finally", finallyContainer);
MoveDominatedBlocksToContainer(entryPointOfFinally, beforeExceptionCaptureBlock, cfg, finallyContainer);
MoveDominatedBlocksToContainer(entryPointOfFinally, beforeExceptionCaptureBlock, cfg, finallyContainer, context);

SimplifyEndOfFinally(context, objectVariable, beforeExceptionCaptureBlock, objectVariableCopy, finallyContainer);

Expand Down Expand Up @@ -193,38 +173,6 @@ public static void Run(ILFunction function, ILTransformContext context)
}
}

void MoveDominatedBlocksToContainer(Block newEntryPoint, Block endBlock, ControlFlowGraph graph,
BlockContainer targetContainer)
{
var node = graph.GetNode(newEntryPoint);
var endNode = endBlock == null ? null : graph.GetNode(endBlock);

MoveBlock(newEntryPoint, targetContainer);

foreach (var n in graph.cfg)
{
Block block = (Block)n.UserData;

if (node.Dominates(n))
{
if (endNode != null && endNode != n && endNode.Dominates(n))
continue;

if (block.Parent == targetContainer)
continue;

MoveBlock(block, targetContainer);
}
}
}

void MoveBlock(Block block, BlockContainer target)
{
context.Step($"Move {block.Label} to container at IL_{target.StartILOffset:x4}", target);
block.Remove();
target.Blocks.Add(block);
}

static void SimplifyEndOfFinally(ILTransformContext context, ILVariable objectVariable, Block beforeExceptionCaptureBlock, ILVariable objectVariableCopy, BlockContainer finallyContainer)
{
if (beforeExceptionCaptureBlock.Instructions.Count >= 3
Expand Down Expand Up @@ -281,6 +229,82 @@ private static bool ValidateStateVariable(ILVariable stateVariable, StLoc initia
return true;
}

/// <summary>
/// Matches a catch handler that stores its exception in an "object"-typed local and then
/// branches out of the catch:
/// [stloc tmp(ldloc exceptionVariable);]
/// stloc objectVariable(ldloc tmp_or_exceptionVariable)
/// br branchTarget
/// Used by the state-machine async lowering AND by the runtime-async try-finally lowering.
/// </summary>
internal static bool MatchObjectStoreCatchHandler(BlockContainer catchBlockContainer,
ILVariable exceptionVariable, out ILVariable objectVariable, out Block branchTarget)
{
objectVariable = null;
branchTarget = null;
var entry = catchBlockContainer.EntryPoint;
ILInstruction value;
switch (entry.Instructions.Count)
{
case 2:
if (!entry.Instructions[0].MatchStLoc(out objectVariable, out value))
return false;
if (!value.MatchLdLoc(exceptionVariable))
return false;
break;
case 3:
if (!entry.Instructions[0].MatchStLoc(out var temporaryVariable, out value))
return false;
if (!value.MatchLdLoc(exceptionVariable))
return false;
if (!entry.Instructions[1].MatchStLoc(out objectVariable, out value))
return false;
if (!value.MatchLdLoc(temporaryVariable))
return false;
break;
default:
return false;
}
return entry.Instructions[entry.Instructions.Count - 1].MatchBranch(out branchTarget);
}

/// <summary>
/// Move <paramref name="newEntryPoint"/> and every block it dominates (excluding any block
/// dominated by <paramref name="endBlock"/> other than <paramref name="endBlock"/> itself)
/// from their current container into <paramref name="targetContainer"/>.
/// </summary>
internal static void MoveDominatedBlocksToContainer(Block newEntryPoint, Block endBlock,
ControlFlowGraph graph, BlockContainer targetContainer, ILTransformContext context)
{
var node = graph.GetNode(newEntryPoint);
var endNode = endBlock == null ? null : graph.GetNode(endBlock);

MoveBlock(newEntryPoint, targetContainer, context);

foreach (var n in graph.cfg)
{
Block block = (Block)n.UserData;

if (node.Dominates(n))
{
if (endNode != null && endNode != n && endNode.Dominates(n))
continue;

if (block.Parent == targetContainer)
continue;

MoveBlock(block, targetContainer, context);
}
}
}

static void MoveBlock(Block block, BlockContainer target, ILTransformContext context)
{
context.Step($"Move {block.Label} to container at IL_{target.StartILOffset:x4}", target);
block.Remove();
target.Blocks.Add(block);
}

static (Block, Block, ILVariable) FindBlockAfterFinally(ILTransformContext context, Block block, ILVariable objectVariable)
{
// Block IL_0327 (incoming: 2) {
Expand Down
Loading
Loading