diff --git a/.gitignore b/.gitignore index 8a30d25..a255f99 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +.idea/ diff --git a/Underanalyzer/Decompiler/AST/ASTBuilder.cs b/Underanalyzer/Decompiler/AST/ASTBuilder.cs index 3c743fa..802c9f7 100644 --- a/Underanalyzer/Decompiler/AST/ASTBuilder.cs +++ b/Underanalyzer/Decompiler/AST/ASTBuilder.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Underanalyzer.Decompiler.ControlFlow; +using Underanalyzer.Decompiler.Warnings; namespace Underanalyzer.Decompiler.AST; diff --git a/Underanalyzer/Decompiler/DecompileContext.cs b/Underanalyzer/Decompiler/DecompileContext.cs index a1e19eb..67fbb12 100644 --- a/Underanalyzer/Decompiler/DecompileContext.cs +++ b/Underanalyzer/Decompiler/DecompileContext.cs @@ -1,4 +1,10 @@ -using System; +/* + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +using System; using System.Collections.Generic; using Underanalyzer.Decompiler.ControlFlow; using Underanalyzer.Decompiler.Macros; @@ -11,22 +17,22 @@ namespace Underanalyzer.Decompiler; public class DecompileContext { /// - /// The game context this decompile context belongs to. + /// The game context this belongs to. /// public IGameContext GameContext { get; } /// - /// The specific code entry within the game this decompile context belongs to. + /// The specific code entry within the game this belongs to. /// public IGMCode Code { get; private set; } /// - /// The decompilation settings to be used for this decompile context in its operation. + /// The decompilation settings to be used for this in its operation. /// / public IDecompileSettings Settings { get; private set; } /// - /// Any warnings produced throughout the decompilation process. + /// A list of warnings produced throughout the decompilation process. /// public List Warnings { get; } = new(); @@ -35,43 +41,63 @@ public class DecompileContext internal bool GMLv2 { get => GameContext.UsingGMLv2; } // Data structures used (and re-used) for decompilation, as well as tests - internal List Blocks { get; set; } - internal Dictionary BlocksByAddress { get; set; } - internal List FragmentNodes { get; set; } - internal List LoopNodes { get; set; } - internal List ShortCircuitBlocks { get; set; } - internal List ShortCircuitNodes { get; set; } - internal List StaticInitNodes { get; set; } - internal List TryCatchNodes { get; set; } - internal List NullishNodes { get; set; } - internal List BinaryBranchNodes { get; set; } - internal HashSet SwitchEndNodes { get; set; } - internal List SwitchData { get; set; } - internal HashSet SwitchContinueBlocks { get; set; } - internal HashSet SwitchIgnoreJumpBlocks { get; set; } - internal List SwitchNodes { get; set; } - internal Dictionary BlockSurroundingLoops { get; set; } - internal Dictionary BlockAfterLimits { get; set; } - internal List EnumDeclarations { get; set; } = new(); - internal Dictionary NameToEnumDeclaration { get; set; } = new(); - internal GMEnum UnknownEnumDeclaration { get; set; } = null; + // See about changing these to not be nullable? + internal List? Blocks { get; set; } + internal Dictionary? BlocksByAddress { get; set; } + internal List? FragmentNodes { get; set; } + internal List? LoopNodes { get; set; } + internal List? ShortCircuitBlocks { get; set; } + internal List? ShortCircuitNodes { get; set; } + internal List? StaticInitNodes { get; set; } + internal List? TryCatchNodes { get; set; } + internal List? NullishNodes { get; set; } + internal List? BinaryBranchNodes { get; set; } + internal HashSet? SwitchEndNodes { get; set; } + internal List? SwitchData { get; set; } + internal HashSet? SwitchContinueBlocks { get; set; } + internal HashSet? SwitchIgnoreJumpBlocks { get; set; } + internal List? SwitchNodes { get; set; } + internal Dictionary? BlockSurroundingLoops { get; set; } + internal Dictionary? BlockAfterLimits { get; set; } + internal List? EnumDeclarations { get; set; } = new(); + internal Dictionary? NameToEnumDeclaration { get; set; } = new(); + internal GMEnum? UnknownEnumDeclaration { get; set; } = null; internal int UnknownEnumReferenceCount { get; set; } = 0; - public DecompileContext(IGameContext gameContext, IGMCode code, IDecompileSettings settings = null) + /// + /// Initializes a new instance of the class. + /// + /// The game context. + /// The code entry. + /// The decompilation settings that should be used. + public DecompileContext(IGameContext gameContext, IGMCode code, IDecompileSettings settings) { GameContext = gameContext; Code = code; - Settings = settings ?? new DecompileSettings(); + Settings = settings; } + /// + /// + /// + /// + /// + public DecompileContext(IGameContext gameContext, IGMCode code) : this(gameContext, code, new DecompileSettings()) + { } + + // Constructor used for control flow tests internal DecompileContext(IGMCode code) { Code = code; GameContext = new Mock.GameContextMock(); + Settings = new DecompileSettings(); } - // Solely decompiles control flow from the code entry + /// + /// Solely decompiles control flow from the code entry . + /// + /// When a decompiler error occured. private void DecompileControlFlow() { try @@ -93,13 +119,18 @@ private void DecompileControlFlow() { throw new DecompilerException($"Decompiler error during control flow analysis: {ex.Message}", ex); } + // Should probably throw something else, 'cause this should basically never happen. catch (Exception ex) { throw new DecompilerException($"Unexpected exception thrown in decompiler during control flow analysis: {ex.Message}", ex); } } - // Decompiles the AST from the code entry4 + /// + /// Decompiles the AST from the code entry. + /// + /// The AST + /// When a decompiler error occured. private AST.IStatementNode DecompileAST() { try @@ -110,13 +141,19 @@ private AST.IStatementNode DecompileAST() { throw new DecompilerException($"Decompiler error during AST building: {ex.Message}", ex); } + // See in DecompileControlFlow catch (Exception ex) { throw new DecompilerException($"Unexpected exception thrown in decompiler during AST building: {ex.Message}", ex); } } - - // Decompiles the AST from the code entry + + /// + /// Cleans up a given AST. + /// + /// The AST that should be cleaned up. + /// A new cleaned AST. + /// When a decompiler error occured. private AST.IStatementNode CleanupAST(AST.IStatementNode ast) { try @@ -142,6 +179,7 @@ private AST.IStatementNode CleanupAST(AST.IStatementNode ast) /// /// Decompiles the code entry, and returns the AST output. /// + /// The AST. public AST.IStatementNode DecompileToAST() { DecompileControlFlow(); @@ -152,6 +190,7 @@ public AST.IStatementNode DecompileToAST() /// /// Decompiles the code entry, and returns the string output. /// + /// The decompiled code. public string DecompileToString() { AST.IStatementNode ast = DecompileToAST(); diff --git a/Underanalyzer/Decompiler/DecompileSettings.cs b/Underanalyzer/Decompiler/DecompileSettings.cs index a3e96ad..3216bcc 100644 --- a/Underanalyzer/Decompiler/DecompileSettings.cs +++ b/Underanalyzer/Decompiler/DecompileSettings.cs @@ -11,8 +11,7 @@ namespace Underanalyzer.Decompiler; /// public interface IDecompileSettings { - // TODO: more settings :) - + // TODO: more settings :3. Also do some better phrasing for some of these. /// /// String used to indent, e.g. tabs or some amount of spaces generally. diff --git a/Underanalyzer/Decompiler/GlobalFunctions.cs b/Underanalyzer/Decompiler/GlobalFunctions.cs index a7d322c..fa784e3 100644 --- a/Underanalyzer/Decompiler/GlobalFunctions.cs +++ b/Underanalyzer/Decompiler/GlobalFunctions.cs @@ -1,7 +1,12 @@ -using System.Collections.Generic; -using System.Reflection; +/* + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; -using Underanalyzer.Decompiler.AST; using Underanalyzer.Decompiler.ControlFlow; using static Underanalyzer.IGMInstruction; @@ -14,23 +19,32 @@ namespace Underanalyzer.Decompiler; public interface IGlobalFunctions { /// - /// Lookup of function reference to name. Should be the same references that are supplied to the decompiler. + /// Lookup of function reference to name. /// + /// + /// Should be the same references that are supplied in and . + /// public Dictionary FunctionToName { get; } /// - /// Lookup of function name to reference. Should be the same references that are supplied to the decompiler. + /// Lookup of function name to reference. /// + /// /// + /// Should be the same references that are supplied in and . + /// public Dictionary NameToFunction { get; } } /// -/// Provided way to find all global functions in a game, using some components of the decompiler. +/// A default implementation to find all global functions in a game, using some +/// components of the decompiler. /// public class GlobalFunctions : IGlobalFunctions { + /// public Dictionary FunctionToName { get; } + /// public Dictionary NameToFunction { get; } /// @@ -43,15 +57,18 @@ public GlobalFunctions() } /// - /// Given a list of global scripts, initializes this class with all global function information. + /// TODO: better description with params, overload for no paralleloptions + /// Given an enumerable of global scripts, initializes this class with all global function information. /// Optionally, can be passed in to configure parallelization. - /// By default, the default settings are used (which has no limits). + /// By default, the default settings are used (which have no limits). TODO: no limits on what??? /// + /// An enumerable containing all global scripts. + /// Options that define how the parallelization gets executed. public GlobalFunctions(IEnumerable globalScripts, ParallelOptions parallelOptions = null) { Dictionary functionToName = new(); Dictionary nameToFunction = new(); - object _lock = new(); + object _lock = new(); // TODO: use system.threading.lock in c#13 Parallel.ForEach(globalScripts, parallelOptions ?? new(), script => { @@ -60,29 +77,29 @@ public GlobalFunctions(IEnumerable globalScripts, ParallelOptions paral List fragments = Fragment.FindFragments(script, blocks); // Find names of functions after each fragment - for (int i = 1; i < fragments.Count; i++) + foreach (Fragment fragment in fragments.Skip(1)) { - Fragment fragment = fragments[i]; if (fragment.Successors.Count == 0) { // If no successors, assume code is corrupt and don't consider it + // TODO: warn? continue; } Block after = fragment.Successors[0] as Block; if (after is null) { // If block after isn't a block, assume code is corrupt as well + // TODO: warn? continue; } string name = GetFunctionNameAfterFragment(after, out IGMFunction function); - if (name is not null) + if (name is null) continue; + + lock (_lock) { - lock (_lock) - { - functionToName[function] = name; - nameToFunction[name] = function; - } + functionToName[function] = name; + nameToFunction[name] = function; } } }); @@ -93,9 +110,9 @@ public GlobalFunctions(IEnumerable globalScripts, ParallelOptions paral /// /// Gets the name of a global function based on the instructions after a code fragment. - /// Returns null if there is none, or the code is corrupt. + /// Returns if there is none, or the code is corrupt. /// - private string GetFunctionNameAfterFragment(Block block, out IGMFunction foundFunction) + private string? GetFunctionNameAfterFragment(Block block, out IGMFunction? foundFunction) { foundFunction = null; @@ -121,70 +138,71 @@ private string GetFunctionNameAfterFragment(Block block, out IGMFunction foundFu switch (block.Instructions[2].Kind) { case Opcode.PushImmediate: + { + // Normal function. Skip past basic instructions. + if (block.Instructions is not + [ + _, _, + { ValueShort: -1 or -16 }, + { Kind: Opcode.Convert, Type1: DataType.Int32, Type2: DataType.Variable }, + { Kind: Opcode.Call, Function.Name.Content: VMConstants.MethodFunction }, + .. + ]) { - // Normal function. Skip past basic instructions. - if (block.Instructions is not - [ - _, _, - { ValueShort: -1 or -16 }, - { Kind: Opcode.Convert, Type1: DataType.Int32, Type2: DataType.Variable }, - { Kind: Opcode.Call, Function.Name.Content: VMConstants.MethodFunction }, - .. - ]) - { - // Failed to match instructions - return null; - } + // Failed to match instructions + return null; + } - // Check if we have a name - if (block .Instructions is - [ - _, _, _, _, _, - { Kind: Opcode.Duplicate, DuplicationSize2: 0 }, - { Kind: Opcode.PushImmediate }, - { Kind: Opcode.Pop, Variable.Name.Content: string funcName }, - .. - ]) - { - // We have a name! - return funcName; - } - break; + // Check if we have a name + if (block.Instructions is + [ + _, _, _, _, _, + { Kind: Opcode.Duplicate, DuplicationSize2: 0 }, + { Kind: Opcode.PushImmediate }, + { Kind: Opcode.Pop, Variable.Name.Content: string funcName }, + .. + ]) + { + // We have a name! + return funcName; } + break; + } case Opcode.Call: + { + // This is a struct or constructor function + if (block.Instructions is not + [ + _, _, + { Kind: Opcode.Call, Function.Name.Content: VMConstants.NullObjectFunction }, + { Kind: Opcode.Call, Function.Name.Content: VMConstants.MethodFunction }, + .. + ]) { - // This is a struct or constructor function - if (block.Instructions is not - [ - _, _, - { Kind: Opcode.Call, Function.Name.Content: VMConstants.NullObjectFunction }, - { Kind: Opcode.Call, Function.Name.Content: VMConstants.MethodFunction }, - .. - ]) - { - // Failed to match instructions - return null; - } + // Failed to match instructions + return null; + } - // Check if we're a struct or function constructor (named) - if (block.Instructions is - [ - _, _, _, _, - { Kind: Opcode.Duplicate, DuplicationSize2: 0 }, - { Kind: Opcode.PushImmediate, ValueShort: short pushVal }, - { Kind: Opcode.Pop, Variable.Name.Content: string funcName }, - .. - ]) + // Check if we're a struct or function constructor (named) + if (block.Instructions is + [ + _, _, _, _, + { Kind: Opcode.Duplicate, DuplicationSize2: 0 }, + { Kind: Opcode.PushImmediate, ValueShort: short pushVal }, + { Kind: Opcode.Pop, Variable.Name.Content: string funcName }, + .. + ]) + { + // Check if struct or constructor + if (pushVal != -16 && pushVal != -5) { - // Check if struct or constructor - if (pushVal != -16 && pushVal != -5) - { - // We're a constructor! - return funcName; - } + // We're a constructor! + return funcName; } - break; } + break; + } + // TODO: default case? } return null; diff --git a/Underanalyzer/Decompiler/Warnings/DecompileDataLeftoverWarning.cs b/Underanalyzer/Decompiler/Warnings/DecompileDataLeftoverWarning.cs index 7bedbaa..947f6fb 100644 --- a/Underanalyzer/Decompiler/Warnings/DecompileDataLeftoverWarning.cs +++ b/Underanalyzer/Decompiler/Warnings/DecompileDataLeftoverWarning.cs @@ -1,13 +1,26 @@ -namespace Underanalyzer.Decompiler; +/* + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +namespace Underanalyzer.Decompiler.Warnings; /// /// Represents a warning that occurs when data is left over on the VM stack at the end of a fragment. -/// With default settings, this is not a warning, and is instead an exception. /// +/// With the default settings, this is not a warning, and is instead an exception. public class DecompileDataLeftoverWarning : IDecompileWarning { + /// public string Message => $"Data left over on VM stack at end of fragment ({NumberOfElements} elements)."; + + /// public string CodeEntryName { get; } + + /// + /// How many unread elements on the stack are left. + /// public int NumberOfElements { get; } internal DecompileDataLeftoverWarning(int numberOfElements, string codeEntryName) diff --git a/Underanalyzer/Underanalyzer.csproj b/Underanalyzer/Underanalyzer.csproj index 20e14ad..4b8ec40 100644 --- a/Underanalyzer/Underanalyzer.csproj +++ b/Underanalyzer/Underanalyzer.csproj @@ -3,6 +3,7 @@ netstandard2.1;net6.0;net7.0;net8.0 12 + annotations diff --git a/UnderanalyzerTest/DecompileContext.DecompileToString.Settings.cs b/UnderanalyzerTest/DecompileContext.DecompileToString.Settings.cs index 507cf48..89fe1fa 100644 --- a/UnderanalyzerTest/DecompileContext.DecompileToString.Settings.cs +++ b/UnderanalyzerTest/DecompileContext.DecompileToString.Settings.cs @@ -1,4 +1,5 @@ using Underanalyzer.Decompiler; +using Underanalyzer.Decompiler.Warnings; namespace UnderanalyzerTest; diff --git a/UnderanalyzerTest/TestUtil.cs b/UnderanalyzerTest/TestUtil.cs index 2284eea..e98bd9b 100644 --- a/UnderanalyzerTest/TestUtil.cs +++ b/UnderanalyzerTest/TestUtil.cs @@ -70,7 +70,7 @@ public static void EnsureNoRemainingJumps(DecompileContext ctx) public static DecompileContext VerifyDecompileResult(string asm, string gml, GameContextMock? gameContext = null, DecompileSettings? decompileSettings = null) { gameContext ??= new(); - DecompileContext decompilerContext = new(gameContext, GetCode(asm), decompileSettings); + DecompileContext decompilerContext = new(gameContext, GetCode(asm), decompileSettings ?? new DecompileSettings()); string decompileResult = decompilerContext.DecompileToString().Trim(); Assert.Equal(gml.Trim().ReplaceLineEndings("\n"), decompileResult); return decompilerContext;