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;