using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Iris.Asm.Models; using Microsoft.Iris.DecompXml.Mock; using Microsoft.Iris.Markup; using Microsoft.Iris.Markup.UIX; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Iris.DecompXml; partial class Decompiler { private SyntaxTree DecompileScript(uint startOffset, MarkupTypeSchema export) { var statements = DecompileMethod(startOffset, export); return SyntaxTree( CompilationUnit().WithMembers( [..statements.Select(GlobalStatement)] ) ); } public List DecompileMethod(uint startOffset, MarkupTypeSchema export) { var methodBody = _context.GetMethodBody(startOffset).ToArray(); var controlBlocks = ControlFlowAnalyzer.CreateGraph(methodBody); Stack blockStack = []; blockStack.Push(new(0, methodBody[^1].Offset, SyntaxKind.Block, null)); Stack stack = new(); for (int i = 0; i < methodBody.Length; i++) { var instruction = methodBody[i]; // TODO: Handle for loops if (instruction.Offset == blockStack.Peek().EndOffset) { // Make sure there is only one top-level block if (blockStack.Count > 1) { var currentBlock = blockStack.Pop(); currentBlock.FinalizeBlock(blockStack.Peek()); } else { // End of function if (i + 1 != methodBody.Length) throw new InvalidOperationException("Expected end of function!"); } } var opCode = instruction.OpCode; try { switch (opCode) { case OpCode.PushConstant: var constant = _context.GetConstant(instruction.Operands.First()); stack.Push(IrisExpression.ToSyntax(constant, _context)); break; case OpCode.PushNull: stack.Push(LiteralExpression(SyntaxKind.NullLiteralExpression)); break; case OpCode.DiscardValue: stack.Pop(); break; case OpCode.LookupSymbol: var symbolIndex = (ushort)instruction.Operands.ElementAt(0).Value; stack.Push(export.SymbolReferenceTable[symbolIndex]); break; case OpCode.WriteSymbol: case OpCode.WriteSymbolPeek: var writeSymbolIndex = (ushort)instruction.Operands.First().Value; var newSymbolValue = opCode is OpCode.WriteSymbolPeek ? stack.Peek() : stack.Pop(); var symbolAssignmentExpr = AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, IrisExpression.ToSyntax(export.SymbolReferenceTable[writeSymbolIndex]), IrisExpression.ToSyntax(newSymbolValue, _context) ); blockStack.Peek().Statements.Add(ExpressionStatement(symbolAssignmentExpr)); break; case OpCode.PropertyAssign: case OpCode.PropertyAssignStatic: var propToSet = _context.GetImportedProperty(instruction.Operands.First()); var propSetTarget = opCode is OpCode.PropertyAssignStatic ? propToSet.Owner : stack.Pop(); var newPropValue = IrisExpression.ToSyntax(stack.Peek(), _context); var propertySetExpression = AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IrisExpression.ToSyntax(propSetTarget, _context), IdentifierName(propToSet.Name) ), newPropValue ); blockStack.Peek().Statements.Add(ExpressionStatement(propertySetExpression)); break; case OpCode.VerifyTypeCast: var objToCast = stack.Pop(); var typeToCastTo = _context.GetImportedType(instruction.Operands.First()); stack.Push(CastExpression( IrisExpression.ToSyntax(typeToCastTo, _context), IrisExpression.ToSyntax(objToCast, _context) )); break; case OpCode.JumpIfFalse: case OpCode.JumpIfFalsePeek: case OpCode.JumpIfTruePeek: var jumpToOffset = (uint)instruction.Operands.First().Value; // TODO: What about for loops? if (instruction.Offset > jumpToOffset) { } var isPeek = opCode is OpCode.JumpIfFalsePeek or OpCode.JumpIfTruePeek or OpCode.JumpIfNullPeek; var rawJumpCondition = IrisExpression.ToSyntax(isPeek ? stack.Peek() : stack.Pop(), _context); var jumpCondition = opCode switch { OpCode.JumpIfFalse or OpCode.JumpIfFalsePeek => rawJumpCondition, OpCode.JumpIfTruePeek => LogicalNotOf(rawJumpCondition), OpCode.JumpIfNullPeek => BinaryExpression(SyntaxKind.ExclamationEqualsToken, rawJumpCondition, LiteralExpression(SyntaxKind.NullLiteralExpression)), _ => throw new NotImplementedException() }; //var ifBlock = new CodeBlockInfo(instruction.Offset, jumpToOffset, SyntaxKind.IfStatement, jumpCondition); //blockStack.Push(ifBlock); var ifBlock = IfStatement(jumpCondition, GotoStatement(SyntaxKind.GotoStatement, IdentifierName($"UIB_{jumpToOffset:X4}"))) .WithLeadingTrivia(Comment($"/* UIB_{instruction.Offset:X4} */")); blockStack.Peek().Statements.Add(ifBlock); break; case OpCode.Jump: var jumpOffset = (uint)instruction.Operands.First().Value; // TODO blockStack.Peek().Statements.Add(GotoStatement(SyntaxKind.GotoStatement, IdentifierName($"UIB_{jumpOffset:X4}"))); break; case OpCode.ReturnValue: var returnStatement = ReturnStatement(IrisExpression.ToSyntax(stack.Pop(), _context)); blockStack.Peek().Statements.Add(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()); break; case OpCode.ClearSymbol: // Ignore these instructions break; default: if (!TryDecompileExpression(instruction, stack, blockStack)) { var unsupportedComment = Comment($"// Unsupported instruction: {instruction}"); blockStack.Peek().Statements.Add(EmptyStatement().WithLeadingTrivia(unsupportedComment)); } break; } } catch (Exception ex) { throw new Exception($"Failed to decompile instruction `{instruction}` @ 0x{instruction.Offset:X} in script for {export.Name}", ex); } } if (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) 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; } private MethodDeclarationSyntax DecompileMethodDeclaration(MarkupMethodSchema method, MarkupTypeSchema export) { var targetType = export.ResolveScriptId(method.CodeOffset, out var offset); var methodBody = DecompileMethod(offset, export); var parameters = method.ParameterTypes .Zip(method.ParameterNames, (t, n) => Parameter(Identifier(n)).WithType(IrisExpression.ToSyntax(t, _context))); return MethodDeclaration( IrisExpression.ToSyntax(method.ReturnType, _context), method.Name) .WithParameterList(ParameterList([..parameters])) .WithBody(Block(methodBody)); } public static string FormatInlineExpression(ExpressionSyntax expr, CancellationToken token = default) { var exprStr = expr .NormalizeWhitespace() .SyntaxTree .GetText(token) .ToString(); return '{' + exprStr + '}'; } public static string FormatScript(SyntaxTree tree, CancellationToken token = default) { return tree .GetRoot(token) .NormalizeWhitespace() .SyntaxTree .GetText(token) .ToString(); } private bool TryDecompileExpression(Instruction instruction, Stack stack, Stack blockStack = null) { var opCode = instruction.OpCode; switch (opCode) { case OpCode.PushThis: stack.Push(ThisExpression()); break; case OpCode.ConstructObject: case OpCode.ConstructObjectParam: var typeToCtor = _context.GetImportedType(instruction.Operands.First()); List ctorParameters = []; if (opCode is OpCode.ConstructObjectParam) { var ctorSchema = _context.GetImportedConstructor(instruction.Operands.ElementAt(1)); int ctorParameterCount = ctorSchema.ParameterTypes.Length; ctorParameters.Capacity = ctorParameterCount; for (ctorParameterCount--; ctorParameterCount >= 0; ctorParameterCount--) { var parameter = IrisExpression.ToSyntax(stack.Pop(), _context); ctorParameters.Add(Argument(parameter)); } ctorParameters.Reverse(); } stack.Push(ObjectCreationExpression( IrisExpression.ToSyntax(typeToCtor, _context), ArgumentList([..ctorParameters]), null )); break; case OpCode.MethodInvoke: case OpCode.MethodInvokePeek: case OpCode.MethodInvokeStatic: case OpCode.MethodInvokePushLastParam: case OpCode.MethodInvokeStaticPushLastParam: var methodSchema = _context.GetImportedMethod(instruction.Operands.First()); int parameterCount = methodSchema.ParameterTypes.Length; var parameters = new ArgumentSyntax[parameterCount]; for (parameterCount--; parameterCount >= 0; parameterCount--) { var parameter = IrisExpression.ToSyntax(stack.Pop(), _context); parameters[parameterCount] = Argument(parameter); } bool isStatic = opCode is OpCode.MethodInvokeStatic or OpCode.MethodInvokeStaticPushLastParam; bool peek = opCode is OpCode.MethodInvokePeek; bool pushLastParam = opCode is OpCode.MethodInvokePushLastParam or OpCode.MethodInvokeStaticPushLastParam; var targetObj = opCode switch { OpCode.MethodInvokeStatic or OpCode.MethodInvokeStaticPushLastParam => methodSchema.Owner, _ when peek => stack.Peek(), _ => stack.Pop(), }; var methodExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IrisExpression.ToSyntax(targetObj, _context), IdentifierName(methodSchema.Name) ); var methodResult = InvocationExpression(methodExpression, ArgumentList([.. parameters])); if (methodSchema.ReturnType != VoidSchema.Type) { stack.Push(methodResult); } else { blockStack?.Peek().Statements.Add(ExpressionStatement(methodResult)); } if (pushLastParam) { stack.Push(parameters[^1].Expression); } break; case OpCode.PropertyGet: case OpCode.PropertyGetPeek: case OpCode.PropertyGetStatic: var propToGet = _context.GetImportedProperty(instruction.Operands.First()); var propGetTarget = instruction.OpCode switch { OpCode.PropertyGet => stack.Pop(), OpCode.PropertyGetPeek => stack.Peek(), _ => propToGet.Owner, }; var propertyGetExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IrisExpression.ToSyntax(propGetTarget, _context), IdentifierName(propToGet.Name) ); stack.Push(propertyGetExpression); break; case OpCode.Operation: DecompileOperation(instruction, stack); break; case OpCode.IsCheck: var typeToCheckFor = _context.GetImportedType(instruction.Operands.First()); var objToCheck = stack.Pop(); stack.Push(IsPatternExpression( IrisExpression.ToSyntax(objToCheck, _context), TypePattern(IrisExpression.ToSyntax(typeToCheckFor, _context)) )); break; default: return false; } return true; } private ExpressionSyntax DecompileOperation(Instruction instruction, Stack stack) { var opHost = _context.GetImportedType(instruction.Operands.ElementAt(0)); var op = (OperationType)(int)(byte)instruction.Operands.ElementAt(1).Value; var isUnary = TypeSchema.IsUnaryOperation(op); var opSyntax = OperationToSyntaxKind(op); ExpressionSyntax operationExpr; if (isUnary) { var left = IrisExpression.ToSyntax(stack.Pop(), _context); var isPostfix = op is OperationType.PostIncrement or OperationType.PostDecrement; operationExpr = isPostfix ? PostfixUnaryExpression(opSyntax, left) : PrefixUnaryExpression(opSyntax, left); } else { var right = IrisExpression.ToSyntax(stack.Pop(), _context); var left = IrisExpression.ToSyntax(stack.Pop(), _context); operationExpr = BinaryExpression(opSyntax, left, right); } stack.Push(operationExpr); return operationExpr; } private static ExpressionSyntax LogicalNotOf(ExpressionSyntax originalExpression) { if (originalExpression is IsPatternExpressionSyntax isPatternExpression) { var baseTypePattern = isPatternExpression.Pattern; return isPatternExpression.WithPattern(UnaryPattern(baseTypePattern)); } if (originalExpression is BinaryExpressionSyntax binaryExpression) { SyntaxKind notOperatorToken; var operatorToken = binaryExpression.OperatorToken.Kind(); switch (operatorToken) { case SyntaxKind.EqualsEqualsToken: notOperatorToken = SyntaxKind.ExclamationEqualsToken; break; case SyntaxKind.LessThanToken: notOperatorToken = SyntaxKind.GreaterThanEqualsToken; break; case SyntaxKind.GreaterThanToken: notOperatorToken = SyntaxKind.LessThanEqualsToken; break; case SyntaxKind.LessThanEqualsToken: notOperatorToken = SyntaxKind.GreaterThanToken; break; case SyntaxKind.GreaterThanEqualsToken: notOperatorToken = SyntaxKind.LessThanToken; break; case SyntaxKind.AmpersandAmpersandToken: case SyntaxKind.BarBarToken: // Apply De Morgan's laws notOperatorToken = operatorToken is SyntaxKind.AmpersandAmpersandToken ? SyntaxKind.LogicalOrExpression : SyntaxKind.LogicalAndExpression; return BinaryExpression(notOperatorToken, LogicalNotOf(binaryExpression.Left), LogicalNotOf(binaryExpression.Right)); default: goto defaultCase; } return binaryExpression.WithOperatorToken(Token(notOperatorToken)); } defaultCase: return PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, ParenthesizedExpression(originalExpression)); } private static SyntaxKind OperationToSyntaxKind(OperationType operation) { return operation switch { OperationType.MathAdd => SyntaxKind.AddExpression, OperationType.MathSubtract => SyntaxKind.SubtractExpression, OperationType.MathMultiply => SyntaxKind.MultiplyExpression, OperationType.MathDivide => SyntaxKind.DivideExpression, OperationType.MathModulus => SyntaxKind.ModuloExpression, OperationType.MathNegate => SyntaxKind.UnaryMinusExpression, OperationType.LogicalAnd => SyntaxKind.LogicalAndExpression, OperationType.LogicalOr => SyntaxKind.LogicalOrExpression, OperationType.LogicalNot => SyntaxKind.LogicalNotExpression, OperationType.RelationalEquals => SyntaxKind.EqualsExpression, OperationType.RelationalNotEquals => SyntaxKind.NotEqualsExpression, OperationType.RelationalLessThan => SyntaxKind.LessThanExpression, OperationType.RelationalGreaterThan => SyntaxKind.GreaterThanExpression, OperationType.RelationalLessThanEquals => SyntaxKind.LessThanOrEqualExpression, OperationType.RelationalGreaterThanEquals => SyntaxKind.GreaterThanOrEqualExpression, OperationType.RelationalIs => SyntaxKind.IsExpression, OperationType.PostIncrement => SyntaxKind.PostIncrementExpression, OperationType.PostDecrement => SyntaxKind.PostDecrementExpression, _ => throw new ArgumentException($"Invalid operation type '{operation}'", nameof(operation)) }; } }