diff --git a/libs/UIX.DecompXml/CodeBlockInfo.cs b/libs/UIX.DecompXml/CodeBlockInfo.cs index c422376..246dd2b 100644 --- a/libs/UIX.DecompXml/CodeBlockInfo.cs +++ b/libs/UIX.DecompXml/CodeBlockInfo.cs @@ -6,7 +6,7 @@ using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Iris.DecompXml; -internal record CodeBlockInfo +public record CodeBlockInfo { public CodeBlockInfo(uint startOffset, uint endOffset, ICodeBlockAdditionalInfo additionalInfo = null) { @@ -51,21 +51,21 @@ internal record CodeBlockInfo break; default: - throw new System.NotImplementedException($"Unrecognized code block kind '{AdditionalInfo.GetType().Name}'"); + throw new NotImplementedException($"Unrecognized code block kind '{AdditionalInfo.GetType().Name}'"); } } } -internal interface ICodeBlockAdditionalInfo; +public interface ICodeBlockAdditionalInfo; -internal class IfBlockInfo(ExpressionSyntax condition = null) : ICodeBlockAdditionalInfo +public class IfBlockInfo(ExpressionSyntax condition = null) : ICodeBlockAdditionalInfo { public ExpressionSyntax Condition { get; set; } = condition; } -internal class ElseBlockInfo : ICodeBlockAdditionalInfo; +public class ElseBlockInfo : ICodeBlockAdditionalInfo; -internal class ForEachBlockInfo : ICodeBlockAdditionalInfo +public class ForEachBlockInfo : ICodeBlockAdditionalInfo { public ExpressionSyntax Source { get; set; } diff --git a/libs/UIX.DecompXml/ControlFlowAnalyzer.cs b/libs/UIX.DecompXml/ControlFlowAnalyzer.cs index a16cf7a..472b05d 100644 --- a/libs/UIX.DecompXml/ControlFlowAnalyzer.cs +++ b/libs/UIX.DecompXml/ControlFlowAnalyzer.cs @@ -1,4 +1,5 @@ -using Microsoft.Iris.Asm.Models; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Iris.Asm.Models; using Microsoft.Iris.Markup; using System.Collections.Generic; using System.Collections.Immutable; @@ -21,10 +22,15 @@ public class ControlFlowAnalyzer public ControlFlowAnalyzer(Instruction[] instructions) { ControlBlocks = CreateGraph(instructions); + + BlockStack = []; + BlockStack.Push(new(0, instructions[^1].Offset)); } public List ControlBlocks { get; } + public Stack BlockStack { get; } + public IProgramBlock GetByOffset(uint offset) { return ControlBlocks.First(b => offset >= b.StartOffset && offset <= b.EndOffset); @@ -34,48 +40,53 @@ public class ControlFlowAnalyzer public IProgramBlock GetByInstruction(Instruction instruction) => GetByOffset(instruction.Offset); - public bool IsAlwaysExecuted(uint offset) + public void FinalizeCompletedBlocks(uint currentOffset) { - if (ControlBlocks.Count == 1) - return true; - - // Checks if the entry node is post-dominated by the block containing this offset. - // Essentially, do all paths through this method execute code at this offset? - - var blockOfInterest = GetByStartOffset(offset); - if (blockOfInterest is null) - return false; - - HashSet exitBlockOffsets = new(ControlBlocks - .Where(b => b.Body[^1].OpCode is OpCode.ReturnValue or OpCode.ReturnVoid) - .Select(b => b.StartOffset)); - - HashSet visitedStartOffsets = []; - Stack stack = []; - - stack.Push(ControlBlocks[0]); - - while (stack.Count > 0) + while (BlockStack.Count > 1) { - var current = stack.Pop(); - var currentOffset = current.StartOffset; + var currentBlock = BlockStack.Pop(); - if (currentOffset == offset || visitedStartOffsets.Contains(currentOffset)) - continue; - - if (exitBlockOffsets.Contains(currentOffset)) - return false; - - visitedStartOffsets.Add(current.StartOffset); - - foreach (var childOffset in current.GetChildrenStartOffsets()) + if (currentBlock.EndOffset != currentOffset) { - var child = GetByStartOffset(childOffset); - stack.Push(child); + BlockStack.Push(currentBlock); + break; + } + + currentBlock.FinalizeBlock(BlockStack.Peek()); + } + } + + public void PushBlock(CodeBlockInfo block) => BlockStack.Push(block); + + public bool TryPeekBlock(out T additionalInfo) where T : ICodeBlockAdditionalInfo + { + if (BlockStack.Count > 1) + { + var currentBlock = BlockStack.Peek(); + if (currentBlock.AdditionalInfo is T a) + { + additionalInfo = a; + return true; } } - return true; + additionalInfo = default; + return false; + } + + public void AppendToBlock(StatementSyntax statement) => BlockStack.Peek().Statements.Add(statement); + + public bool TryGetLastStatement(out StatementSyntax statement) + { + var statements = BlockStack.Peek().Statements; + if (statements.Count > 0) + { + statement = statements[^1]; + return true; + } + + statement = null; + return false; } private static List CreateGraph(Instruction[] instructions) diff --git a/libs/UIX.DecompXml/Decompiler.Script.cs b/libs/UIX.DecompXml/Decompiler.Script.cs index 96f01f7..9cf4d39 100644 --- a/libs/UIX.DecompXml/Decompiler.Script.cs +++ b/libs/UIX.DecompXml/Decompiler.Script.cs @@ -8,7 +8,6 @@ using Microsoft.Iris.Markup; using Microsoft.Iris.Markup.UIX; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -36,9 +35,6 @@ partial class Decompiler var dotGraph = cfa.SerializeToGraphviz(); Console.WriteLine(dotGraph); - Stack blockStack = []; - blockStack.Push(new(0, methodBody[^1].Offset)); - HashSet foreachLoopHeadOffsets = []; Dictionary scopedLocals = []; @@ -48,18 +44,7 @@ partial class Decompiler { var instruction = methodBody[i]; - while (blockStack.Count > 1) - { - var currentBlock = blockStack.Pop(); - - if (currentBlock.EndOffset != instruction.Offset) - { - blockStack.Push(currentBlock); - break; - } - - currentBlock.FinalizeBlock(blockStack.Peek()); - } + cfa.FinalizeCompletedBlocks(instruction.Offset); var opCode = instruction.OpCode; @@ -98,13 +83,13 @@ partial class Decompiler }; var foreachBlock = new CodeBlockInfo(instruction.Offset, loopBodyEndOffset, forEachBlockInfo); - blockStack.Push(foreachBlock); + 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 (!TryPeekBlock(out var forEachBlockInfo1) || forEachBlockInfo1.Type is not null) + if (!cfa.TryPeekBlock(out var forEachBlockInfo1) || forEachBlockInfo1.Type is not null) goto default; var methodSchemaPeek = _context.GetImportedMethod(instruction.Operands.First()); @@ -130,21 +115,18 @@ partial class Decompiler break; case OpCode.DiscardValue: - var value = stack.Pop(); - if (value is ExpressionSyntax expr) + if (stack.Pop() is ExpressionSyntax expr) { if (expr is ParenthesizedExpressionSyntax parenExpr) expr = parenExpr.Expression; - var statements = blockStack.Peek().Statements; - if (statements.Count > 0) + if (cfa.TryGetLastStatement(out var lastStatement)) { - var lastStatement = statements[^1]; if (lastStatement.DescendantNodes().Any(n => n.IsEquivalentTo(expr))) break; } - blockStack.Peek().Statements.Add(ExpressionStatement(expr)); + cfa.AppendToBlock(ExpressionStatement(expr)); } break; @@ -197,7 +179,7 @@ partial class Decompiler symbolWriteExpr = ExpressionStatement(symbolAssignmentExpr); } - blockStack.Peek().Statements.Add(symbolWriteExpr); + cfa.AppendToBlock(symbolWriteExpr); break; case OpCode.PropertyAssign: @@ -218,13 +200,13 @@ partial class Decompiler newPropValue ); - blockStack.Peek().Statements.Add(ExpressionStatement(propertySetExpression)); + cfa.AppendToBlock(ExpressionStatement(propertySetExpression)); break; case OpCode.PropertyGetPeek: // PGETP is only used in foreach loops - if (!TryPeekBlock(out var forEachBlockInfo2)) + if (!cfa.TryPeekBlock(out var forEachBlockInfo2)) throw new InvalidOperationException("Unexpected call to Current outside of a foreach loop"); var propToGet = _context.GetImportedProperty(instruction.Operands.First()); @@ -272,7 +254,7 @@ partial class Decompiler case OpCode.JumpIfTruePeek: var jumpToOffset = (uint)instruction.Operands.First().Value; - if (opCode is OpCode.JumpIfFalse && TryPeekBlock(out var jmpfForEachBlockInfo) + if (opCode is OpCode.JumpIfFalse && cfa.TryPeekBlock(out var jmpfForEachBlockInfo) && jmpfForEachBlockInfo.Type is null) break; @@ -289,13 +271,13 @@ partial class Decompiler .Offset; var ifBlock = new CodeBlockInfo(instruction.Offset, ifBlockEndOffset, new IfBlockInfo(jumpCondition)); - blockStack.Push(ifBlock); + cfa.PushBlock(ifBlock); } else { // JMPFP and JMPTP are only used to implement short-circuiting - var ifBlock = SimplifyExpression(jumpCondition); - stack.Push(ifBlock); + var ifCondition = SimplifyExpression(jumpCondition); + stack.Push(ifCondition); } break; @@ -324,21 +306,21 @@ partial class Decompiler .Offset; var elseBlock = new CodeBlockInfo(instruction.Offset, elseBlockEndOffset, new ElseBlockInfo()); - blockStack.Push(elseBlock); + cfa.PushBlock(elseBlock); } break; case OpCode.ReturnValue: var returnStatement = ReturnStatement(IrisExpression.ToSyntax(stack.Pop(), _context)); - blockStack.Peek().Statements.Add(returnStatement); + cfa.AppendToBlock(returnStatement); break; case OpCode.ReturnVoid: // Include return statement when we're not in the main block (which would return anyway) // or when we're not at the end of the function - if (blockStack.Count > 1 || i + 1 < methodBody.Length) - blockStack.Peek().Statements.Add(ReturnStatement()); + if (cfa.BlockStack.Count > 1 || i + 1 < methodBody.Length) + cfa.AppendToBlock(ReturnStatement()); break; case OpCode.ClearSymbol: @@ -346,10 +328,10 @@ partial class Decompiler break; default: - if (!TryDecompileExpression(instruction, stack, blockStack)) + if (!TryDecompileExpression(instruction, stack, cfa)) { var unsupportedComment = Comment($"// Unsupported instruction: {instruction}"); - blockStack.Peek().Statements.Add(EmptyStatement().WithLeadingTrivia(unsupportedComment)); + cfa.AppendToBlock(EmptyStatement().WithLeadingTrivia(unsupportedComment)); } break; } @@ -360,29 +342,13 @@ partial class Decompiler } } - if (blockStack.Count > 1) + if (cfa.BlockStack.Count > 1) throw new InvalidOperationException($"Failed to decompile script for {export.Name}, more than one top-level code block"); - else if (blockStack.Count < 0) + else if (cfa.BlockStack.Count < 0) throw new InvalidOperationException($"Failed to decompile script for {export.Name}, no top-level code blocks"); // Unwrap top-most block to avoid extra curly braces around entire script - return blockStack.Pop().Statements; - - bool TryPeekBlock([NotNullWhen(true)] out T additionalInfo) where T : ICodeBlockAdditionalInfo - { - if (blockStack.Count > 1) - { - var currentBlock = blockStack.Peek(); - if (currentBlock.AdditionalInfo is T a) - { - additionalInfo = a; - return true; - } - } - - additionalInfo = default; - return false; - } + return cfa.BlockStack.Pop().Statements; } private MethodDeclarationSyntax DecompileMethodDeclaration(MarkupMethodSchema method, MarkupTypeSchema export) @@ -615,7 +581,7 @@ partial class Decompiler .ToString(); } - private bool TryDecompileExpression(Instruction instruction, Stack stack, Stack blockStack = null) + private bool TryDecompileExpression(Instruction instruction, Stack stack, ControlFlowAnalyzer cfa = null) { var opCode = instruction.OpCode; @@ -702,7 +668,7 @@ partial class Decompiler } else { - blockStack?.Peek().Statements.Add(ExpressionStatement(methodResult)); + cfa?.AppendToBlock(ExpressionStatement(methodResult)); } if (pushLastParam)