From b173a0291210b011287c51c7d50bcdcd14ddc899 Mon Sep 17 00:00:00 2001 From: HumanoidSandvichDispenser Date: Tue, 26 Sep 2023 10:23:04 -0700 Subject: [PATCH] debug console transpiles GDScript to GD Expression --- Debug/CharIterator.cs | 2 +- Debug/DebugConsole.cs | 66 ++++++-- Debug/Entry.cs | 30 +++- Debug/Sanitizer.cs | 16 +- Debug/Token.cs | 49 ------ Debug/Tokenizer.cs | 185 --------------------- Debug/Transpiler/AssignmentExpression.cs | 25 +++ Debug/Transpiler/CallExpression.cs | 25 +++ Debug/Transpiler/Expression.cs | 16 ++ Debug/Transpiler/GroupedExpression.cs | 17 ++ Debug/Transpiler/LiteralExpression.cs | 34 ++++ Debug/Transpiler/OperationExpression.cs | 40 +++++ Debug/Transpiler/Parser.cs | 145 +++++++++++++++++ Debug/Transpiler/Token.cs | 64 ++++++++ Debug/Transpiler/Tokenizer.cs | 182 +++++++++++++++++++++ Debug/Transpiler/Transpiler.cs | 15 ++ SupaLidlGame.csproj | 5 +- SupaLidlGame.sln | 14 +- UI/Debug/DebugUI.tscn | 6 +- UnitTests/.gitignore | 3 + UnitTests/GodotTranspilerTest.cs | 194 +++++++++++++++++++++++ UnitTests/UnitTest1.cs | 10 ++ UnitTests/UnitTests.csproj | 28 ++++ UnitTests/Usings.cs | 1 + 24 files changed, 911 insertions(+), 261 deletions(-) create mode 100644 Debug/Transpiler/AssignmentExpression.cs create mode 100644 Debug/Transpiler/CallExpression.cs create mode 100644 Debug/Transpiler/Expression.cs create mode 100644 Debug/Transpiler/GroupedExpression.cs create mode 100644 Debug/Transpiler/LiteralExpression.cs create mode 100644 Debug/Transpiler/OperationExpression.cs create mode 100644 Debug/Transpiler/Parser.cs create mode 100644 Debug/Transpiler/Token.cs create mode 100644 Debug/Transpiler/Tokenizer.cs create mode 100644 Debug/Transpiler/Transpiler.cs create mode 100644 UnitTests/.gitignore create mode 100644 UnitTests/GodotTranspilerTest.cs create mode 100644 UnitTests/UnitTest1.cs create mode 100644 UnitTests/UnitTests.csproj create mode 100644 UnitTests/Usings.cs diff --git a/Debug/CharIterator.cs b/Debug/CharIterator.cs index 124cfc1..889a9a9 100644 --- a/Debug/CharIterator.cs +++ b/Debug/CharIterator.cs @@ -1,6 +1,6 @@ namespace SupaLidlGame.Debug; -internal sealed class CharIterator : Iterator +public class CharIterator : Iterator { public CharIterator(string str) : base(str.ToCharArray()) { diff --git a/Debug/DebugConsole.cs b/Debug/DebugConsole.cs index b0ae227..de02685 100644 --- a/Debug/DebugConsole.cs +++ b/Debug/DebugConsole.cs @@ -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 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(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) diff --git a/Debug/Entry.cs b/Debug/Entry.cs index 323bee6..f2e43c8 100644 --- a/Debug/Entry.cs +++ b/Debug/Entry.cs @@ -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 = ""; + } } } } diff --git a/Debug/Sanitizer.cs b/Debug/Sanitizer.cs index 4741ada..3f9341b 100644 --- a/Debug/Sanitizer.cs +++ b/Debug/Sanitizer.cs @@ -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; diff --git a/Debug/Token.cs b/Debug/Token.cs index 6a52e93..e69de29 100644 --- a/Debug/Token.cs +++ b/Debug/Token.cs @@ -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); -} diff --git a/Debug/Tokenizer.cs b/Debug/Tokenizer.cs index 4dcfd1d..e69de29 100644 --- a/Debug/Tokenizer.cs +++ b/Debug/Tokenizer.cs @@ -1,185 +0,0 @@ -using System.Collections.Generic; - -namespace SupaLidlGame.Debug; - -internal sealed class Tokenizer -{ - private static readonly HashSet WHITESPACE = new HashSet { ' ', '\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 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); - } -} - diff --git a/Debug/Transpiler/AssignmentExpression.cs b/Debug/Transpiler/AssignmentExpression.cs new file mode 100644 index 0000000..73892b3 --- /dev/null +++ b/Debug/Transpiler/AssignmentExpression.cs @@ -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})"; + } +} diff --git a/Debug/Transpiler/CallExpression.cs b/Debug/Transpiler/CallExpression.cs new file mode 100644 index 0000000..2f3bac5 --- /dev/null +++ b/Debug/Transpiler/CallExpression.cs @@ -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})"; + } +} diff --git a/Debug/Transpiler/Expression.cs b/Debug/Transpiler/Expression.cs new file mode 100644 index 0000000..67de1b7 --- /dev/null +++ b/Debug/Transpiler/Expression.cs @@ -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(); +} diff --git a/Debug/Transpiler/GroupedExpression.cs b/Debug/Transpiler/GroupedExpression.cs new file mode 100644 index 0000000..e3a9b76 --- /dev/null +++ b/Debug/Transpiler/GroupedExpression.cs @@ -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()})"; + } +} diff --git a/Debug/Transpiler/LiteralExpression.cs b/Debug/Transpiler/LiteralExpression.cs new file mode 100644 index 0000000..08a8283 --- /dev/null +++ b/Debug/Transpiler/LiteralExpression.cs @@ -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}\")"; + } +} diff --git a/Debug/Transpiler/OperationExpression.cs b/Debug/Transpiler/OperationExpression.cs new file mode 100644 index 0000000..56dff4d --- /dev/null +++ b/Debug/Transpiler/OperationExpression.cs @@ -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}"; + } + } +} diff --git a/Debug/Transpiler/Parser.cs b/Debug/Transpiler/Parser.cs new file mode 100644 index 0000000..f28cf16 --- /dev/null +++ b/Debug/Transpiler/Parser.cs @@ -0,0 +1,145 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SupaLidlGame.Debug.Transpiler; + +public class Parser +{ + private HashSet _endTokens = null; + + private Iterator _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 { default }; + } + + private Parser(Iterator iterator, HashSet endTokens) + { + _iterator = iterator; + _endTokens = endTokens; + } + + public GroupedExpression GroupedExpression() + { + Parser p = new Parser(_iterator, new HashSet { CLOSE_DELIM }); + var next = p.NextExpression(null); + Expect(CLOSE_DELIM); + _iterator.MoveNext(); + return new GroupedExpression(next, next.Line, next.Column); + } + + public IEnumerable DelimitedExpressions(Token delim, Token end) + { + Expect(end); + + var endTokens = new HashSet { 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 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(); + } + } +} diff --git a/Debug/Transpiler/Token.cs b/Debug/Transpiler/Token.cs new file mode 100644 index 0000000..f06f509 --- /dev/null +++ b/Debug/Transpiler/Token.cs @@ -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); +} diff --git a/Debug/Transpiler/Tokenizer.cs b/Debug/Transpiler/Tokenizer.cs new file mode 100644 index 0000000..fa575ad --- /dev/null +++ b/Debug/Transpiler/Tokenizer.cs @@ -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 WHITESPACE = new HashSet + { + ' ', + '\n' + }; + + private readonly HashSet OPERATOR = new HashSet + { + '+', + '-', + '*', + '/', + '.', + ',', + '=', + '!', + }; + + private readonly HashSet GROUPING = new HashSet + { + '(', + ')', + }; + + private readonly HashSet STRING_DELIM = new HashSet + { + '"', + '\'', + }; + + 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 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); + } + } + } +} diff --git a/Debug/Transpiler/Transpiler.cs b/Debug/Transpiler/Transpiler.cs new file mode 100644 index 0000000..20ff883 --- /dev/null +++ b/Debug/Transpiler/Transpiler.cs @@ -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(); + } +} diff --git a/SupaLidlGame.csproj b/SupaLidlGame.csproj index e7f1627..ff2346e 100644 --- a/SupaLidlGame.csproj +++ b/SupaLidlGame.csproj @@ -4,6 +4,9 @@ true + + + - \ No newline at end of file + diff --git a/SupaLidlGame.sln b/SupaLidlGame.sln index f6135a2..0447fd4 100644 --- a/SupaLidlGame.sln +++ b/SupaLidlGame.sln @@ -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 diff --git a/UI/Debug/DebugUI.tscn b/UI/Debug/DebugUI.tscn index 1d347f7..81c52e9 100644 --- a/UI/Debug/DebugUI.tscn +++ b/UI/Debug/DebugUI.tscn @@ -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") diff --git a/UnitTests/.gitignore b/UnitTests/.gitignore new file mode 100644 index 0000000..fdd3cc3 --- /dev/null +++ b/UnitTests/.gitignore @@ -0,0 +1,3 @@ +# output +bin/ +obj/ diff --git a/UnitTests/GodotTranspilerTest.cs b/UnitTests/GodotTranspilerTest.cs new file mode 100644 index 0000000..2e4253c --- /dev/null +++ b/UnitTests/GodotTranspilerTest.cs @@ -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(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(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(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(exprs[0]); + var call = exprs[0] as CallExpression; + Assert.IsType(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(exprs[0]); + var call = exprs[0] as AssignmentExpression; + Assert.NotEqual(null, call.Left); + Assert.IsType(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()); + } +} diff --git a/UnitTests/UnitTest1.cs b/UnitTests/UnitTest1.cs new file mode 100644 index 0000000..14e3fcc --- /dev/null +++ b/UnitTests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace SupaLidlGame.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..bea46e4 --- /dev/null +++ b/UnitTests/UnitTests.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/UnitTests/Usings.cs b/UnitTests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file