Foreach decompilation

This commit is contained in:
Yoshi Askharoun
2025-09-26 13:20:12 -05:00
parent 3fdd482fad
commit 89fd689001
4 changed files with 102 additions and 52 deletions
@@ -6,42 +6,43 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Microsoft.Iris.DecompXml;
public record CodeBlockInfo
public record CodeBlock
{
public CodeBlockInfo(uint startOffset, uint endOffset, ICodeBlockAdditionalInfo additionalInfo = null)
public CodeBlock(uint startOffset, uint endOffset, ICodeBlockInfo meta = null)
{
StartOffset = startOffset;
EndOffset = endOffset;
AdditionalInfo = additionalInfo;
Info = meta;
}
public uint StartOffset { get; init; }
public uint EndOffset { get; init; }
public ICodeBlockAdditionalInfo AdditionalInfo { get; init; }
public ICodeBlockInfo Info { get; init; }
public List<StatementSyntax> Statements { get; } = [];
public void FinalizeBlock(CodeBlockInfo parentBlock)
public void FinalizeBlock(CodeBlock parentBlock)
{
var blockBody = Block(Statements);
switch (AdditionalInfo)
switch (Info)
{
case IfBlockInfo ifBlockInfo:
var ifStatement = IfStatement(ifBlockInfo.Condition, blockBody);
if (ifBlockInfo.ElseClause is not null)
ifStatement = ifStatement.WithElse(ifBlockInfo.ElseClause);
parentBlock.Statements.Add(ifStatement);
break;
case ElseBlockInfo _:
var elseClause = ElseClause(blockBody);
if (parentBlock.Info is not IfBlockInfo ifElseBlockInfo)
throw new InvalidOperationException($"Else block must be preceded by an if block, got '{parentBlock.Info}'");
var parentLastStatement = parentBlock.Statements[^1];
if (parentLastStatement is not IfStatementSyntax ifElseBlock)
throw new InvalidOperationException($"Else block must be preceded by an if block, got '{parentLastStatement.Kind()}'");
parentBlock.Statements[^1] = ifElseBlock.WithElse(elseClause);
ifElseBlockInfo.ElseClause = ElseClause(blockBody);
break;
case ForEachBlockInfo forEachBlockInfo:
@@ -51,21 +52,23 @@ public record CodeBlockInfo
break;
default:
throw new NotImplementedException($"Unrecognized code block kind '{AdditionalInfo.GetType().Name}'");
throw new NotImplementedException($"Unrecognized code block kind '{Info.GetType().Name}'");
}
}
}
public interface ICodeBlockAdditionalInfo;
public interface ICodeBlockInfo;
public class IfBlockInfo(ExpressionSyntax condition = null) : ICodeBlockAdditionalInfo
public class IfBlockInfo(ExpressionSyntax condition = null) : ICodeBlockInfo
{
public ExpressionSyntax Condition { get; set; } = condition;
public ElseClauseSyntax ElseClause { get; set; }
}
public class ElseBlockInfo : ICodeBlockAdditionalInfo;
public class ElseBlockInfo : ICodeBlockInfo;
public class ForEachBlockInfo : ICodeBlockAdditionalInfo
public class ForEachBlockInfo : ICodeBlockInfo
{
public ExpressionSyntax Source { get; set; }
+29 -7
View File
@@ -29,7 +29,7 @@ public class ControlFlowAnalyzer
public List<IProgramBlock> ControlBlocks { get; }
public Stack<CodeBlockInfo> BlockStack { get; }
public Stack<CodeBlock> BlockStack { get; }
public IProgramBlock GetByOffset(uint offset)
{
@@ -46,7 +46,7 @@ public class ControlFlowAnalyzer
{
var currentBlock = BlockStack.Pop();
if (currentBlock.EndOffset != currentOffset)
if (currentBlock.EndOffset > currentOffset)
{
BlockStack.Push(currentBlock);
break;
@@ -56,21 +56,43 @@ public class ControlFlowAnalyzer
}
}
public void PushBlock(CodeBlockInfo block) => BlockStack.Push(block);
public void PushBlock(CodeBlock block)
{
BlockStack.Push(block);
return;
public bool TryPeekBlock<T>(out T additionalInfo) where T : ICodeBlockAdditionalInfo
while (BlockStack.Count > 1)
{
var currentBlock = BlockStack.Pop();
var parentBlock = BlockStack.Peek();
if (currentBlock.EndOffset <= parentBlock.EndOffset)
{
BlockStack.Push(currentBlock);
break;
}
currentBlock.FinalizeBlock(parentBlock);
}
}
public bool TryPeekBlockInfo<T>(out T info) where T : ICodeBlockInfo => TryPeekBlock(out _, out info);
public bool TryPeekBlock<T>(out CodeBlock block, out T info) where T : ICodeBlockInfo
{
if (BlockStack.Count > 1)
{
var currentBlock = BlockStack.Peek();
if (currentBlock.AdditionalInfo is T a)
if (currentBlock.Info is T a)
{
additionalInfo = a;
block = currentBlock;
info = a;
return true;
}
}
additionalInfo = default;
block = default;
info = default;
return false;
}
+49 -20
View File
@@ -35,6 +35,12 @@ partial class Decompiler
var dotGraph = cfa.SerializeToGraphviz();
Console.WriteLine(dotGraph);
var breakOrContinueBlocks = controlBlocks
.Where(c => c.Body.TrueForAll(i => i.OpCode is OpCode.ClearSymbol or OpCode.Jump))
.ToList();
HashSet<uint> breakOrContinueStartOffsets = new(breakOrContinueBlocks.Select(c => c.StartOffset));
HashSet<uint> breakOrContinueEndOffsets = new(breakOrContinueBlocks.Select(c => c.EndOffset));
HashSet<uint> foreachLoopHeadOffsets = [];
Dictionary<string, TypeSchema> scopedLocals = [];
@@ -44,8 +50,6 @@ partial class Decompiler
{
var instruction = methodBody[i];
cfa.FinalizeCompletedBlocks(instruction.Offset);
var opCode = instruction.OpCode;
try
@@ -82,14 +86,14 @@ partial class Decompiler
Source = IrisExpression.ToSyntax(stack.Pop(), _context),
};
var foreachBlock = new CodeBlockInfo(instruction.Offset, loopBodyEndOffset, forEachBlockInfo);
var foreachBlock = new CodeBlock(instruction.Offset, loopBodyEndOffset, forEachBlockInfo);
cfa.PushBlock(foreachBlock);
break;
case OpCode.MethodInvokePeek:
// Ignore MoveNext calls when in a foreach loop, as long as we haven't already initialized this loop
if (!cfa.TryPeekBlock<ForEachBlockInfo>(out var forEachBlockInfo1) || forEachBlockInfo1.Type is not null)
if (!cfa.TryPeekBlockInfo<ForEachBlockInfo>(out var forEachBlockInfo1) || forEachBlockInfo1.Type is not null)
goto default;
var methodSchemaPeek = _context.GetImportedMethod(instruction.Operands.First());
@@ -206,7 +210,7 @@ partial class Decompiler
case OpCode.PropertyGetPeek:
// PGETP is only used in foreach loops
if (!cfa.TryPeekBlock<ForEachBlockInfo>(out var forEachBlockInfo2))
if (!cfa.TryPeekBlockInfo<ForEachBlockInfo>(out var forEachBlockInfo2))
throw new InvalidOperationException("Unexpected call to Current outside of a foreach loop");
var propToGet = _context.GetImportedProperty(instruction.Operands.First());
@@ -254,7 +258,7 @@ partial class Decompiler
case OpCode.JumpIfTruePeek:
var jumpToOffset = (uint)instruction.Operands.First().Value;
if (opCode is OpCode.JumpIfFalse && cfa.TryPeekBlock<ForEachBlockInfo>(out var jmpfForEachBlockInfo)
if (opCode is OpCode.JumpIfFalse && cfa.TryPeekBlockInfo<ForEachBlockInfo>(out var jmpfForEachBlockInfo)
&& jmpfForEachBlockInfo.Type is null)
break;
@@ -270,7 +274,7 @@ partial class Decompiler
.First()
.Offset;
var ifBlock = new CodeBlockInfo(instruction.Offset, ifBlockEndOffset, new IfBlockInfo(jumpCondition));
var ifBlock = new CodeBlock(instruction.Offset, ifBlockEndOffset, new IfBlockInfo(jumpCondition));
cfa.PushBlock(ifBlock);
}
else
@@ -285,28 +289,51 @@ partial class Decompiler
case OpCode.Jump:
var jumpOffset = (uint)instruction.Operands.First().Value;
CodeBlock currentForEachBlock = null;
ForEachBlockInfo jmpForEachBlockInfo = null;
foreach (var block in cfa.BlockStack)
{
currentForEachBlock = block;
jmpForEachBlockInfo = block.Info as ForEachBlockInfo;
if (jmpForEachBlockInfo is not null)
break;
}
if (jumpOffset < instruction.Offset)
{
// End of loop
if (!foreachLoopHeadOffsets.Contains(jumpOffset))
{
throw new NotImplementedException("For and while loops are not supported at this time.");
}
if (currentForEachBlock is null)
throw new NotImplementedException($"Unexpected backwards JMP at 0x{instruction.Offset:X}");
// Continue statements look like premature jumps back to the loop header
if (currentForEachBlock.EndOffset > instruction.Offset)
cfa.AppendToBlock(ContinueStatement());
}
else
{
// End of if block, skipping else block
// Break statements look like premature jumps to the loop tail
if (currentForEachBlock is not null && currentForEachBlock.EndOffset < jumpOffset)
{
cfa.AppendToBlock(BreakStatement());
}
else
{
// End of if block, skipping over else block
// Figure out where the else block ends by searching for the last instruction we skip
var elseBlockEndOffset = methodBody
.Reverse()
.SkipWhile(i => i.Offset >= jumpOffset)
.First()
.Offset;
// Figure out where the else block ends by searching for the last instruction we skip
var elseBlockEndOffset = methodBody
.Reverse()
.SkipWhile(i => i.Offset >= jumpOffset)
.First()
.Offset;
var elseBlock = new CodeBlockInfo(instruction.Offset, elseBlockEndOffset, new ElseBlockInfo());
cfa.PushBlock(elseBlock);
var elseBlock = new CodeBlock(instruction.Offset, elseBlockEndOffset, new ElseBlockInfo());
cfa.PushBlock(elseBlock);
}
}
break;
@@ -335,6 +362,8 @@ partial class Decompiler
}
break;
}
cfa.FinalizeCompletedBlocks(instruction.Offset);
}
catch (Exception ex)
{