From 89fd68900154817d2bf63153a18bde43eaee1f68 Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Fri, 26 Sep 2025 13:20:12 -0500 Subject: [PATCH] Foreach decompilation --- .../{CodeBlockInfo.cs => CodeBlock.cs} | 37 +++++----- libs/UIX.DecompXml/ControlFlowAnalyzer.cs | 36 ++++++++-- libs/UIX.DecompXml/Decompiler.Script.cs | 69 +++++++++++++------ libs/UIX.Test/Resources/cfa_foreach06.uix | 12 ++-- 4 files changed, 102 insertions(+), 52 deletions(-) rename libs/UIX.DecompXml/{CodeBlockInfo.cs => CodeBlock.cs} (61%) diff --git a/libs/UIX.DecompXml/CodeBlockInfo.cs b/libs/UIX.DecompXml/CodeBlock.cs similarity index 61% rename from libs/UIX.DecompXml/CodeBlockInfo.cs rename to libs/UIX.DecompXml/CodeBlock.cs index 246dd2b..47ebae9 100644 --- a/libs/UIX.DecompXml/CodeBlockInfo.cs +++ b/libs/UIX.DecompXml/CodeBlock.cs @@ -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 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; } diff --git a/libs/UIX.DecompXml/ControlFlowAnalyzer.cs b/libs/UIX.DecompXml/ControlFlowAnalyzer.cs index 472b05d..bad44db 100644 --- a/libs/UIX.DecompXml/ControlFlowAnalyzer.cs +++ b/libs/UIX.DecompXml/ControlFlowAnalyzer.cs @@ -29,7 +29,7 @@ public class ControlFlowAnalyzer public List ControlBlocks { get; } - public Stack BlockStack { get; } + public Stack 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(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(out T info) where T : ICodeBlockInfo => TryPeekBlock(out _, out info); + + public bool TryPeekBlock(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; } diff --git a/libs/UIX.DecompXml/Decompiler.Script.cs b/libs/UIX.DecompXml/Decompiler.Script.cs index 9cf4d39..20cfbfe 100644 --- a/libs/UIX.DecompXml/Decompiler.Script.cs +++ b/libs/UIX.DecompXml/Decompiler.Script.cs @@ -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 breakOrContinueStartOffsets = new(breakOrContinueBlocks.Select(c => c.StartOffset)); + HashSet breakOrContinueEndOffsets = new(breakOrContinueBlocks.Select(c => c.EndOffset)); + HashSet foreachLoopHeadOffsets = []; Dictionary 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(out var forEachBlockInfo1) || forEachBlockInfo1.Type is not null) + if (!cfa.TryPeekBlockInfo(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(out var forEachBlockInfo2)) + if (!cfa.TryPeekBlockInfo(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(out var jmpfForEachBlockInfo) + if (opCode is OpCode.JumpIfFalse && cfa.TryPeekBlockInfo(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) { diff --git a/libs/UIX.Test/Resources/cfa_foreach06.uix b/libs/UIX.Test/Resources/cfa_foreach06.uix index 383ad95..9632ea8 100644 --- a/libs/UIX.Test/Resources/cfa_foreach06.uix +++ b/libs/UIX.Test/Resources/cfa_foreach06.uix @@ -24,14 +24,10 @@ foreach (Int32 num in ArraySet) { - int y = num; - - if (y % 4 == 0) - { - continue; - } - - sum = sum + y; + sum = sum + num; + continue; + + num = sum - 1; } int x = sum * 5;