debug console transpiles GDScript to GD Expression

godot-4.2
John Montagu, the 4th Earl of Sandvich 2023-09-26 10:23:04 -07:00
parent ad29c9cd29
commit b173a02912
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
24 changed files with 911 additions and 261 deletions

View File

@ -1,6 +1,6 @@
namespace SupaLidlGame.Debug;
internal sealed class CharIterator : Iterator<char>
public class CharIterator : Iterator<char>
{
public CharIterator(string str) : base(str.ToCharArray())
{

View File

@ -1,4 +1,5 @@
using Godot;
using System.Collections.Generic;
namespace SupaLidlGame.Debug;
@ -55,11 +56,21 @@ public sealed partial class DebugConsole : Control
Property
};
public Variant From(NodePath path)
public IEnumerable<NodePathToken> SplitPath(NodePath path)
{
CharIterator iterator = new(path);
return NodePathParser.ParseNodePath(iterator);
}
public NodePath ToNodePath(string path)
{
return Variant.From(path).AsNodePath();
}
public Variant From(NodePath path)
{
Variant variant = Context ?? this;
foreach (var subpath in NodePathParser.ParseNodePath(iterator))
foreach (var subpath in SplitPath(path))
{
if (variant.VariantType == Variant.Type.Object)
{
@ -83,14 +94,44 @@ public sealed partial class DebugConsole : Control
return variant;
}
public void SetProp(NodePath path, Variant value)
public void SetProp(Variant prop, Variant value)
{
var node = GetNode(path.GetAsPropertyPath());
//var ent = CurrentMap.Entities.GetNodeOrNull(entityName);
//if (ent is not null)
//{
// ent.Set(property, value);
//}
if (prop.VariantType == Variant.Type.NodePath)
{
var tokens = new List<NodePathToken>(SplitPath(prop.AsNodePath()));
Node variant = Context ?? this;
for (int i = 0; i < tokens.Count; i++)
{
var subpath = tokens[i];
GD.Print(subpath);
if (i == tokens.Count - 1)
{
if (subpath.Type == NodePathTokenType.Property)
{
variant.SetIndexed(":" + subpath.Path, value);
}
}
else
{
if (subpath.Type == NodePathTokenType.Node)
{
if (subpath.Path != "")
{
variant = variant.GetNode(subpath.Path);
}
}
else
{
variant = variant.GetIndexed(subpath.Path)
.AsGodotObject() as Node;
}
}
}
}
else
{
}
}
public string CallMethod(
@ -115,18 +156,21 @@ public sealed partial class DebugConsole : Control
public void Execute(string str)
{
str = Sanitizer.Sanitize(str);
//str = Sanitizer.Sanitize(str);
str = Transpiler.Transpiler.Transpile(str);
string inputMirror = $"[b]{Context.GetPath()}:[/b] {str}";
_output.Text += inputMirror + "\n";
var context = Context;
Godot.Expression exp = new();
string[] reserved = { "from", "set_context", "context" };
string[] reserved = { "from", "set_context", "context", "set_prop", "to_node_path" };
Godot.Collections.Array reservedMap = new();
reservedMap.Add(new Callable(this, MethodName.From));
reservedMap.Add(new Callable(this, MethodName.SetContext));
reservedMap.Add(Context);
reservedMap.Add(new Callable(this, MethodName.SetProp));
reservedMap.Add(new Callable(this, MethodName.ToNodePath));
var err = exp.Parse(str, reserved);
if (err != Error.Ok)

View File

@ -2,7 +2,7 @@ using Godot;
namespace SupaLidlGame.Debug;
public partial class Entry : LineEdit
public partial class Entry : CodeEdit
{
[Signal]
public delegate void ConsoleInputEventHandler(string input);
@ -12,17 +12,35 @@ public partial class Entry : LineEdit
GuiInput += OnGuiInput;
}
/*
public override void _Input(InputEvent @event)
{
if (HasFocus())
{
if (@event is InputEventKey && @event.IsPressed())
{
AcceptEvent();
OnGuiInput(@event);
}
}
}
*/
public void OnGuiInput(InputEvent @event)
{
if (@event is InputEventKey key)
{
if (key.KeyLabel == Key.Enter && !key.Pressed)
if (key.KeyLabel == Key.Enter)
{
EmitSignal(SignalName.ConsoleInput, Text);
if (!key.CtrlPressed)
AcceptEvent();
if (!key.Pressed)
{
Text = "";
EmitSignal(SignalName.ConsoleInput, Text);
if (!key.IsCommandOrControlPressed())
{
Text = "";
}
}
}
}

View File

@ -1,4 +1,3 @@
using Godot;
using System.Text.RegularExpressions;
namespace SupaLidlGame.Debug;
@ -7,7 +6,7 @@ public static class Sanitizer
{
private static Regex _nonAlphanum = new("[^a-zA-Z0-9_]");
private static Regex _nonNodeName = new("[^a-zA-Z0-9_\\-\\/]");
private static Regex _nonNodeName = new("[^a-zA-Z0-9_\\-\\/\\.\\:]");
private static string ScanString(CharIterator iterator)
{
@ -69,12 +68,12 @@ public static class Sanitizer
return ret;
}
private static string ScanUntilOrEOL(CharIterator iterator, char delim)
private static string ScanUntilOrEOL(CharIterator iterator, char? delim)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
char c = iterator.MoveNext();
if (c == delim)
{
return ret;
@ -125,6 +124,15 @@ public static class Sanitizer
string command = ScanGlobalCommand(iterator);
ret += $"{command}.call";
}
else if (c == '=')
{
if (iterator.GetNext(-2) != '!')
{
var val = ScanUntilOrEOL(iterator, null);
ret = ret.Replace("from.call", "to_node_path.call");
ret = $"set_prop.call({ret}, {val})";
}
}
else
{
ret += c;

View File

@ -1,49 +0,0 @@
namespace SupaLidlGame.Debug;
public enum TokenType
{
None,
Identifier,
String,
GodotExpression,
Command,
End
};
public struct Token
{
public TokenType Type { get; set; }
public string Value { get; set; }
public int Line { get; set; }
public int Column { get; set; }
public Token(TokenType type, string value, int line, int col)
{
Type = type;
Value = value;
Line = line;
Column = col;
}
public bool CompareTypeValue(Token token)
{
return Type == token.Type && Value == token.Value;
}
public override bool Equals(object obj)
{
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(Token left, Token right) => left.Equals(right);
public static bool operator !=(Token left, Token right) => !left.Equals(right);
}

View File

@ -1,185 +0,0 @@
using System.Collections.Generic;
namespace SupaLidlGame.Debug;
internal sealed class Tokenizer
{
private static readonly HashSet<char> WHITESPACE = new HashSet<char> { ' ', '\n' };
private static string ScanString(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (c == '"')
{
return ret;
}
else if (c == '\\')
{
char escape = iterator.MoveNext();
switch (escape)
{
case 'n':
ret += '\n';
break;
case 't':
ret += '\t';
break;
case '\0':
throw new InterpreterException("Unexpected EOL",
iterator.Line, iterator.Column);
default:
ret += escape;
break;
}
}
ret += c;
}
throw new InterpreterException("Unexpected EOL, expected '\"'",
iterator.Line, iterator.Column);
}
private static string ScanNodePath(CharIterator iterator)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == '"')
{
ret += ScanString(iterator);
}
else if (WHITESPACE.Contains(c))
{
return ret;
}
ret += c;
}
return ret;
}
/*
private static string ScanUntil(CharIterator iterator)
{
}
*/
private static string ScanExpression(CharIterator iterator)
{
int level = 0;
string exp = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == '(')
{
level++;
}
else if (c == ')')
{
level--;
}
if (level < 0)
{
return exp;
}
exp += c;
}
return exp;
}
private static string ScanUntilOrEOL(CharIterator iterator, char delim)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.GetNext();
if (c == delim)
{
return ret;
}
ret += c;
}
return ret;
}
public static IEnumerable<Token> Tokenize(CharIterator iterator)
{
System.Diagnostics.Debug.Print("hi");
while (iterator.GetNext() != '\0')
{
char curChar = iterator.MoveNext();
System.Diagnostics.Debug.Print(curChar.ToString());
int line = iterator.Line;
int col = iterator.Column;
if (WHITESPACE.Contains(curChar))
{
continue;
}
else if (curChar == '\\')
{
string command = ScanUntilOrEOL(iterator, ' ');
if (command == "")
{
throw new InterpreterException(
"Expected a command name",
iterator.Line,
iterator.Column);
}
yield return new Token(TokenType.Command,
command,
line,
col);
}
else if (curChar == '(')
{
string exp = ScanExpression(iterator);
yield return new Token(TokenType.GodotExpression,
exp,
line,
col);
}
else if (curChar == '"')
{
yield return new Token(TokenType.String,
ScanString(iterator),
line,
col);
}
else
{
// parse this as expression
string exp = ScanUntilOrEOL(iterator, ' ');
yield return new Token(TokenType.GodotExpression,
exp,
line,
col);
}
/*
else if (curChar == '$')
{
yield return new Token(TokenType.NodePath,
ScanNodePath(iterator),
line,
col);
}
*/
}
yield return new Token(TokenType.End, "",
iterator.Line, iterator.Column);
}
}

View File

@ -0,0 +1,25 @@
namespace SupaLidlGame.Debug.Transpiler;
public class AssignmentExpression : Expression
{
public LiteralExpression Left { get; set; }
public Expression Expression { get; set; }
public AssignmentExpression(LiteralExpression left, Expression expression,
int line, int col) : base(line, col)
{
Left = left;
Expression = expression;
}
public override string Transpile()
{
var right = Expression.Transpile();
if (Left.Literal.Type == TokenType.NodePath)
{
return $"set_prop.call({Left.TranspileNodePath()}, {right})";
}
return $"set(\"{Left.Transpile()}\", {right})";
}
}

View File

@ -0,0 +1,25 @@
using System.Linq;
namespace SupaLidlGame.Debug.Transpiler;
public class CallExpression : Expression
{
public Expression Identifier { get; set; }
public Expression[] Arguments { get; set; }
public CallExpression(LiteralExpression identifier, Expression[] args,
int line, int col) : base(line, col)
{
Identifier = identifier;
Arguments = args;
}
public override string Transpile()
{
var args = Arguments
.Select((ex) => ex.Transpile())
.Aggregate((a, b) => a + ", " + b);
return $"{Identifier.Transpile()}({args})";
}
}

View File

@ -0,0 +1,16 @@
namespace SupaLidlGame.Debug.Transpiler;
public abstract class Expression
{
public int Line { get; set; }
public int Column { get; set; }
public Expression(int line, int col)
{
Line = line;
Column = col;
}
public abstract string Transpile();
}

View File

@ -0,0 +1,17 @@
namespace SupaLidlGame.Debug.Transpiler;
public class GroupedExpression : Expression
{
public Expression Root { get; set; }
public GroupedExpression(Expression root, int line, int col)
: base(line, col)
{
Root = root;
}
public override string Transpile()
{
return $"({Root.Transpile()})";
}
}

View File

@ -0,0 +1,34 @@
using System.Text.RegularExpressions;
namespace SupaLidlGame.Debug.Transpiler;
public class LiteralExpression : Expression
{
public Token Literal { get; set; }
public LiteralExpression(Token literal, int line, int col)
: base(line, col)
{
Literal = literal;
}
public override string Transpile()
{
if (Literal.Type == TokenType.NodePath)
{
var val = Regex.Escape(Literal.Value);
return $"from.call({val})";
}
else if (Literal.Type == TokenType.String)
{
return $"\"{Literal.Value}\"";
}
return Literal.Value;
}
public string TranspileNodePath()
{
var val = Regex.Escape(Literal.Value);
return $"to_node_path.call(\"{val}\")";
}
}

View File

@ -0,0 +1,40 @@
namespace SupaLidlGame.Debug.Transpiler;
public class OperationExpression : Expression
{
public Expression Left { get; set; }
public Expression Right { get; set; }
public Token Operator { get; set; }
public OperationExpression(Expression left, Token token, Expression right,
int line, int col) : base(line, col)
{
if (token.Type != TokenType.Operator)
{
throw new InterpreterException(
$"Expected operator, got {token.Value}",
token.Line,
token.Column);
}
Left = left;
Operator = token;
Right = right;
}
public override string Transpile()
{
var left = Left.Transpile();
var right = Right.Transpile();
var op = Operator.Value;
if (op == ".")
{
return $"{left}{op}{right}";
}
else
{
return $"{left} {op} {right}";
}
}
}

View File

@ -0,0 +1,145 @@
using System.Collections.Generic;
using System.Linq;
namespace SupaLidlGame.Debug.Transpiler;
public class Parser
{
private HashSet<Token> _endTokens = null;
private Iterator<Token> _iterator;
private static readonly Token ARGS_DELIM = new Token(TokenType.Operator, ",", 0, 0);
private static readonly Token CLOSE_DELIM = new Token(TokenType.Grouping, ")", 0, 0);
private Parser(Token[] tokens)
{
_iterator = new(tokens);
_endTokens = new HashSet<Token> { default };
}
private Parser(Iterator<Token> iterator, HashSet<Token> endTokens)
{
_iterator = iterator;
_endTokens = endTokens;
}
public GroupedExpression GroupedExpression()
{
Parser p = new Parser(_iterator, new HashSet<Token> { CLOSE_DELIM });
var next = p.NextExpression(null);
Expect(CLOSE_DELIM);
_iterator.MoveNext();
return new GroupedExpression(next, next.Line, next.Column);
}
public IEnumerable<Expression> DelimitedExpressions(Token delim, Token end)
{
Expect(end);
var endTokens = new HashSet<Token> { delim, end };
Parser p = new Parser(_iterator, endTokens);
var next = _iterator.GetNext();
while (next != end)
{
var expr = p.NextExpression(null);
if (expr is not null)
{
yield return expr;
}
next = _iterator.MoveNext();
}
}
public Expression NextExpression(Expression prev)
{
foreach (var end in _endTokens)
{
if (end == _iterator.GetNext())
{
return prev;
}
}
var token = _iterator.MoveNext();
if (prev is null && token.IsSymbol)
{
var exp = new LiteralExpression(token, token.Line, token.Column);
return NextExpression(exp);
}
else if (token.Type == TokenType.Operator)
{
Expression right = NextExpression(null);
if (token.Value == "=")
{
if (prev is not LiteralExpression l)
{
throw new InterpreterException("Invalid assignment",
prev.Line, prev.Column);
}
var assignment = new AssignmentExpression(l, right,
token.Line, token.Column);
return NextExpression(assignment);
}
var exp = new OperationExpression(
prev, token, right, token.Line, token.Column);
return NextExpression(exp);
}
else if (token.Type == TokenType.Grouping)
{
if (token.Value == ")")
{
throw new InterpreterException("Unexpected )",
token.Line, token.Column);
}
if (prev is LiteralExpression l)
{
// this is a function call
Expression[] args =
DelimitedExpressions(ARGS_DELIM, CLOSE_DELIM)
.ToArray();
var c = new CallExpression(l, args, token.Line, token.Column);
return NextExpression(c);
}
else
{
// otherwise it's just a grouping
return NextExpression(GroupedExpression());
}
}
throw new InterpreterException($"Unexpected token {token.Value}",
token.Line, token.Column);
}
public void Expect(Token token)
{
var next = _iterator.GetNext();
if (next == default)
{
var cur = _iterator.GetNext(-1);
throw new InterpreterException($"Expected {token.Value}",
cur.Line, cur.Column);
}
}
public static IEnumerable<Expression> Parse(Token[] tokens)
{
var parser = new Parser(tokens);
var iterator = parser._iterator;
while (iterator.GetNext() != default)
{
var expr = parser.NextExpression(null);
if (expr is not null)
{
yield return expr;
}
iterator.MoveNext();
}
}
}

View File

@ -0,0 +1,64 @@
namespace SupaLidlGame.Debug.Transpiler;
public enum TokenType
{
None,
Identifier,
Grouping,
Operator,
String,
Number,
NodePath,
}
public struct Token
{
//public static Token EndToken => new Token(TokenType.End, ";", -1, -1);
public TokenType Type { get; set; }
public string Value { get; set; }
public int Line { get; set; }
public int Column { get; set; }
public Token(TokenType type, string value, int line, int col)
{
Type = type;
Value = value;
Line = line;
Column = col;
}
#if DEBUG
public override string ToString()
{
return $"{Type} - {Value}\t\t@{Line}:{Column}";
}
#endif
public bool CompareTypeValue(Token token)
{
return Type == token.Type && Value == token.Value;
}
public override bool Equals(object o)
{
return base.Equals(o);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public bool IsLiteral => Type == TokenType.String ||
Type == TokenType.Number ||
Type == TokenType.NodePath;
public bool IsSymbol => IsLiteral || Type == TokenType.Identifier;
public static bool operator ==(Token left, Token right)
{
return left.Type == right.Type && left.Value == right.Value;
}
public static bool operator !=(Token left, Token right) => !(left == right);
}

View File

@ -0,0 +1,182 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SupaLidlGame.Debug.Transpiler;
public sealed class Tokenizer
{
public readonly char DECIMAL_POINT = '.';
public readonly char DECIMAL_SUBSEPARATOR = ',';
public readonly char NODE_PATH_PREFIX = '$';
private readonly HashSet<char> WHITESPACE = new HashSet<char>
{
' ',
'\n'
};
private readonly HashSet<char> OPERATOR = new HashSet<char>
{
'+',
'-',
'*',
'/',
'.',
',',
'=',
'!',
};
private readonly HashSet<char> GROUPING = new HashSet<char>
{
'(',
')',
};
private readonly HashSet<char> STRING_DELIM = new HashSet<char>
{
'"',
'\'',
};
private readonly Regex REGEX_NUMBER = new Regex("[.0-9]");
private readonly Regex REGEX_IDENTIFIER_START = new Regex("[_a-zA-Z]");
private readonly Regex REGEX_IDENTIFIER = new Regex("[_a-zA-Z0-9]");
private Regex NON_NODE_PATH = new("[^a-zA-Z0-9_\\-\\/\\.\\:]");
private static string ScanString(CharIterator iterator, char delim = '"')
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (c == delim)
{
return ret;
}
else if (c == '\\')
{
char escape = iterator.MoveNext();
switch (escape)
{
case 'n':
ret += '\n';
break;
case 't':
ret += '\t';
break;
case '\0':
throw new InterpreterException("Unexpected EOL, " +
"expected proper string termination",
iterator.Line, iterator.Column);
default:
ret += escape;
break;
}
}
else
{
ret += c;
}
}
throw new InterpreterException($"Unexpected EOL, expected: {delim}",
iterator.Line, iterator.Column);
}
private string ScanNodePath(CharIterator iterator)
{
string ret = "";
bool isAtStart = true;
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (isAtStart && STRING_DELIM.Contains(c))
{
isAtStart = false;
return ScanString(iterator, c);
}
else if (NON_NODE_PATH.IsMatch(c.ToString()))
{
iterator.MoveBack();
return ret;
}
isAtStart = false;
ret += c;
}
return ret;
}
private string ScanRegex(CharIterator iterator, Regex regex)
{
string ret = "";
while (iterator.GetNext() != '\0')
{
char c = iterator.MoveNext();
if (!regex.IsMatch(c.ToString()))
{
iterator.MoveBack();
return ret;
}
ret += c;
}
return ret;
}
public IEnumerable<Token> Lex(CharIterator iterator)
{
//Token curToken = new Token(TokenType.Any, );
while (iterator.GetNext() != default)
{
char c = iterator.MoveNext();
int line = iterator.Line;
int col = iterator.Column;
if (GROUPING.Contains(c))
{
yield return new Token(TokenType.Grouping,
c.ToString(), line, col);
}
else if (OPERATOR.Contains(c))
{
yield return new Token(TokenType.Operator,
c.ToString(), line, col);
}
else if (c == NODE_PATH_PREFIX)
{
yield return new Token(TokenType.NodePath,
ScanNodePath(iterator), line, col);
}
else if (STRING_DELIM.Contains(c))
{
yield return new Token(TokenType.String,
ScanString(iterator, c), line, col);
}
else if (REGEX_IDENTIFIER_START.IsMatch(c.ToString()))
{
yield return new Token(TokenType.Identifier,
c + ScanRegex(iterator, REGEX_IDENTIFIER), line, col);
}
else if (REGEX_NUMBER.IsMatch(c.ToString()))
{
yield return new Token(TokenType.Number,
c + ScanRegex(iterator, REGEX_NUMBER), line, col);
}
else if (WHITESPACE.Contains(c))
{
continue;
}
else
{
throw new InterpreterException($"Unknown symbol {c}",
line, col);
}
}
}
}

View File

@ -0,0 +1,15 @@
using System.Linq;
namespace SupaLidlGame.Debug.Transpiler;
public static class Transpiler
{
public static string Transpile(string source)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
var tokens = tokenizer.Lex(iterator).ToArray();
var exprs = Parser.Parse(tokens).ToArray();
return exprs[0].Transpile();
}
}

View File

@ -4,6 +4,9 @@
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<Content Remove="UnitTests/**/*" />
<Compile Remove="UnitTests/**/*" />
<None Remove="UnitTests/**/*" />
<PackageReference Include="Firebelley.GodotUtilities" Version="4.0.4" />
</ItemGroup>
</Project>

View File

@ -2,11 +2,13 @@
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SupaLidlGame", "SupaLidlGame.csproj", "{BC071CA6-9462-4CEC-AA20-B0CA618321E5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BC071CA6-9462-4CEC-AA20-B0CA618321E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@ -15,5 +17,11 @@ Global
{BC071CA6-9462-4CEC-AA20-B0CA618321E5}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{BC071CA6-9462-4CEC-AA20-B0CA618321E5}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{BC071CA6-9462-4CEC-AA20-B0CA618321E5}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
{5D279483-BBEE-46A7-B5B9-68F335BDD6ED}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
EndGlobal

View File

@ -48,10 +48,14 @@ text = "[b]/root/World:[/b] \\echo :CurrentPlayer:Health
"
scroll_following = true
[node name="Entry" type="LineEdit" parent="Window/DebugConsole/VBoxContainer"]
[node name="Entry" type="CodeEdit" parent="Window/DebugConsole/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 48)
layout_mode = 2
theme_override_font_sizes/font_size = 24
placeholder_text = "Enter a GDScript expression or \\command..."
draw_control_chars = true
code_completion_enabled = true
auto_brace_completion_enabled = true
auto_brace_completion_highlight_matching = true
script = ExtResource("2_kdlsh")

3
UnitTests/.gitignore vendored 100644
View File

@ -0,0 +1,3 @@
# output
bin/
obj/

View File

@ -0,0 +1,194 @@
using SupaLidlGame.Debug.Transpiler;
using Xunit.Abstractions;
namespace SupaLidlGame.UnitTests;
public class GodotTranspilerTest
{
private readonly ITestOutputHelper output;
public GodotTranspilerTest(ITestOutputHelper output)
{
this.output = output;
}
[Theory]
[InlineData("abc", TokenType.Identifier)]
[InlineData("123", TokenType.Number)]
[InlineData("$Node/Path:Property", TokenType.NodePath)]
public void Tokenize(string str, TokenType expectedType)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(str);
Token[] tokens = tokenizer.Lex(iterator).ToArray();
Assert.Equal(1, tokens.Length);
Assert.Equal(expectedType, tokens[0].Type);
}
[Theory]
[InlineData("abc", "abc")]
[InlineData("ABC_DEF", "ABC_DEF")]
[InlineData("\"str\"", "str")]
public void TokenizeIdentifier(string str, string expectedValue)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(str);
Token[] tokens = tokenizer.Lex(iterator).ToArray();
Assert.Equal(1, tokens.Length);
Assert.Equal(expectedValue, tokens[0].Value);
}
[Fact]
public void TokenizeSource()
{
string source = "abc $NodePath + operator=";
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
Token[] tokens = tokenizer.Lex(iterator).ToArray();
TokenType[] expectedTypes =
{
TokenType.Identifier,
TokenType.NodePath,
TokenType.Operator,
TokenType.Identifier,
TokenType.Operator,
};
Assert.Equal(tokens.Length, expectedTypes.Length);
for (int i = 0; i < tokens.Length; i++)
{
Assert.Equal(expectedTypes[i], tokens[i].Type);
}
}
[Theory]
[InlineData("$Some/Nested", "Some/Nested")]
[InlineData("$'Some/Nested'", "Some/Nested")]
[InlineData("$'With A/Path and:Property'", "With A/Path and:Property")]
[InlineData("$'broken as'hell", "broken as", 2)]
public void TestNodePath(string source, string expectedValue, int count = 1)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
Token[] tokens = tokenizer.Lex(iterator).ToArray();
Assert.Equal(count, tokens.Length);
Assert.Equal(expectedValue, tokens[0].Value);
}
[Theory]
[InlineData("\"this is a string\"", "this is a string")]
[InlineData("\"escape\\\\\"", "escape\\")]
public void TestStrings(string source, string expectedValue)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
Token[] tokens = tokenizer.Lex(iterator).ToArray();
Assert.Equal(expectedValue, tokens[0].Value);
}
[Theory]
[InlineData("()", 2)]
[InlineData("\"escape\\\\\"", 1)]
public void TestCount(string source, int expectedValue)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
Token[] tokens = tokenizer.Lex(iterator).ToArray();
Assert.Equal(expectedValue, tokens.Length);
}
[Fact]
public void TestAssignmentParsing()
{
Token[] tokens =
{
new(TokenType.Identifier, "x", 0, 0),
new(TokenType.Operator, "=", 0, 0),
new(TokenType.Identifier, "val", 0, 0),
};
Expression[] expr = Parser.Parse(tokens).ToArray();
Assert.Equal(1, expr.Length);
Assert.IsType<AssignmentExpression>(expr[0]);
}
[Fact]
public void TestAssignmentGrouping()
{
Token[] tokens =
{
new(TokenType.Identifier, "x", 0, 0),
new(TokenType.Grouping, "(", 0, 0),
new(TokenType.Identifier, "val", 0, 0),
new(TokenType.Operator, ",", 0, 0),
new(TokenType.Identifier, "val", 0, 0),
new(TokenType.Grouping, ")", 0, 0),
new(TokenType.Operator, "+", 0, 0),
new(TokenType.Grouping, "(", 0, 0),
new(TokenType.Number, "2", 0, 0),
new(TokenType.Grouping, ")", 0, 0),
};
Expression[] expr = Parser.Parse(tokens).ToArray();
Assert.Equal(1, expr.Length);
Assert.IsType<OperationExpression>(expr[0]);
var op = expr[0] as OperationExpression;
Assert.NotNull(op);
var call = op.Left as CallExpression;
Assert.NotNull(call);
Assert.Equal(2, call.Arguments.Length);
Assert.IsType<GroupedExpression>(op.Right);
}
[Fact]
public void TestExpressionTypes()
{
string source = "print(\"test\")";
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
var tokens = tokenizer.Lex(iterator).ToArray();
var exprs = Parser.Parse(tokens).ToArray();
Assert.IsType<CallExpression>(exprs[0]);
var call = exprs[0] as CallExpression;
Assert.IsType<LiteralExpression>(call.Arguments[0]);
}
[Fact]
public void TestExpressionTypes2()
{
string source = "x = \"val\"";
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
var tokens = tokenizer.Lex(iterator).ToArray();
var exprs = Parser.Parse(tokens).ToArray();
Assert.IsType<AssignmentExpression>(exprs[0]);
var call = exprs[0] as AssignmentExpression;
Assert.NotEqual(null, call.Left);
Assert.IsType<LiteralExpression>(call.Expression);
Assert.Equal("x", call.Left.Literal.Value);
Assert.Equal("x", call.Left.Transpile());
Assert.Equal("\"val\"", call.Expression.Transpile());
}
[Fact]
public void TestIndividualTranspilation()
{
var left = new LiteralExpression(new Token(TokenType.Identifier, "x", 0, 0), 0, 0);
var right = new LiteralExpression(new Token(TokenType.String, "lol", 0, 0), 0, 0);
var exp = new AssignmentExpression(left, right, 0, 0);
Assert.Equal("set(\"x\", \"lol\")", exp.Transpile());
}
[Theory]
[InlineData("x = 2", "set(\"x\", 2)")]
[InlineData("call(arg1, arg2)", "call(arg1, arg2)")]
[InlineData("call(arg1,arg2)", "call(arg1, arg2)")]
[InlineData("x = (a + b) *c+d + f(\"str\")", "set(\"x\", (a + b) * c + d + f(\"str\"))")]
[InlineData("3 + My_Func(x * 2, x*5)", "3 + My_Func(x * 2, x * 5)")]
public void TestTranspilation(string source, string transpiled)
{
var tokenizer = new Debug.Transpiler.Tokenizer();
SupaLidlGame.Debug.CharIterator iterator = new(source);
var tokens = tokenizer.Lex(iterator).ToArray();
var exprs = Parser.Parse(tokens).ToArray();
Assert.Equal(1, exprs.Length);
Assert.Equal(transpiled, exprs[0].Transpile());
}
}

View File

@ -0,0 +1,10 @@
namespace SupaLidlGame.UnitTests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SupaLidlGame.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
global using Xunit;