Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Underanalyzer/Compiler/Lexer/Token.cs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,14 @@ public TokenVariable(TokenKeyword keyword)
BuiltinVariable = Context.CompileContext.GameContext.Builtins.LookupBuiltinVariable(Text);
}

public TokenVariable(TokenBoolean boolean)
{
Context = boolean.Context;
TextPosition = boolean.TextPosition;
Text = boolean.ToString();
BuiltinVariable = null;
}

public override string ToString()
{
return Text;
Expand Down
63 changes: 58 additions & 5 deletions Underanalyzer/Compiler/Nodes/FunctionDeclNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ public static SimpleFunctionCallNode ParseStruct(ParseContext context, IToken to
// Read assignments from struct literal
while (!context.EndOfCode && !context.IsCurrentToken(SeparatorKind.BlockClose, KeywordKind.End))
{
bool parsedStringName = false;
if (context.Tokens[context.Position] is not TokenVariable variable)
{
// Failed to find a variable here... check if a constant/asset reference, string, or keyword
Expand All @@ -301,11 +302,16 @@ public static SimpleFunctionCallNode ParseStruct(ParseContext context, IToken to
else if (context.Tokens[context.Position] is TokenString str)
{
variable = new TokenVariable(str);
parsedStringName = true;
}
else if (context.Tokens[context.Position] is TokenKeyword keyword)
{
variable = new TokenVariable(keyword);
}
else if (context.Tokens[context.Position] is TokenBoolean boolean)
{
variable = new TokenVariable(boolean);
}
else
{
// Nothing matched; stop parsing
Expand Down Expand Up @@ -357,12 +363,59 @@ public static SimpleFunctionCallNode ParseStruct(ParseContext context, IToken to
}
}

// Create assignment statement
SimpleVariableNode destination = new(variable)
// Create assignment statement (or a function call to "variable_struct_set" in some cases)
string variableName = variable.Text;
bool modernSpecialCaseNames = context.CompileContext.GameContext.UsingStructSpecialCaseNames;
if (string.IsNullOrEmpty(variableName))
{
context.CompileContext.PushError("Struct field name cannot be empty", variable);
}
else if (variableName[0] is not ((>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '_'))
{
StructVariable = true
};
block.Children.Add(new AssignNode(AssignNode.AssignKind.Normal, destination, value));
if (context.CompileContext.GameContext.UsingStructAnyNonemptyString)
{
List<IASTNode> callArgs =
[
new SimpleFunctionCallNode(VMConstants.SelfFunction, null, []),
new StringNode(variable.Text, variable),
value
];
SimpleFunctionCallNode callNode = new(VMConstants.StructSetFunction, null, callArgs)
{
IsStatement = true
};
block.Children.Add(callNode);
}
else
{
context.CompileContext.PushError("Struct field name must start with a-z, A-Z, or _ in this GameMaker version", variable);
}
}
else if (modernSpecialCaseNames && !parsedStringName && VMConstants.ModernDisallowedStructKeywords.Contains(variableName))
{
context.CompileContext.PushError("Invalid keyword used for struct field name, must surround with quotes", variable);
}
else if (modernSpecialCaseNames && (variableName == "self" || variableName == "other"))
{
DotVariableNode destination = new(
new SimpleFunctionCallNode(VMConstants.SelfFunction,
context.CompileContext.GameContext.Builtins.LookupBuiltinFunction(VMConstants.SelfFunction),
[]),
variable);
block.Children.Add(new AssignNode(AssignNode.AssignKind.Normal, destination, value));
}
else if (!modernSpecialCaseNames && !parsedStringName && VMConstants.OldDisallowedStructKeywords.Contains(variableName))
{
context.CompileContext.PushError("Invalid keyword used for struct field name in this GameMaker version, must surround with quotes", variable);
}
else
{
SimpleVariableNode destination = new(variable)
{
StructVariable = true
};
block.Children.Add(new AssignNode(AssignNode.AssignKind.Normal, destination, value));
}

// Expect "," or "}"
if (context.IsCurrentToken(SeparatorKind.Comma))
Expand Down
15 changes: 13 additions & 2 deletions Underanalyzer/Decompiler/AST/Nodes/AssignNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,25 @@ public int BlockClean(ASTCleaner cleaner, BlockNode block, int i)
/// <summary>
/// Returns whether a variable name is a valid GML identifier or not.
/// </summary>
private static bool VariableNameIsValidIdentifier(string name)
private static bool VariableNameIsValidIdentifier(IGameContext context, string name)
{
// If name is empty, it's clearly not valid
if (name.Length == 0)
{
return false;
}

// If using a disallowed keyword, that's also not valid
bool modernSpecialCaseNames = context.UsingStructSpecialCaseNames;
if (modernSpecialCaseNames && VMConstants.ModernDisallowedStructKeywords.Contains(name))
{
return false;
}
if (!modernSpecialCaseNames && VMConstants.OldDisallowedStructKeywords.Contains(name))
{
return false;
}

// Check first character
char firstChar = name[0];
if ((firstChar < 'a' || firstChar > 'z') &&
Expand Down Expand Up @@ -342,7 +353,7 @@ public void Print(ASTPrinter printer)
if (Variable is VariableNode { Variable.Name.Content: string variableName })
{
// Write just the variable name if possible
if (VariableNameIsValidIdentifier(variableName))
if (VariableNameIsValidIdentifier(printer.Context.GameContext, variableName))
{
printer.Write(variableName);
}
Expand Down
15 changes: 15 additions & 0 deletions Underanalyzer/Decompiler/AST/Nodes/StructNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ public IExpressionNode Clean(ASTCleaner cleaner)
public IExpressionNode PostClean(ASTCleaner cleaner)
{
Body.PostCleanStruct(cleaner);

// Replace impossible fields that use "variable_struct_set" and make them use quoted fields instead.
for (int i = 0; i < Body.Children.Count; i++)
{
if (Body.Children[i] is FunctionCallNode
{
Function.Name.Content: VMConstants.StructSetFunction,
Arguments.Count: 3
}
callNode)
{
Body.Children[i] = new AssignNode(callNode.Arguments[1], callNode.Arguments[2]);
}
}

return this;
}

Expand Down
2 changes: 1 addition & 1 deletion Underanalyzer/Decompiler/AST/Nodes/VariableNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class VariableNode(IGMVariable variable, VariableType referenceType, IExp
/// Meant for tracking obscure compiler quirks.
/// </summary>
public bool ForceSelf { get; set; } = false;

/// <inheritdoc/>
public bool Duplicated { get; set; } = false;

Expand Down
10 changes: 10 additions & 0 deletions Underanalyzer/IGameContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ public interface IGameContext
/// </summary>
public bool UsingNewFunctionResolution { get; }

/// <summary>
/// <see langword="true"/> if the game uses special variable name cases for structs introduced in GameMaker 2024.13; <see langword="false"/> otherwise.
/// </summary>
public bool UsingStructSpecialCaseNames { get; }

/// <summary>
/// <see langword="true"/> if the game uses bytecode 14 or lower; <see langword="true"/> otherwise.
/// </summary>
Expand Down Expand Up @@ -220,6 +225,11 @@ public interface IGameContext
/// </remarks>
public bool UsingModernTemplateStrings { get; }

/// <summary>
/// <see langword="true"/> if the game supports any non-empty string as a struct variable, introduced in GameMaker 2024.14; <see langword="false"/> otherwise.
/// </summary>
public bool UsingStructAnyNonemptyString { get; }

/// <summary>
/// Interface for getting global functions.
/// Can be custom, or can use the provided implementation of <see cref="Decompiler.GlobalFunctions"/>.
Expand Down
1 change: 1 addition & 0 deletions Underanalyzer/Mock/BuiltinsMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class BuiltinsMock : IBuiltins
{ "script_execute", new("script_execute", 1, int.MaxValue) },
{ "array_set", new("array_set", 3, 3) },
{ "array_create", new("array_create", 1, 2) },
{ VMConstants.StructSetFunction, new(VMConstants.StructSetFunction, 3, 3) },
{ VMConstants.SelfFunction, new(VMConstants.SelfFunction, 0, 0) },
{ VMConstants.OtherFunction, new(VMConstants.OtherFunction, 0, 0) },
{ VMConstants.GlobalFunction, new(VMConstants.GlobalFunction, 0, 0) },
Expand Down
4 changes: 4 additions & 0 deletions Underanalyzer/Mock/GameContextMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class GameContextMock : IGameContext
/// <inheritdoc/>
public bool UsingNewFunctionVariables { get; set; } = false;
/// <inheritdoc/>
public bool UsingStructSpecialCaseNames { get; set; } = false;
/// <inheritdoc/>
public bool UsingSelfToBuiltin { get; set; } = false;
/// <inheritdoc/>
public bool UsingGlobalConstantFunction { get; set; } = false;
Expand Down Expand Up @@ -74,6 +76,8 @@ public class GameContextMock : IGameContext
/// <inheritdoc/>
public bool UsingModernTemplateStrings { get; set; } = true;
/// <inheritdoc/>
public bool UsingStructAnyNonemptyString { get; set; } = false;
/// <inheritdoc/>
public IGlobalFunctions GlobalFunctions { get; } = new GlobalFunctions();
/// <inheritdoc/>
public GameSpecificRegistry GameSpecificRegistry { get; set; } = new();
Expand Down
21 changes: 16 additions & 5 deletions Underanalyzer/Mock/VMAssemblyMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,12 @@ public static GMCode ParseAssemblyFromLines(IEnumerable<string> lines, IGameCont
{
throw new Exception("Pop needs parameter");
}
string data = string.Join(' ', parts[1..]).Trim();

// Parse swap variant
if (instr.Type1 == IGMInstruction.DataType.Int16)
{
if (!byte.TryParse(parts[1], out byte popSwapSize) || popSwapSize < 5 || popSwapSize > 6)
if (!byte.TryParse(data, out byte popSwapSize) || popSwapSize < 5 || popSwapSize > 6)
{
throw new Exception("Unexpected pop swap size");
}
Expand All @@ -260,9 +261,9 @@ public static GMCode ParseAssemblyFromLines(IEnumerable<string> lines, IGameCont
}

// Parse variable destination
if (!ParseVariableFromString(parts[1], variables, out var variable, out var varType, out var instType))
if (!ParseVariableFromString(data, variables, out var variable, out var varType, out var instType))
{
throw new Exception($"Failed to parse variable {parts[1]}");
throw new Exception($"Failed to parse variable {data}");
}
instr.ResolvedVariable = variable;
instr.ReferenceVarType = varType;
Expand Down Expand Up @@ -346,7 +347,7 @@ public static GMCode ParseAssemblyFromLines(IEnumerable<string> lines, IGameCont
{
throw new Exception("Push instruction needs data");
}
string data = parts[1];
string data = string.Join(' ', parts[1..]).Trim();

switch (type1)
{
Expand Down Expand Up @@ -402,7 +403,7 @@ public static GMCode ParseAssemblyFromLines(IEnumerable<string> lines, IGameCont
case IGMInstruction.DataType.Variable:
if (!ParseVariableFromString(data, variables, out var variable, out var varType, out var instType))
{
throw new Exception($"Failed to parse variable {parts[1]}");
throw new Exception($"Failed to parse variable {data}");
}
instr.ResolvedVariable = variable;
instr.ReferenceVarType = varType;
Expand Down Expand Up @@ -580,6 +581,16 @@ private static bool ParseVariableFromString(
// Get actual variable name
str = str[(dot + 1)..];

// If the variable name starts with ", treat it as a string we need to deal with escapes in
if (str.StartsWith('"'))
{
if (!str.EndsWith('"'))
{
throw new Exception("Expected end quote at end of variable name");
}
str = UnescapeStringContents(str[1..^1]);
}

// Update variable
variable = new GMVariable(new GMString(str))
{
Expand Down
55 changes: 55 additions & 0 deletions Underanalyzer/VMConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ internal static class VMConstants
// Function name used to set struct variables (used to de-optimize to be closer to source code)
public const string StructGetFromHashFunction = "struct_get_from_hash";

// Function name used to set a variable on a struct directly, by string name
public const string StructSetFunction = "variable_struct_set";

// Special-case GML functions used during macro resolution
public const string ChooseFunction = "choose";
public const string ScriptExecuteFunction = "script_execute";
Expand Down Expand Up @@ -91,4 +94,56 @@ internal static class VMConstants
"phy_col_normal_x",
"phy_col_normal_y"
];

// Keywords disallowed to be directly used in structs, without quotes (in older versions)
public static readonly HashSet<string> OldDisallowedStructKeywords =
[
"if",
"then",
"else",
"begin",
"end",
"for",
"while",
"do",
"until",
"repeat",
"switch",
"case",
"default",
"break",
"continue",
"with",
"new",
"function",
"return",
"exit",
"var",
"not",
"and",
"or",
"xor",
"mod",
"div",
"throw",
"static",
"try",
"catch",
"finally",
"enum"
];

// Keywords disallowed to be directly used in structs, without quotes (in newer versions)
public static readonly HashSet<string> ModernDisallowedStructKeywords =
[
"end",
"not",
"and",
"or",
"xor",
"mod",
"div",
"enum",
"function"
];
}
Loading