debug console transpiles GDScript to GD Expression
parent
ad29c9cd29
commit
b173a02912
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,15 +12,32 @@ 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)
|
||||
{
|
||||
AcceptEvent();
|
||||
if (!key.Pressed)
|
||||
{
|
||||
EmitSignal(SignalName.ConsoleInput, Text);
|
||||
|
||||
if (!key.CtrlPressed)
|
||||
if (!key.IsCommandOrControlPressed())
|
||||
{
|
||||
Text = "";
|
||||
}
|
||||
|
@ -28,3 +45,4 @@ public partial class Entry : LineEdit
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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})";
|
||||
}
|
||||
}
|
|
@ -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})";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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()})";
|
||||
}
|
||||
}
|
|
@ -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}\")";
|
||||
}
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -2,6 +2,8 @@
|
|||
# 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
|
||||
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# output
|
||||
bin/
|
||||
obj/
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
namespace SupaLidlGame.UnitTests;
|
||||
|
||||
public class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
global using Xunit;
|
Loading…
Reference in New Issue