Akeit0 9 месяцев назад
Родитель
Сommit
9a0eb84ba9
50 измененных файлов с 3449 добавлено и 6 удалено
  1. 4 4
      src/Lua/CodeAnalysis/Compilation/Dump.cs
  2. 1 1
      src/Lua/CodeAnalysis/Compilation/Scanner.cs
  3. 14 0
      src/Lua/CodeAnalysis/SourcePosition.cs
  4. 555 0
      src/Lua/CodeAnalysis/Syntax/DisplayStringSyntaxVisitor.cs
  5. 40 0
      src/Lua/CodeAnalysis/Syntax/ISyntaxNodeVisitor.cs
  6. 61 0
      src/Lua/CodeAnalysis/Syntax/Keywords.cs
  7. 543 0
      src/Lua/CodeAnalysis/Syntax/Lexer.cs
  8. 30 0
      src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs
  9. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/AssignmentStatementNode.cs
  10. 54 0
      src/Lua/CodeAnalysis/Syntax/Nodes/BinaryExpressionNode.cs
  11. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/BooleanLiteralNode.cs
  12. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/BreakStatementNode.cs
  13. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionExpressionNode.cs
  14. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionStatementNode.cs
  15. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodExpressionNode.cs
  16. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodStatementNode.cs
  17. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/DoStatementNode.cs
  18. 3 0
      src/Lua/CodeAnalysis/Syntax/Nodes/ExpressionNode.cs
  19. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationExpressionNode.cs
  20. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationStatementNode.cs
  21. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/GenericForStatementNode.cs
  22. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/GotoStatementNode.cs
  23. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/GroupedExpressionNode.cs
  24. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/IdentifierNode.cs
  25. 16 0
      src/Lua/CodeAnalysis/Syntax/Nodes/IfStatementNode.cs
  26. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/LabelStatementNode.cs
  27. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/LocalAssignmentStatementNode.cs
  28. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/LocalFunctionDeclarationNode.cs
  29. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/NilLiteralNode.cs
  30. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/NumericForStatementNode.cs
  31. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/NumericLiteralNode.cs
  32. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/RepeatStatementNode.cs
  33. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/ReturnStatementNode.cs
  34. 3 0
      src/Lua/CodeAnalysis/Syntax/Nodes/StatementNode.cs
  35. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/StringLiteralNode.cs
  36. 14 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableConstructorExpressionNode.cs
  37. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableIndexerAccessExpressionNode.cs
  38. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableMemberAccessExpressionNode.cs
  39. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/TableMethodDeclarationStatementNode.cs
  40. 30 0
      src/Lua/CodeAnalysis/Syntax/Nodes/UnaryExpressionNode.cs
  41. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/VariableArgumentsExpressionNode.cs
  42. 9 0
      src/Lua/CodeAnalysis/Syntax/Nodes/WhileStatementNode.cs
  43. 49 0
      src/Lua/CodeAnalysis/Syntax/OperatorPrecedence.cs
  44. 1037 0
      src/Lua/CodeAnalysis/Syntax/Parser.cs
  45. 6 0
      src/Lua/CodeAnalysis/Syntax/SyntaxNode.cs
  46. 364 0
      src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs
  47. 60 0
      src/Lua/CodeAnalysis/Syntax/SyntaxTokenEnumerator.cs
  48. 43 1
      src/Lua/Exceptions.cs
  49. 241 0
      tests/Lua.Tests/LexerTests.cs
  50. 29 0
      tests/Lua.Tests/ParserTests.cs

+ 4 - 4
src/Lua/CodeAnalysis/Compilation/Dump.cs

@@ -51,7 +51,7 @@ internal unsafe struct Header
         {
             if (!LuaSignature.SequenceEqual(new(signature, 4)))
             {
-                throw new LuaParseException($"{name.ToString()}: is not a precompiled chunk");
+                throw new LuaUnDumpException($"{name.ToString()}: is not a precompiled chunk");
             }
         }
 
@@ -59,7 +59,7 @@ internal unsafe struct Header
         var minor = Version & 0xF;
         if (major != Constants.VersionMajor || minor != Constants.VersionMinor)
         {
-            throw new LuaParseException($"{name.ToString()}: version mismatch in precompiled chunk {major}.{minor} != {Constants.VersionMajor}.{Constants.VersionMinor}");
+            throw new LuaUnDumpException($"{name.ToString()}: version mismatch in precompiled chunk {major}.{minor} != {Constants.VersionMajor}.{Constants.VersionMinor}");
         }
 
         if (IntSize != 4 || Format != 0 || IntegralNumber != 0 || PointerSize is not (4 or 8) || InstructionSize != 4 || NumberSize != 8)
@@ -77,7 +77,7 @@ internal unsafe struct Header
 
         return;
     ErrIncompatible:
-        throw new LuaParseException($"{name.ToString()}: incompatible precompiled chunk");
+        throw new LuaUnDumpException($"{name.ToString()}: incompatible precompiled chunk");
     }
 }
 
@@ -274,7 +274,7 @@ internal unsafe ref struct UnDumpState(ReadOnlySpan<byte> span, ReadOnlySpan<cha
     int pointerSize;
     readonly ReadOnlySpan<char> name = name;
 
-    void Throw(string why) => throw new LuaParseException($"{name.ToString()}: {why} precompiled chunk");
+    void Throw(string why) => throw new LuaUnDumpException($"{name.ToString()}: {why} precompiled chunk");
     void ThrowTooShort() => Throw("truncate");
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]

+ 1 - 1
src/Lua/CodeAnalysis/Compilation/Scanner.cs

@@ -127,7 +127,7 @@ internal struct Scanner
         var buff = ChunkID(Source);
         if (token != 0) message = $"{buff}:{LineNumber}: {message} near {TokenToString(token)}";
         else message = $"{buff}:{LineNumber}: {message}";
-        throw new(message);
+        throw new LuaScanException(message);
     }
 
 

+ 14 - 0
src/Lua/CodeAnalysis/SourcePosition.cs

@@ -0,0 +1,14 @@
+namespace Lua.CodeAnalysis;
+
+public record struct SourcePosition
+{
+    public SourcePosition(int line, int column)
+    {
+        Line = line;
+        Column = column;
+    }
+
+    public int Line { get; set; }
+    public int Column { get; set; }
+    public override readonly string ToString() => $"({Line},{Column})";
+}

+ 555 - 0
src/Lua/CodeAnalysis/Syntax/DisplayStringSyntaxVisitor.cs

@@ -0,0 +1,555 @@
+using System.Text;
+using Lua.CodeAnalysis.Syntax.Nodes;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public sealed class DisplayStringSyntaxVisitor : ISyntaxNodeVisitor<DisplayStringSyntaxVisitor.Context, bool>
+{
+    public sealed class Context
+    {
+        public readonly ref struct IndentScope
+        {
+            readonly Context source;
+
+            public IndentScope(Context source)
+            {
+                this.source = source;
+                source.IncreaseIndent();
+            }
+
+            public void Dispose()
+            {
+                source.DecreaseIndent();
+            }
+        }
+
+        readonly StringBuilder buffer = new();
+        int indentLevel;
+        bool isNewLine = true;
+
+        public IndentScope BeginIndentScope() => new(this);
+
+        public void Append(string value)
+        {
+            if (isNewLine)
+            {
+                buffer.Append(' ', indentLevel * 4);
+                isNewLine = false;
+            }
+            buffer.Append(value);
+        }
+
+        public void AppendLine(string value)
+        {
+            if (isNewLine)
+            {
+                buffer.Append(' ', indentLevel * 4);
+                isNewLine = false;
+            }
+
+            buffer.AppendLine(value);
+            isNewLine = true;
+        }
+
+        public void AppendLine()
+        {
+            buffer.AppendLine();
+            isNewLine = true;
+        }
+
+        public override string ToString() => buffer.ToString();
+
+        public void IncreaseIndent()
+        {
+            indentLevel++;
+        }
+
+        public void DecreaseIndent()
+        {
+            if (indentLevel > 0)
+                indentLevel--;
+        }
+
+        public void Reset()
+        {
+            buffer.Clear();
+            indentLevel = 0;
+            isNewLine = true;
+        }
+    }
+
+    readonly Context context = new();
+
+    public string GetDisplayString(SyntaxNode node)
+    {
+        context.Reset();
+        node.Accept(this, context);
+        return context.ToString();
+    }
+
+    public bool VisitBinaryExpressionNode(BinaryExpressionNode node, Context context)
+    {
+        node.LeftNode.Accept(this, context);
+        context.Append($" {node.OperatorType.ToDisplayString()} ");
+        node.RightNode.Accept(this, context);
+        return true;
+    }
+
+    public bool VisitBooleanLiteralNode(BooleanLiteralNode node, Context context)
+    {
+        context.Append(node.Value ? Keywords.True : Keywords.False);
+        return true;
+    }
+
+    public bool VisitBreakStatementNode(BreakStatementNode node, Context context)
+    {
+        context.Append(Keywords.Break);
+        return true;
+    }
+
+    public bool VisitCallFunctionExpressionNode(CallFunctionExpressionNode node, Context context)
+    {
+        node.FunctionNode.Accept(this, context);
+        context.Append("(");
+        VisitSyntaxNodes(node.ArgumentNodes, context);
+        context.Append(")");
+        return true;
+    }
+
+    public bool VisitCallFunctionStatementNode(CallFunctionStatementNode node, Context context)
+    {
+        node.Expression.Accept(this, context);
+        return true;
+    }
+
+    public bool VisitDoStatementNode(DoStatementNode node, Context context)
+    {
+        context.AppendLine("do");
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitFunctionDeclarationExpressionNode(FunctionDeclarationExpressionNode node, Context context)
+    {
+        context.Append("function(");
+        VisitSyntaxNodes(node.ParameterNodes, context);
+        if (node.HasVariableArguments)
+        {
+            if (node.ParameterNodes.Length > 0) context.Append(", ");
+            context.Append("...");
+        }
+        context.AppendLine(")");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitFunctionDeclarationStatementNode(FunctionDeclarationStatementNode node, Context context)
+    {
+        context.Append("function ");
+        context.Append(node.Name.ToString());
+        context.Append("(");
+        VisitSyntaxNodes(node.ParameterNodes, context);
+        if (node.HasVariableArguments)
+        {
+            if (node.ParameterNodes.Length > 0) context.Append(", ");
+            context.Append("...");
+        }
+        context.AppendLine(")");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitTableMethodDeclarationStatementNode(TableMethodDeclarationStatementNode node, Context context)
+    {
+        context.Append("function ");
+
+        for (int i = 0; i < node.MemberPath.Length; i++)
+        {
+            context.Append(node.MemberPath[i].Name.ToString());
+
+            if (i == node.MemberPath.Length - 2 && node.HasSelfParameter)
+            {
+                context.Append(":");
+            }
+            else if (i != node.MemberPath.Length - 1)
+            {
+                context.Append(".");
+            }
+        }
+
+        context.Append("(");
+        VisitSyntaxNodes(node.ParameterNodes, context);
+        if (node.HasVariableArguments)
+        {
+            if (node.ParameterNodes.Length > 0) context.Append(", ");
+            context.Append("...");
+        }
+        context.AppendLine(")");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitGenericForStatementNode(GenericForStatementNode node, Context context)
+    {
+        context.Append($"for ");
+        VisitSyntaxNodes(node.Names, context);
+        context.Append(" in ");
+        VisitSyntaxNodes(node.ExpressionNodes, context);
+        context.AppendLine(" do");
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitGotoStatementNode(GotoStatementNode node, Context context)
+    {
+        context.Append($"goto {node.Name}");
+        return true;
+    }
+
+    public bool VisitIdentifierNode(IdentifierNode node, Context context)
+    {
+        context.Append(node.Name.ToString());
+        return true;
+    }
+
+    public bool VisitIfStatementNode(IfStatementNode node, Context context)
+    {
+        context.Append("if ");
+        node.IfNode.ConditionNode.Accept(this, context);
+        context.AppendLine(" then");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.IfNode.ThenNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        foreach (var elseif in node.ElseIfNodes)
+        {
+            context.Append("elseif ");
+            elseif.ConditionNode.Accept(this, context);
+            context.AppendLine(" then");
+
+            using (context.BeginIndentScope())
+            {
+                foreach (var childNode in elseif.ThenNodes)
+                {
+                    childNode.Accept(this, context);
+                    context.AppendLine();
+                }
+            }
+        }
+
+        if (node.ElseNodes.Length > 0)
+        {
+            context.AppendLine("else");
+
+            using (context.BeginIndentScope())
+            {
+                foreach (var childNode in node.ElseNodes)
+                {
+                    childNode.Accept(this, context);
+                    context.AppendLine();
+                }
+            }
+        }
+
+        context.Append("end");
+
+        return true;
+    }
+
+    public bool VisitLabelStatementNode(LabelStatementNode node, Context context)
+    {
+        context.Append($"::{node.Name}::");
+        return true;
+    }
+
+    public bool VisitAssignmentStatementNode(AssignmentStatementNode node, Context context)
+    {
+        VisitSyntaxNodes(node.LeftNodes, context);
+
+        if (node.RightNodes.Length > 0)
+        {
+            context.Append(" = ");
+            VisitSyntaxNodes(node.RightNodes, context);
+        }
+
+        return true;
+    }
+
+    public bool VisitLocalAssignmentStatementNode(LocalAssignmentStatementNode node, Context context)
+    {
+        context.Append("local ");
+        return VisitAssignmentStatementNode(node, context);
+    }
+
+    public bool VisitLocalFunctionDeclarationStatementNode(LocalFunctionDeclarationStatementNode node, Context context)
+    {
+        context.Append("local ");
+        return VisitFunctionDeclarationStatementNode(node, context);
+    }
+
+    public bool VisitNilLiteralNode(NilLiteralNode node, Context context)
+    {
+        context.Append(Keywords.Nil);
+        return true;
+    }
+
+    public bool VisitNumericForStatementNode(NumericForStatementNode node, Context context)
+    {
+        context.Append($"for {node.VariableName} = ");
+        node.InitNode.Accept(this, context);
+        context.Append(", ");
+        node.LimitNode.Accept(this, context);
+        if (node.StepNode != null)
+        {
+            context.Append(", ");
+            node.StepNode.Accept(this, context);
+        }
+
+        context.AppendLine(" do");
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.StatementNodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitNumericLiteralNode(NumericLiteralNode node, Context context)
+    {
+        context.Append(node.Value.ToString());
+        return true;
+    }
+
+    public bool VisitRepeatStatementNode(RepeatStatementNode node, Context context)
+    {
+        context.AppendLine("repeat");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.Append("until ");
+        node.ConditionNode.Accept(this, context);
+        context.AppendLine();
+
+        return true;
+    }
+
+    public bool VisitReturnStatementNode(ReturnStatementNode node, Context context)
+    {
+        context.Append("return ");
+        VisitSyntaxNodes(node.Nodes, context);
+        return true;
+    }
+
+    public bool VisitStringLiteralNode(StringLiteralNode node, Context context)
+    {
+        if (node.IsShortLiteral)
+        {
+            context.Append("\"");
+            context.Append(node.Text.ToString());
+            context.Append("\"");
+        }
+        else
+        {
+            context.Append("[[");
+            context.Append(node.Text.ToString());
+            context.Append("]]");
+        }
+        return true;
+    }
+
+    public bool VisitSyntaxTree(LuaSyntaxTree node, Context context)
+    {
+        foreach (var statement in node.Nodes)
+        {
+            statement.Accept(this, context);
+            context.AppendLine();
+        }
+
+        return true;
+    }
+
+    public bool VisitTableConstructorExpressionNode(TableConstructorExpressionNode node, Context context)
+    {
+        context.AppendLine("{");
+        using (context.BeginIndentScope())
+        {
+            for (int i = 0; i < node.Fields.Length; i++)
+            {
+                var field = node.Fields[i];
+
+                switch (field)
+                {
+                    case GeneralTableConstructorField general:
+                        context.Append("[");
+                        general.KeyExpression.Accept(this, context);
+                        context.Append("] = ");
+                        general.ValueExpression.Accept(this, context);
+                        break;
+                    case RecordTableConstructorField record:
+                        context.Append($"{record.Key} = ");
+                        record.ValueExpression.Accept(this, context);
+                        break;
+                    case ListTableConstructorField list:
+                        list.Expression.Accept(this, context);
+                        break;
+                }
+
+                context.AppendLine(i == node.Fields.Length - 1 ? "" : ",");
+            }
+        }
+        context.AppendLine("}");
+
+        return true;
+    }
+
+    public bool VisitTableIndexerAccessExpressionNode(TableIndexerAccessExpressionNode node, Context context)
+    {
+        node.TableNode.Accept(this, context);
+        context.Append("[");
+        node.KeyNode.Accept(this, context);
+        context.Append("]");
+        return true;
+    }
+
+    public bool VisitTableMemberAccessExpressionNode(TableMemberAccessExpressionNode node, Context context)
+    {
+        node.TableNode.Accept(this, context);
+        context.Append($".{node.MemberName}");
+        return true;
+    }
+
+    public bool VisitCallTableMethodExpressionNode(CallTableMethodExpressionNode node, Context context)
+    {
+        node.TableNode.Accept(this, context);
+        context.Append($":{node.MethodName}(");
+        VisitSyntaxNodes(node.ArgumentNodes, context);
+        context.Append(")");
+        return true;
+    }
+
+    public bool VisitCallTableMethodStatementNode(CallTableMethodStatementNode node, Context context)
+    {
+        return node.Expression.Accept(this, context);
+    }
+
+    public bool VisitUnaryExpressionNode(UnaryExpressionNode node, Context context)
+    {
+        context.Append(node.Operator.ToDisplayString());
+        if (node.Operator is UnaryOperator.Not) context.Append(" ");
+        node.Node.Accept(this, context);
+
+        return true;
+    }
+
+    public bool VisitWhileStatementNode(WhileStatementNode node, Context context)
+    {
+        context.Append("while ");
+        node.ConditionNode.Accept(this, context);
+        context.AppendLine(" do");
+
+        using (context.BeginIndentScope())
+        {
+            foreach (var childNode in node.Nodes)
+            {
+                childNode.Accept(this, context);
+                context.AppendLine();
+            }
+        }
+
+        context.AppendLine("end");
+
+        return true;
+    }
+
+    public bool VisitVariableArgumentsExpressionNode(VariableArgumentsExpressionNode node, Context context)
+    {
+        context.Append("...");
+        return true;
+    }
+
+    void VisitSyntaxNodes(SyntaxNode[] nodes, Context context)
+    {
+        for (int i = 0; i < nodes.Length; i++)
+        {
+            nodes[i].Accept(this, context);
+            if (i != nodes.Length - 1) context.Append(", ");
+        }
+    }
+
+    public bool VisitGroupedExpressionNode(GroupedExpressionNode node, Context context)
+    {
+        context.Append("(");
+        node.Expression.Accept(this, context);
+        context.Append(")");
+        return true;
+    }
+}

+ 40 - 0
src/Lua/CodeAnalysis/Syntax/ISyntaxNodeVisitor.cs

@@ -0,0 +1,40 @@
+using Lua.CodeAnalysis.Syntax.Nodes;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public interface ISyntaxNodeVisitor<TContext, TResult>
+{
+    TResult VisitNumericLiteralNode(NumericLiteralNode node, TContext context);
+    TResult VisitBooleanLiteralNode(BooleanLiteralNode node, TContext context);
+    TResult VisitNilLiteralNode(NilLiteralNode node, TContext context);
+    TResult VisitStringLiteralNode(StringLiteralNode node, TContext context);
+    TResult VisitUnaryExpressionNode(UnaryExpressionNode node, TContext context);
+    TResult VisitBinaryExpressionNode(BinaryExpressionNode node, TContext context);
+    TResult VisitGroupedExpressionNode(GroupedExpressionNode node, TContext context);
+    TResult VisitIdentifierNode(IdentifierNode node, TContext context);
+    TResult VisitDoStatementNode(DoStatementNode node, TContext context);
+    TResult VisitFunctionDeclarationExpressionNode(FunctionDeclarationExpressionNode node, TContext context);
+    TResult VisitFunctionDeclarationStatementNode(FunctionDeclarationStatementNode node, TContext context);
+    TResult VisitLocalFunctionDeclarationStatementNode(LocalFunctionDeclarationStatementNode node, TContext context);
+    TResult VisitWhileStatementNode(WhileStatementNode node, TContext context);
+    TResult VisitRepeatStatementNode(RepeatStatementNode node, TContext context);
+    TResult VisitIfStatementNode(IfStatementNode node, TContext context);
+    TResult VisitLabelStatementNode(LabelStatementNode node, TContext context);
+    TResult VisitGotoStatementNode(GotoStatementNode node, TContext context);
+    TResult VisitBreakStatementNode(BreakStatementNode node, TContext context);
+    TResult VisitReturnStatementNode(ReturnStatementNode node, TContext context);
+    TResult VisitAssignmentStatementNode(AssignmentStatementNode node, TContext context);
+    TResult VisitLocalAssignmentStatementNode(LocalAssignmentStatementNode node, TContext context);
+    TResult VisitCallFunctionExpressionNode(CallFunctionExpressionNode node, TContext context);
+    TResult VisitCallFunctionStatementNode(CallFunctionStatementNode node, TContext context);
+    TResult VisitNumericForStatementNode(NumericForStatementNode node, TContext context);
+    TResult VisitGenericForStatementNode(GenericForStatementNode node, TContext context);
+    TResult VisitTableConstructorExpressionNode(TableConstructorExpressionNode node, TContext context);
+    TResult VisitTableMethodDeclarationStatementNode(TableMethodDeclarationStatementNode node, TContext context);
+    TResult VisitTableIndexerAccessExpressionNode(TableIndexerAccessExpressionNode node, TContext context);
+    TResult VisitTableMemberAccessExpressionNode(TableMemberAccessExpressionNode node, TContext context);
+    TResult VisitCallTableMethodExpressionNode(CallTableMethodExpressionNode node, TContext context);
+    TResult VisitCallTableMethodStatementNode(CallTableMethodStatementNode node, TContext context);
+    TResult VisitVariableArgumentsExpressionNode(VariableArgumentsExpressionNode node, TContext context);
+    TResult VisitSyntaxTree(LuaSyntaxTree node, TContext context);
+}

+ 61 - 0
src/Lua/CodeAnalysis/Syntax/Keywords.cs

@@ -0,0 +1,61 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+internal static class Keywords
+{
+    public const string LF = "\n";
+
+    public const string LParen = "(";
+    public const string RParen = ")";
+    public const string LCurly = "{";
+    public const string RCurly = "}";
+    public const string LSquare = "[";
+    public const string RSquare = "]";
+
+    public const string Assignment = "=";
+
+    public const string Nil = "nil";
+    public const string True = "true";
+    public const string False = "false";
+
+    public const string Addition = "+";
+    public const string Subtraction = "-";
+    public const string Multiplication = "*";
+    public const string Division = "/";
+    public const string Modulo = "%";
+    public const string Exponentiation = "^";
+
+    public const string Length = "#";
+    public const string Concat = "..";
+
+    public const string Equality = "==";
+    public const string Inequality = "~=";
+    public const string GreaterThan = ">";
+    public const string GreaterThanOrEqual = ">=";
+    public const string LessThan = "<";
+    public const string LessThanOrEqual = "<=";
+
+    public const string And = "and";
+    public const string Or = "or";
+    public const string Not = "not";
+
+    public const string Do = "do";
+    public const string End = "end";
+    public const string Then = "then";
+
+    public const string If = "if";
+    public const string ElseIf = "elseif";
+    public const string Else = "else";
+
+    public const string Return = "return";
+    public const string Break = "break";
+    public const string Goto = "goto";
+
+    public const string For = "for";
+    public const string In = "in";
+    public const string While = "while";
+    public const string Repeat = "repeat";
+    public const string Until = "until";
+
+    public const string Function = "function";
+    public const string Local = "local";
+}

+ 543 - 0
src/Lua/CodeAnalysis/Syntax/Lexer.cs

@@ -0,0 +1,543 @@
+using System.Runtime.CompilerServices;
+using Lua.Internal;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public ref struct Lexer
+{
+    public required ReadOnlyMemory<char> Source { get; init; }
+    public string? ChunkName { get; init; }
+
+    SyntaxToken current;
+    SourcePosition position = new(1, 0);
+    int offset;
+
+    public Lexer()
+    {
+    }
+
+    public readonly SyntaxToken Current => current;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void Advance(int count)
+    {
+        var span = Source.Span;
+        for (int i = 0; i < count; i++)
+        {
+            if (offset >= span.Length)
+            {
+                LuaParseException.SyntaxError(ChunkName, position, null);
+            }
+
+            var c = span[offset];
+            offset++;
+
+            var isLF = c is '\n';
+            var isCR = c is '\r' && (span.Length == offset || span[offset] is not '\n');
+
+            if (isLF || isCR)
+            {
+                position.Column = 0;
+                position.Line++;
+            }
+            else
+            {
+                position.Column++;
+            }
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    bool TryRead(int offset, out char value)
+    {
+        if (Source.Length <= offset)
+        {
+            value = default;
+            return false;
+        }
+
+        value = Source.Span[offset];
+        return true;
+    }
+
+    public bool MoveNext()
+    {
+        if (Source.Length <= offset) return false;
+
+        var span = Source.Span;
+        var startOffset = offset;
+        var position = this.position;
+
+        var c1 = span[offset];
+        Advance(1);
+        var c2 = span.Length == offset ? char.MinValue : span[offset];
+
+        switch (c1)
+        {
+            case ' ':
+            case '\t':
+                return MoveNext();
+            case '\n':
+                current = SyntaxToken.EndOfLine(position);
+                return true;
+            case '\r':
+                if (c2 == '\n') Advance(1);
+                current = SyntaxToken.EndOfLine(position);
+                return true;
+            case '(':
+                current = SyntaxToken.LParen(position);
+                return true;
+            case ')':
+                current = SyntaxToken.RParen(position);
+                return true;
+            case '{':
+                current = SyntaxToken.LCurly(position);
+                return true;
+            case '}':
+                current = SyntaxToken.RCurly(position);
+                return true;
+            case ']':
+                current = SyntaxToken.RSquare(position);
+                return true;
+            case '+':
+                current = SyntaxToken.Addition(position);
+                return true;
+            case '-':
+                // comment
+                if (c2 == '-')
+                {
+                    var pos = position;
+                    Advance(1);
+
+                    // block comment
+                    if (span.Length > offset + 1 && span[offset] is '[' && span[offset + 1] is '[' or '=')
+                    {
+                        Advance(1);
+                        (_, _, var isTerminated) = ReadUntilLongBracketEnd(ref span);
+                        if (!isTerminated) LuaParseException.UnfinishedLongComment(ChunkName, pos);
+                    }
+                    else // line comment
+                    {
+                        ReadUntilEOL(ref span, ref offset, out _);
+                    }
+
+                    return MoveNext();
+                }
+                else
+                {
+                    current = SyntaxToken.Subtraction(position);
+                    return true;
+                }
+            case '*':
+                current = SyntaxToken.Multiplication(position);
+                return true;
+            case '/':
+                current = SyntaxToken.Division(position);
+                return true;
+            case '%':
+                current = SyntaxToken.Modulo(position);
+                return true;
+            case '^':
+                current = SyntaxToken.Exponentiation(position);
+                return true;
+            case '=':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.Equality(position);
+                    Advance(1);
+                }
+                else
+                {
+                    current = SyntaxToken.Assignment(position);
+                }
+                return true;
+            case '~':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.Inequality(position);
+                    Advance(1);
+                }
+                else
+                {
+                    throw new LuaParseException(ChunkName, position, $"error: Invalid '~' token");
+                }
+                return true;
+            case '>':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.GreaterThanOrEqual(position);
+                    Advance(1);
+                }
+                else
+                {
+                    current = SyntaxToken.GreaterThan(position);
+                }
+                return true;
+            case '<':
+                if (c2 == '=')
+                {
+                    current = SyntaxToken.LessThanOrEqual(position);
+                    Advance(1);
+                }
+                else
+                {
+                    current = SyntaxToken.LessThan(position);
+                }
+                return true;
+            case '.':
+                if (c2 == '.')
+                {
+                    var c3 = span.Length == (offset + 1) ? char.MinValue : span[offset + 1];
+
+                    if (c3 == '.')
+                    {
+                        // vararg
+                        current = SyntaxToken.VarArg(position);
+                        Advance(2);
+                    }
+                    else
+                    {
+                        // concat
+                        current = SyntaxToken.Concat(position);
+                        Advance(1);
+                    }
+
+                    return true;
+                }
+
+                if (!StringHelper.IsNumber(c2))
+                {
+                    current = SyntaxToken.Dot(position);
+                    return true;
+                }
+
+                break;
+            case '#':
+                current = SyntaxToken.Length(position);
+                return true;
+            case ',':
+                current = SyntaxToken.Comma(position);
+                return true;
+            case ';':
+                current = SyntaxToken.SemiColon(position);
+                return true;
+        }
+
+        // numeric literal
+        if (c1 is '.' || StringHelper.IsNumber(c1))
+        {
+            if (c1 is '0' && c2 is 'x' or 'X') // hex 0x
+            {
+                Advance(1);
+                if (span[offset] is '.') Advance(1);
+
+                ReadDigit(ref span, ref offset, out var readCount);
+
+                if (span.Length > offset && span[offset] is '.')
+                {
+                    Advance(1);
+                    ReadDigit(ref span, ref offset, out _);
+                }
+
+                if (span.Length > offset && span[offset] is 'p' or 'P')
+                {
+                    Advance(1);
+                    if (span[offset] is '-' or '+') Advance(1);
+
+                    ReadDigit(ref span, ref offset, out _);
+                }
+
+                if (readCount == 0)
+                {
+                    throw new LuaParseException(ChunkName, this.position, $"error: Illegal hexadecimal number");
+                }
+            }
+            else
+            {
+                ReadNumber(ref span, ref offset, out _);
+
+                if (span.Length > offset && span[offset] is '.')
+                {
+                    Advance(1);
+                    ReadNumber(ref span, ref offset, out _);
+                }
+
+                if (span.Length > offset && span[offset] is 'e' or 'E')
+                {
+                    Advance(1);
+                    if (span[offset] is '-' or '+') Advance(1);
+
+                    ReadNumber(ref span, ref offset, out _);
+                }
+            }
+
+            current = new(SyntaxTokenType.Number, Source[startOffset..offset], position);
+            return true;
+        }
+
+        // label
+        if (c1 is ':')
+        {
+            if (c2 is ':')
+            {
+                var stringStartOffset = offset + 1;
+                Advance(2);
+
+                var prevC = char.MinValue;
+
+                while (span.Length > offset)
+                {
+                    var c = span[offset];
+                    if (prevC == ':' && c == ':') break;
+
+                    Advance(1);
+                    prevC = c;
+                }
+
+                current = SyntaxToken.Label(Source[stringStartOffset..(offset - 1)], position);
+                Advance(1);
+            }
+            else
+            {
+                current = SyntaxToken.Colon(position);
+            }
+
+            return true;
+        }
+
+        // short string literal
+        if (c1 is '"' or '\'')
+        {
+            var quote = c1;
+            var stringStartOffset = offset;
+            var isTerminated = false;
+
+            while (span.Length > offset)
+            {
+                var c = span[offset];
+
+                if (c is '\n' or '\r')
+                {
+                    break;
+                }
+
+                if (c is '\\')
+                {
+                    Advance(1);
+
+                    if (span.Length <= offset) break;
+                    if (span[offset] == '\r')
+                    {
+                        if (span.Length<=offset +1) continue;
+                        if (span[offset+1] == '\n')Advance(1);
+                    }
+                }
+                else if (c == quote)
+                {
+                    isTerminated = true;
+                    break;
+                }
+
+                Advance(1);
+            }
+
+            if (!isTerminated)
+            {
+                throw new LuaParseException(ChunkName, this.position, "error: Unterminated string");
+            }
+
+            current = SyntaxToken.String(Source[stringStartOffset..offset], position);
+            Advance(1);
+            return true;
+        }
+
+        // long string literal
+        if (c1 is '[')
+        {
+            if (c2 is '[' or '=')
+            {
+                (var start, var end, var isTerminated) = ReadUntilLongBracketEnd(ref span);
+
+                if (!isTerminated)
+                {
+                    throw new LuaParseException(ChunkName, this.position, "error: Unterminated string");
+                }
+
+                current = SyntaxToken.RawString(Source[start..end], position);
+                return true;
+            }
+            else
+            {
+                current = SyntaxToken.LSquare(position);
+                return true;
+            }
+        }
+
+        // identifier
+        if (IsIdentifier(c1))
+        {
+            while (span.Length > offset && IsIdentifier(span[offset]))
+            {
+                Advance(1);
+            }
+
+            var identifier = Source[startOffset..offset];
+
+            current = identifier.Span switch
+            {
+                Keywords.Nil => SyntaxToken.Nil(position),
+                Keywords.True => SyntaxToken.True(position),
+                Keywords.False => SyntaxToken.False(position),
+                Keywords.And => SyntaxToken.And(position),
+                Keywords.Or => SyntaxToken.Or(position),
+                Keywords.Not => SyntaxToken.Not(position),
+                Keywords.End => SyntaxToken.End(position),
+                Keywords.Then => SyntaxToken.Then(position),
+                Keywords.If => SyntaxToken.If(position),
+                Keywords.ElseIf => SyntaxToken.ElseIf(position),
+                Keywords.Else => SyntaxToken.Else(position),
+                Keywords.Local => SyntaxToken.Local(position),
+                Keywords.Return => SyntaxToken.Return(position),
+                Keywords.Goto => SyntaxToken.Goto(position),
+                Keywords.Do => SyntaxToken.Do(position),
+                Keywords.In => SyntaxToken.In(position),
+                Keywords.While => SyntaxToken.While(position),
+                Keywords.Repeat => SyntaxToken.Repeat(position),
+                Keywords.For => SyntaxToken.For(position),
+                Keywords.Until => SyntaxToken.Until(position),
+                Keywords.Break => SyntaxToken.Break(position),
+                Keywords.Function => SyntaxToken.Function(position),
+                _ => new(SyntaxTokenType.Identifier, identifier, position),
+            };
+
+            return true;
+        }
+
+        throw new LuaParseException(ChunkName, position, $"unexpected symbol near '{c1}'");
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadUntilEOL(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
+    {
+        readCount = 0;
+        var flag = true;
+        while (flag)
+        {
+            if (span.Length <= offset) return;
+
+            var c1 = span[offset];
+
+            if (c1 is '\n')
+            {
+                flag = false;
+            }
+            else if (c1 is '\r')
+            {
+                var c2 = span.Length == offset + 1 ? char.MinValue : span[offset + 1];
+                if (c2 is '\n')
+                {
+                    Advance(1);
+                    readCount++;
+                }
+                flag = false;
+            }
+
+            Advance(1);
+            readCount++;
+        }
+    }
+
+    (int Start, int End, bool IsTerminated) ReadUntilLongBracketEnd(ref ReadOnlySpan<char> span)
+    {
+        var c = span[offset];
+        var level = 0;
+        while (c is '=')
+        {
+            level++;
+            Advance(1);
+            c = span[offset];
+        }
+
+        Advance(1);
+
+        var startOffset = offset;
+        var endOffset = 0;
+        var isTerminated = false;
+        var prevC = char.MinValue;
+
+        while (span.Length > offset + level + 1)
+        {
+            var current = span[offset];
+
+            // skip first newline
+            if (offset == startOffset)
+            {
+                if (current == '\r')
+                {
+                    startOffset += 2;
+                    Advance(span[offset + 1] == '\n' ? 2 : 1);
+                    continue;
+                }
+                else if (current == '\n')
+                {
+                    startOffset++;
+                    Advance(1);
+                    continue;
+                }
+            }
+
+            if (current is ']' && prevC is not '\\')
+            {
+                endOffset = offset;
+
+                for (int i = 1; i <= level; i++)
+                {
+                    if (span[offset + i] is not '=') goto CONTINUE;
+                }
+
+                if (span[offset + level + 1] is not ']') goto CONTINUE;
+
+                Advance(level + 2);
+                isTerminated = true;
+                break;
+            }
+
+        CONTINUE:
+            prevC = current;
+            Advance(1);
+        }
+
+        return (startOffset, endOffset, isTerminated);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadDigit(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
+    {
+        readCount = 0;
+        while (span.Length > offset && StringHelper.IsDigit(span[offset]))
+        {
+            Advance(1);
+            readCount++;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void ReadNumber(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
+    {
+        readCount = 0;
+        while (span.Length > offset && StringHelper.IsNumber(span[offset]))
+        {
+            Advance(1);
+            readCount++;
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static bool IsIdentifier(char c)
+    {
+        return c == '_' ||
+               ('A' <= c && c <= 'Z') ||
+               ('a' <= c && c <= 'z') ||
+               StringHelper.IsNumber(c);
+    }
+}

+ 30 - 0
src/Lua/CodeAnalysis/Syntax/LuaSyntaxTree.cs

@@ -0,0 +1,30 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+public record LuaSyntaxTree(SyntaxNode[] Nodes,SourcePosition Position) : SyntaxNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitSyntaxTree(this, context);
+    }
+
+    public static LuaSyntaxTree Parse(string source, string? chunkName = null)
+    {
+        var lexer = new Lexer
+        {
+            Source = source.AsMemory(),
+            ChunkName = chunkName,
+        };
+
+        var parser = new Parser
+        {
+            ChunkName = chunkName
+        };
+
+        while (lexer.MoveNext())
+        {
+            parser.Add(lexer.Current);
+        }
+
+        return parser.Parse();
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/AssignmentStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record AssignmentStatementNode(SyntaxNode[] LeftNodes, ExpressionNode[] RightNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitAssignmentStatementNode(this, context);
+    }
+}

+ 54 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/BinaryExpressionNode.cs

@@ -0,0 +1,54 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public enum BinaryOperator
+{
+    Addition,
+    Subtraction,
+    Multiplication,
+    Division,
+    Modulo,
+    Exponentiation,
+    Equality,
+    Inequality,
+    GreaterThan,
+    GreaterThanOrEqual,
+    LessThan,
+    LessThanOrEqual,
+    And,
+    Or,
+    Concat,
+}
+
+internal static class BinaryOperatorEx
+{
+    public static string ToDisplayString(this BinaryOperator @operator)
+    {
+        return @operator switch
+        {
+            BinaryOperator.Addition => Keywords.Addition,
+            BinaryOperator.Subtraction => Keywords.Subtraction,
+            BinaryOperator.Multiplication => Keywords.Multiplication,
+            BinaryOperator.Division => Keywords.Division,
+            BinaryOperator.Modulo => Keywords.Modulo,
+            BinaryOperator.Exponentiation => Keywords.Exponentiation,
+            BinaryOperator.Equality => Keywords.Equality,
+            BinaryOperator.Inequality => Keywords.Inequality,
+            BinaryOperator.GreaterThan => Keywords.GreaterThan,
+            BinaryOperator.GreaterThanOrEqual => Keywords.GreaterThanOrEqual,
+            BinaryOperator.LessThan => Keywords.LessThan,
+            BinaryOperator.LessThanOrEqual => Keywords.LessThanOrEqual,
+            BinaryOperator.And => Keywords.And,
+            BinaryOperator.Or => Keywords.Or,
+            BinaryOperator.Concat => Keywords.Concat,
+            _ => "",
+        };
+    }
+}
+
+public record BinaryExpressionNode(BinaryOperator OperatorType, ExpressionNode LeftNode, ExpressionNode RightNode, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitBinaryExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/BooleanLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record BooleanLiteralNode(bool Value, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitBooleanLiteralNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/BreakStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record BreakStatementNode(SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitBreakStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallFunctionExpressionNode(ExpressionNode FunctionNode, ExpressionNode[] ArgumentNodes) : ExpressionNode(FunctionNode.Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallFunctionExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallFunctionStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallFunctionStatementNode(CallFunctionExpressionNode Expression) : StatementNode(Expression.Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallFunctionStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallTableMethodExpressionNode(ExpressionNode TableNode, string MethodName, ExpressionNode[] ArgumentNodes, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallTableMethodExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/CallTableMethodStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record CallTableMethodStatementNode(CallTableMethodExpressionNode Expression) : StatementNode(Expression.Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitCallTableMethodStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/DoStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record DoStatementNode(StatementNode[] StatementNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitDoStatementNode(this, context);
+    }
+}

+ 3 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/ExpressionNode.cs

@@ -0,0 +1,3 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public abstract record ExpressionNode(SourcePosition Position) : SyntaxNode(Position);

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record FunctionDeclarationExpressionNode(IdentifierNode[] ParameterNodes, SyntaxNode[] Nodes, bool HasVariableArguments, SourcePosition Position, int LineDefined,SourcePosition EndPosition) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitFunctionDeclarationExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/FunctionDeclarationStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record FunctionDeclarationStatementNode(ReadOnlyMemory<char> Name, IdentifierNode[] ParameterNodes, SyntaxNode[] Nodes, bool HasVariableArguments, SourcePosition Position,int LineDefined,SourcePosition EndPosition) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitFunctionDeclarationStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/GenericForStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record GenericForStatementNode(IdentifierNode[] Names, ExpressionNode[] ExpressionNodes, StatementNode[] StatementNodes, SourcePosition Position, SourcePosition DoPosition, SourcePosition EndPosition) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitGenericForStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/GotoStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record GotoStatementNode(ReadOnlyMemory<char> Name, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitGotoStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/GroupedExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record GroupedExpressionNode(ExpressionNode Expression, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitGroupedExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/IdentifierNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record IdentifierNode(ReadOnlyMemory<char> Name, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitIdentifierNode(this, context);
+    }
+}

+ 16 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/IfStatementNode.cs

@@ -0,0 +1,16 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record IfStatementNode(IfStatementNode.ConditionAndThenNodes IfNode, IfStatementNode.ConditionAndThenNodes[] ElseIfNodes, StatementNode[] ElseNodes, SourcePosition Position) : StatementNode(Position)
+{
+    public record ConditionAndThenNodes
+    {
+        public SourcePosition Position;
+        public required ExpressionNode ConditionNode;
+        public required StatementNode[] ThenNodes;
+    }
+
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitIfStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/LabelStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record LabelStatementNode(ReadOnlyMemory<char> Name, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitLabelStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/LocalAssignmentStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record LocalAssignmentStatementNode(IdentifierNode[] Identifiers, ExpressionNode[] RightNodes, SourcePosition Position) : AssignmentStatementNode(Identifiers, RightNodes, Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitLocalAssignmentStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/LocalFunctionDeclarationNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record LocalFunctionDeclarationStatementNode(ReadOnlyMemory<char> Name, IdentifierNode[] ParameterNodes, SyntaxNode[] Nodes, bool HasVariableArguments, SourcePosition Position, int LineDefined,SourcePosition EndPosition) : FunctionDeclarationStatementNode(Name, ParameterNodes, Nodes, HasVariableArguments, Position, LineDefined, EndPosition)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitLocalFunctionDeclarationStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/NilLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record NilLiteralNode(SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitNilLiteralNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/NumericForStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record NumericForStatementNode(ReadOnlyMemory<char> VariableName, ExpressionNode InitNode, ExpressionNode LimitNode, ExpressionNode? StepNode, StatementNode[] StatementNodes, SourcePosition Position,SourcePosition DoPosition) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitNumericForStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/NumericLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record NumericLiteralNode(double Value, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitNumericLiteralNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/RepeatStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record RepeatStatementNode(ExpressionNode ConditionNode, SyntaxNode[] Nodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitRepeatStatementNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/ReturnStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record ReturnStatementNode(ExpressionNode[] Nodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitReturnStatementNode(this, context);
+    }
+}

+ 3 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/StatementNode.cs

@@ -0,0 +1,3 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public abstract record StatementNode(SourcePosition Position) : SyntaxNode(Position);

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/StringLiteralNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record StringLiteralNode(ReadOnlyMemory<char> Text, bool IsShortLiteral, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitStringLiteralNode(this, context);
+    }
+}

+ 14 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableConstructorExpressionNode.cs

@@ -0,0 +1,14 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableConstructorExpressionNode(TableConstructorField[] Fields, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableConstructorExpressionNode(this, context);
+    }
+}
+
+public abstract record TableConstructorField(SourcePosition Position);
+public record GeneralTableConstructorField(ExpressionNode KeyExpression, ExpressionNode ValueExpression, SourcePosition Position) : TableConstructorField(Position);
+public record RecordTableConstructorField(string Key, ExpressionNode ValueExpression, SourcePosition Position) : TableConstructorField(Position);
+public record ListTableConstructorField(ExpressionNode Expression, SourcePosition Position) : TableConstructorField(Position);

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableIndexerAccessExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableIndexerAccessExpressionNode(ExpressionNode TableNode, ExpressionNode KeyNode, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableIndexerAccessExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableMemberAccessExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableMemberAccessExpressionNode(ExpressionNode TableNode, string MemberName, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableMemberAccessExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/TableMethodDeclarationStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record TableMethodDeclarationStatementNode(IdentifierNode[] MemberPath, IdentifierNode[] ParameterNodes, StatementNode[] Nodes, bool HasVariableArguments, bool HasSelfParameter, SourcePosition Position, int LineDefined,SourcePosition EndPosition) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitTableMethodDeclarationStatementNode(this, context);
+    }
+}

+ 30 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/UnaryExpressionNode.cs

@@ -0,0 +1,30 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public enum UnaryOperator
+{
+    Negate,
+    Not,
+    Length,
+}
+
+public record UnaryExpressionNode(UnaryOperator Operator, ExpressionNode Node, SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitUnaryExpressionNode(this, context);
+    }
+}
+
+internal static class UnaryOperatorEx
+{
+    public static string ToDisplayString(this UnaryOperator @operator)
+    {
+        return @operator switch
+        {
+            UnaryOperator.Negate => Keywords.Subtraction,
+            UnaryOperator.Not => Keywords.Not,
+            UnaryOperator.Length => Keywords.Length,
+            _ => "",
+        };
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/VariableArgumentsExpressionNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record VariableArgumentsExpressionNode(SourcePosition Position) : ExpressionNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitVariableArgumentsExpressionNode(this, context);
+    }
+}

+ 9 - 0
src/Lua/CodeAnalysis/Syntax/Nodes/WhileStatementNode.cs

@@ -0,0 +1,9 @@
+namespace Lua.CodeAnalysis.Syntax.Nodes;
+
+public record WhileStatementNode(ExpressionNode ConditionNode, SyntaxNode[] Nodes, SourcePosition Position) : StatementNode(Position)
+{
+    public override TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context)
+    {
+        return visitor.VisitWhileStatementNode(this, context);
+    }
+}

+ 49 - 0
src/Lua/CodeAnalysis/Syntax/OperatorPrecedence.cs

@@ -0,0 +1,49 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+public enum OperatorPrecedence
+{
+    /// <summary>
+    /// Non-operator token precedence
+    /// </summary>
+    NonOperator,
+
+    /// <summary>
+    /// 'or' operator
+    /// </summary>
+    Or,
+
+    /// <summary>
+    /// 'and' operator
+    /// </summary>
+    And,
+
+    /// <summary>
+    /// Relational operators (&lt;, &lt;=, &gt;, &gt;=, ==, ~=)
+    /// </summary>
+    Relational,
+
+    /// <summary>
+    /// Concat operator (..)
+    /// </summary>
+    Concat,
+
+    /// <summary>
+    /// Addition and Subtraction (+, -)
+    /// </summary>
+    Addition,
+
+    /// <summary>
+    /// Multipilcation, Division and Modulo (*, /, %)
+    /// </summary>
+    Multiplication,
+
+    /// <summary>
+    /// Negate, Not, Length (-, 'not', #)
+    /// </summary>
+    Unary,
+
+    /// <summary>
+    /// Exponentiation (^)
+    /// </summary>
+    Exponentiation,
+}

+ 1037 - 0
src/Lua/CodeAnalysis/Syntax/Parser.cs

@@ -0,0 +1,1037 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Lua.Internal;
+using Lua.CodeAnalysis.Syntax.Nodes;
+using System.Globalization;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public ref struct Parser
+{
+    public string? ChunkName { get; init; }
+
+    PooledList<SyntaxToken> tokens;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Add(SyntaxToken token) => tokens.Add(token);
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Dispose()
+    {
+        tokens.Dispose();
+    }
+
+    public LuaSyntaxTree Parse()
+    {
+        using var root = new PooledList<SyntaxNode>(64);
+
+        var enumerator = new SyntaxTokenEnumerator(tokens.AsSpan());
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            root.Add(node);
+        }
+        var tokensSpan = tokens.AsSpan();
+        var lastToken = tokensSpan[0];
+        for (int i = tokensSpan.Length-1; 0<i;i--)
+        {
+            var t = tokensSpan[i];
+            if (t.Type is not SyntaxTokenType.EndOfLine)
+            {
+                lastToken = t;
+                break;
+            }
+        }
+
+        var tree = new LuaSyntaxTree(root.AsSpan().ToArray(),lastToken.Position);
+        Dispose();
+
+        return tree;
+    }
+
+    StatementNode ParseStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        switch (enumerator.Current.Type)
+        {
+            case SyntaxTokenType.LParen:
+            case SyntaxTokenType.Identifier:
+                {
+                    var firstExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+
+                    switch (firstExpression)
+                    {
+                        case CallFunctionExpressionNode callFunctionExpression:
+                            return new CallFunctionStatementNode(callFunctionExpression);
+                        case CallTableMethodExpressionNode callTableMethodExpression:
+                            return new CallTableMethodStatementNode(callTableMethodExpression);
+                        default:
+                            if (enumerator.GetNext(true).Type is SyntaxTokenType.Comma or SyntaxTokenType.Assignment)
+                            {
+                                // skip ','
+                                MoveNextWithValidation(ref enumerator);
+                                enumerator.SkipEoL();
+
+                                return ParseAssignmentStatement(firstExpression, ref enumerator);
+                            }
+
+                            break;
+                    }
+                }
+                break;
+            case SyntaxTokenType.Return:
+                return ParseReturnStatement(ref enumerator);
+            case SyntaxTokenType.Do:
+                return ParseDoStatement(ref enumerator);
+            case SyntaxTokenType.Goto:
+                return ParseGotoStatement(ref enumerator);
+            case SyntaxTokenType.Label:
+                return new LabelStatementNode(enumerator.Current.Text, enumerator.Current.Position);
+            case SyntaxTokenType.If:
+                return ParseIfStatement(ref enumerator);
+            case SyntaxTokenType.While:
+                return ParseWhileStatement(ref enumerator);
+            case SyntaxTokenType.Repeat:
+                return ParseRepeatStatement(ref enumerator);
+            case SyntaxTokenType.For:
+                {
+                    // skip 'for' keyword
+                    var forToken = enumerator.Current;
+                    MoveNextWithValidation(ref enumerator);
+                    enumerator.SkipEoL();
+
+                    if (enumerator.GetNext(true).Type is SyntaxTokenType.Assignment)
+                    {
+                        return ParseNumericForStatement(ref enumerator, forToken);
+                    }
+                    else
+                    {
+                        return ParseGenericForStatement(ref enumerator, forToken);
+                    }
+                }
+            case SyntaxTokenType.Break:
+                return new BreakStatementNode(enumerator.Current.Position);
+            case SyntaxTokenType.Local:
+                {
+                    // skip 'local' keyword
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Local, out var localToken);
+
+                    // local function
+                    if (enumerator.Current.Type is SyntaxTokenType.Function)
+                    {
+                        // skip 'function' keyword
+                        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Function, out var functionToken);
+                        enumerator.SkipEoL();
+
+                        return ParseLocalFunctionDeclarationStatement(ref enumerator, functionToken);
+                    }
+
+                    CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+
+                    var nextType = enumerator.GetNext().Type;
+
+                    if (nextType is SyntaxTokenType.Comma or SyntaxTokenType.Assignment)
+                    {
+                        return ParseLocalAssignmentStatement(ref enumerator, localToken);
+                    }
+                    else if (nextType is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon)
+                    {
+                        return new LocalAssignmentStatementNode([new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position)], [], localToken.Position);
+                    }
+                }
+                break;
+            case SyntaxTokenType.Function:
+                {
+                    // skip 'function' keyword
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Function, out var functionToken);
+                    enumerator.SkipEoL();
+
+                    if (enumerator.GetNext(true).Type is SyntaxTokenType.Dot or SyntaxTokenType.Colon)
+                    {
+                        return ParseTableMethodDeclarationStatement(ref enumerator, functionToken);
+                    }
+                    else
+                    {
+                        return ParseFunctionDeclarationStatement(ref enumerator, functionToken);
+                    }
+                }
+        }
+
+        LuaParseException.UnexpectedToken(ChunkName, enumerator.Current.Position, enumerator.Current);
+        return default!;
+    }
+
+    ReturnStatementNode ParseReturnStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'return' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Return, out var returnToken);
+
+        // parse parameters
+        var expressions = ParseExpressionList(ref enumerator);
+
+        return new ReturnStatementNode(expressions, returnToken.Position);
+    }
+
+    DoStatementNode ParseDoStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // check 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+        var doToken = enumerator.Current;
+
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out _);
+
+        return new DoStatementNode(statements, doToken.Position);
+    }
+
+    GotoStatementNode ParseGotoStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'goto' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Goto, out var gotoToken);
+
+        CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+        return new GotoStatementNode(enumerator.Current.Text, gotoToken.Position);
+    }
+
+    AssignmentStatementNode ParseAssignmentStatement(ExpressionNode firstExpression, ref SyntaxTokenEnumerator enumerator)
+    {
+        // parse leftNodes
+        using var leftNodes = new PooledList<SyntaxNode>(8);
+        leftNodes.Add(firstExpression);
+
+        while (enumerator.Current.Type == SyntaxTokenType.Comma)
+        {
+            // skip ','
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            leftNodes.Add(ParseExpression(ref enumerator, OperatorPrecedence.NonOperator));
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // skip '='
+        if (enumerator.Current.Type is not SyntaxTokenType.Assignment)
+        {
+            enumerator.MovePrevious();
+            return new AssignmentStatementNode(leftNodes.AsSpan().ToArray(), [], firstExpression.Position);
+        }
+
+        MoveNextWithValidation(ref enumerator);
+
+        // parse expressions
+        var expressions = ParseExpressionList(ref enumerator);
+
+        return new AssignmentStatementNode(leftNodes.AsSpan().ToArray(), expressions, firstExpression.Position);
+    }
+
+    LocalAssignmentStatementNode ParseLocalAssignmentStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken localToken)
+    {
+        // parse identifiers
+        var identifiers = ParseIdentifierList(ref enumerator);
+
+        // skip '='
+        if (enumerator.Current.Type is not SyntaxTokenType.Assignment)
+        {
+            enumerator.MovePrevious();
+            return new LocalAssignmentStatementNode(identifiers, [], localToken.Position);
+        }
+
+        MoveNextWithValidation(ref enumerator);
+
+        // parse expressions
+        var expressions = ParseExpressionList(ref enumerator);
+
+        return new LocalAssignmentStatementNode(identifiers, expressions, localToken.Position);
+    }
+
+    IfStatementNode ParseIfStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'if' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.If, out var ifToken);
+        enumerator.SkipEoL();
+
+        // parse condition
+        var condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'then' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Then);
+        var thenToken = enumerator.Current;
+
+        using var builder = new PooledList<StatementNode>(64);
+        using var elseIfBuilder = new PooledList<IfStatementNode.ConditionAndThenNodes>(64);
+
+        IfStatementNode.ConditionAndThenNodes ifNodes = default!;
+        StatementNode[] elseNodes = [];
+
+        // if = 0, elseif = 1, else = 2
+        var state = 0;
+
+        // parse statements
+        while (true)
+        {
+            if (!enumerator.MoveNext())
+            {
+                LuaParseException.ExpectedToken(ChunkName, enumerator.Current.Position, SyntaxTokenType.End);
+            }
+
+            var tokenType = enumerator.Current.Type;
+
+            if (tokenType is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon)
+            {
+                continue;
+            }
+
+            if (tokenType is SyntaxTokenType.ElseIf or SyntaxTokenType.Else or SyntaxTokenType.End)
+            {
+                switch (state)
+                {
+                    case 0:
+                        ifNodes = new()
+                        {
+                            Position = thenToken.Position,
+                            ConditionNode = condition,
+                            ThenNodes = builder.AsSpan().ToArray(),
+                        };
+                        builder.Clear();
+                        break;
+                    case 1:
+                        elseIfBuilder.Add(new()
+                        {
+                            Position = thenToken.Position,
+                            ConditionNode = condition,
+                            ThenNodes = builder.AsSpan().ToArray(),
+                        });
+                        builder.Clear();
+                        break;
+                    case 2:
+                        elseNodes = builder.AsSpan().ToArray();
+                        break;
+                }
+
+                if (tokenType is SyntaxTokenType.ElseIf)
+                {
+                    // skip 'elseif' keywords
+                    MoveNextWithValidation(ref enumerator);
+                    enumerator.SkipEoL();
+
+                    // parse condition
+                    condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+                    MoveNextWithValidation(ref enumerator);
+                    enumerator.SkipEoL();
+
+                    // check 'then' keyword
+                    CheckCurrent(ref enumerator, SyntaxTokenType.Then);
+                    thenToken = enumerator.Current;
+                    // set elseif state
+                    state = 1;
+
+                    continue;
+                }
+                else if (tokenType is SyntaxTokenType.Else)
+                {
+                    // set else state
+                    state = 2;
+
+                    continue;
+                }
+                else if (tokenType is SyntaxTokenType.End)
+                {
+                    goto RETURN;
+                }
+            }
+
+            var node = ParseStatement(ref enumerator);
+            builder.Add(node);
+        }
+
+    RETURN:
+        return new IfStatementNode(ifNodes, elseIfBuilder.AsSpan().ToArray(), elseNodes, ifToken.Position);
+    }
+
+    WhileStatementNode ParseWhileStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'while' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.While, out var whileToken);
+        enumerator.SkipEoL();
+
+        // parse condition
+        var condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out _);
+
+        return new WhileStatementNode(condition, statements, whileToken.Position);
+    }
+
+    RepeatStatementNode ParseRepeatStatement(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'repeat' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Repeat);
+        var repeatToken = enumerator.Current;
+
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.Until, out _);
+
+        // skip 'until keyword'
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Until, out _);
+        enumerator.SkipEoL();
+
+        // parse condition
+        var condition = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+
+        return new RepeatStatementNode(condition, statements, repeatToken.Position);
+    }
+
+    NumericForStatementNode ParseNumericForStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken forToken)
+    {
+        // parse variable name
+        CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+        var varName = enumerator.Current.Text;
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip '='
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Assignment, out _);
+        enumerator.SkipEoL();
+
+        // parse initial value
+        var initialValueNode = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip ','
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Comma, out _);
+        enumerator.SkipEoL();
+
+        // parse limit
+        var limitNode = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // parse stepNode
+        ExpressionNode? stepNode = null;
+        if (enumerator.Current.Type is SyntaxTokenType.Comma)
+        {
+            // skip ','
+            enumerator.MoveNext();
+
+            // parse step
+            stepNode = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // skip 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+        var doToken = enumerator.Current;
+
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out _);
+
+        return new NumericForStatementNode(varName, initialValueNode, limitNode, stepNode, statements, forToken.Position, doToken.Position);
+    }
+
+    GenericForStatementNode ParseGenericForStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken forToken)
+    {
+        var identifiers = ParseIdentifierList(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'in' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.In, out _);
+        enumerator.SkipEoL();
+        var iteratorToken = enumerator.Current;
+        var expressions = ParseExpressionList(ref enumerator);
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        // skip 'do' keyword
+        CheckCurrent(ref enumerator, SyntaxTokenType.Do);
+        var doToken = enumerator.Current;
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out var endToken);
+
+        return new GenericForStatementNode(identifiers, expressions, statements, iteratorToken.Position, doToken.Position, endToken.Position);
+    }
+
+    FunctionDeclarationStatementNode ParseFunctionDeclarationStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken functionToken)
+    {
+        var (Name, Identifiers, Statements, HasVariableArgments, LineDefined, EndPosition) = ParseFunctionDeclarationCore(ref enumerator, false);
+        return new FunctionDeclarationStatementNode(Name, Identifiers, Statements, HasVariableArgments, functionToken.Position, LineDefined, EndPosition);
+    }
+
+    LocalFunctionDeclarationStatementNode ParseLocalFunctionDeclarationStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken functionToken)
+    {
+        var (Name, Identifiers, Statements, HasVariableArgments, LineDefined, EndPosition) = ParseFunctionDeclarationCore(ref enumerator, false);
+        return new LocalFunctionDeclarationStatementNode(Name, Identifiers, Statements, HasVariableArgments, functionToken.Position, LineDefined, EndPosition);
+    }
+
+    (ReadOnlyMemory<char> Name, IdentifierNode[] Identifiers, StatementNode[] Statements, bool HasVariableArgments, int LineDefined, SourcePosition EndPosition) ParseFunctionDeclarationCore(ref SyntaxTokenEnumerator enumerator, bool isAnonymous)
+    {
+        ReadOnlyMemory<char> name;
+
+        if (isAnonymous)
+        {
+            name = ReadOnlyMemory<char>.Empty;
+        }
+        else
+        {
+            // parse function name
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            name = enumerator.Current.Text;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        // skip '('
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out var leftParenToken);
+        enumerator.SkipEoL();
+
+        // parse parameters
+        var identifiers = enumerator.Current.Type is SyntaxTokenType.Identifier
+            ? ParseIdentifierList(ref enumerator)
+            : [];
+
+        // check variable arguments
+        var hasVarArg = enumerator.Current.Type is SyntaxTokenType.VarArg;
+        if (hasVarArg) enumerator.MoveNext();
+
+        // skip ')'
+        CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out var endToken);
+
+        return (name, identifiers, statements, hasVarArg, leftParenToken.Position.Line, endToken.Position);
+    }
+
+    TableMethodDeclarationStatementNode ParseTableMethodDeclarationStatement(ref SyntaxTokenEnumerator enumerator, SyntaxToken functionToken)
+    {
+        using var names = new PooledList<IdentifierNode>(32);
+        var hasSelfParameter = false;
+        SyntaxToken leftParenToken;
+        while (true)
+        {
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            names.Add(new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position));
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            if (enumerator.Current.Type is SyntaxTokenType.Dot or SyntaxTokenType.Colon)
+            {
+                if (hasSelfParameter)
+                {
+                    LuaParseException.UnexpectedToken(ChunkName, enumerator.Current.Position, enumerator.Current);
+                }
+
+                hasSelfParameter = enumerator.Current.Type is SyntaxTokenType.Colon;
+
+                MoveNextWithValidation(ref enumerator);
+                enumerator.SkipEoL();
+            }
+            else if (enumerator.Current.Type is SyntaxTokenType.LParen)
+            {
+                leftParenToken = enumerator.Current;
+                // skip '('
+                MoveNextWithValidation(ref enumerator);
+                enumerator.SkipEoL();
+                break;
+            }
+        }
+
+        // parse parameters
+        var identifiers = enumerator.Current.Type is SyntaxTokenType.Identifier
+            ? ParseIdentifierList(ref enumerator)
+            : [];
+
+        // check variable arguments
+        var hasVarArg = enumerator.Current.Type is SyntaxTokenType.VarArg;
+        if (hasVarArg) enumerator.MoveNext();
+
+        // skip ')'
+        CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+
+        // parse statements
+        var statements = ParseStatementList(ref enumerator, SyntaxTokenType.End, out var endToken);
+
+        return new TableMethodDeclarationStatementNode(names.AsSpan().ToArray(), identifiers, statements, hasVarArg, hasSelfParameter, functionToken.Position, leftParenToken.Position.Line, endToken.Position);
+    }
+
+    bool TryParseExpression(ref SyntaxTokenEnumerator enumerator, OperatorPrecedence precedence, [NotNullWhen(true)] out ExpressionNode? result)
+    {
+        result = enumerator.Current.Type switch
+        {
+            SyntaxTokenType.Identifier => enumerator.GetNext(true).Type switch
+            {
+                SyntaxTokenType.LParen or SyntaxTokenType.String or SyntaxTokenType.RawString => ParseCallFunctionExpression(ref enumerator, null),
+                SyntaxTokenType.LSquare or SyntaxTokenType.Dot or SyntaxTokenType.Colon => ParseTableAccessExpression(ref enumerator, null),
+                _ => new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position),
+            },
+            SyntaxTokenType.Number => new NumericLiteralNode(ConvertTextToNumber(enumerator.Current.Text.Span), enumerator.Current.Position),
+            SyntaxTokenType.String => new StringLiteralNode(enumerator.Current.Text, true, enumerator.Current.Position),
+            SyntaxTokenType.RawString => new StringLiteralNode(enumerator.Current.Text, false, enumerator.Current.Position),
+            SyntaxTokenType.True => new BooleanLiteralNode(true, enumerator.Current.Position),
+            SyntaxTokenType.False => new BooleanLiteralNode(false, enumerator.Current.Position),
+            SyntaxTokenType.Nil => new NilLiteralNode(enumerator.Current.Position),
+            SyntaxTokenType.VarArg => new VariableArgumentsExpressionNode(enumerator.Current.Position),
+            SyntaxTokenType.Subtraction => ParseMinusNumber(ref enumerator),
+            SyntaxTokenType.Not or SyntaxTokenType.Length => ParseUnaryExpression(ref enumerator, enumerator.Current),
+            SyntaxTokenType.LParen => ParseGroupedExpression(ref enumerator),
+            SyntaxTokenType.LCurly => ParseTableConstructorExpression(ref enumerator),
+            SyntaxTokenType.Function => ParseFunctionDeclarationExpression(ref enumerator),
+            _ => null,
+        };
+
+        if (result == null) return false;
+
+        // nested table access & function call
+    RECURSIVE:
+        enumerator.SkipEoL();
+
+        var nextType = enumerator.GetNext().Type;
+        if (nextType is SyntaxTokenType.LSquare or SyntaxTokenType.Dot or SyntaxTokenType.Colon)
+        {
+            MoveNextWithValidation(ref enumerator);
+            result = ParseTableAccessExpression(ref enumerator, result);
+            goto RECURSIVE;
+        }
+        else if (nextType is SyntaxTokenType.LParen or SyntaxTokenType.String or SyntaxTokenType.RawString or SyntaxTokenType.LCurly)
+        {
+            MoveNextWithValidation(ref enumerator);
+            result = ParseCallFunctionExpression(ref enumerator, result);
+            goto RECURSIVE;
+        }
+
+        // binary expression
+        while (true)
+        {
+            var opPrecedence = GetPrecedence(enumerator.GetNext(true).Type);
+            if (precedence >= opPrecedence) break;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+            result = ParseBinaryExpression(ref enumerator, opPrecedence, result);
+
+            enumerator.SkipEoL();
+        }
+
+        return true;
+    }
+
+    ExpressionNode ParseExpression(ref SyntaxTokenEnumerator enumerator, OperatorPrecedence precedence)
+    {
+        if (!TryParseExpression(ref enumerator, precedence, out var result))
+        {
+            throw new LuaParseException(ChunkName, enumerator.Current.Position, $"Unexpected token <{enumerator.Current.Type}>");
+        }
+
+        return result;
+    }
+
+    ExpressionNode ParseMinusNumber(ref SyntaxTokenEnumerator enumerator)
+    {
+        var token = enumerator.Current;
+        if (enumerator.GetNext(true).Type is SyntaxTokenType.Number)
+        {
+            enumerator.MoveNext();
+            enumerator.SkipEoL();
+
+            return new NumericLiteralNode(-ConvertTextToNumber(enumerator.Current.Text.Span), token.Position);
+        }
+        else
+        {
+            return ParseUnaryExpression(ref enumerator, token);
+        }
+    }
+
+    UnaryExpressionNode ParseUnaryExpression(ref SyntaxTokenEnumerator enumerator, SyntaxToken operatorToken)
+    {
+        var operatorType = enumerator.Current.Type switch
+        {
+            SyntaxTokenType.Subtraction => UnaryOperator.Negate,
+            SyntaxTokenType.Not => UnaryOperator.Not,
+            SyntaxTokenType.Length => UnaryOperator.Length,
+            _ => throw new LuaParseException(ChunkName, operatorToken.Position, $"unexpected symbol near '{enumerator.Current.Text}'"),
+        };
+
+        MoveNextWithValidation(ref enumerator);
+        var right = ParseExpression(ref enumerator, OperatorPrecedence.Unary);
+
+        return new UnaryExpressionNode(operatorType, right, operatorToken.Position);
+    }
+
+    BinaryExpressionNode ParseBinaryExpression(ref SyntaxTokenEnumerator enumerator, OperatorPrecedence precedence, ExpressionNode left)
+    {
+        var operatorToken = enumerator.Current;
+        var operatorType = operatorToken.Type switch
+        {
+            SyntaxTokenType.Addition => BinaryOperator.Addition,
+            SyntaxTokenType.Subtraction => BinaryOperator.Subtraction,
+            SyntaxTokenType.Multiplication => BinaryOperator.Multiplication,
+            SyntaxTokenType.Division => BinaryOperator.Division,
+            SyntaxTokenType.Modulo => BinaryOperator.Modulo,
+            SyntaxTokenType.Exponentiation => BinaryOperator.Exponentiation,
+            SyntaxTokenType.Equality => BinaryOperator.Equality,
+            SyntaxTokenType.Inequality => BinaryOperator.Inequality,
+            SyntaxTokenType.LessThan => BinaryOperator.LessThan,
+            SyntaxTokenType.LessThanOrEqual => BinaryOperator.LessThanOrEqual,
+            SyntaxTokenType.GreaterThan => BinaryOperator.GreaterThan,
+            SyntaxTokenType.GreaterThanOrEqual => BinaryOperator.GreaterThanOrEqual,
+            SyntaxTokenType.And => BinaryOperator.And,
+            SyntaxTokenType.Or => BinaryOperator.Or,
+            SyntaxTokenType.Concat => BinaryOperator.Concat,
+            _ => throw new LuaParseException(ChunkName, enumerator.Current.Position, $"unexpected symbol near '{enumerator.Current.Text}'"),
+        };
+
+        enumerator.SkipEoL();
+        MoveNextWithValidation(ref enumerator);
+        enumerator.SkipEoL();
+
+        var right = ParseExpression(ref enumerator, precedence);
+
+        return new BinaryExpressionNode(operatorType, left, right, operatorToken.Position);
+    }
+
+    TableConstructorExpressionNode ParseTableConstructorExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        CheckCurrent(ref enumerator, SyntaxTokenType.LCurly);
+        var startToken = enumerator.Current;
+
+        using var items = new PooledList<TableConstructorField>(16);
+
+        while (enumerator.MoveNext())
+        {
+            var currentToken = enumerator.Current;
+            switch (currentToken.Type)
+            {
+                case SyntaxTokenType.RCurly:
+                    goto RETURN;
+                case SyntaxTokenType.EndOfLine:
+                case SyntaxTokenType.SemiColon:
+                case SyntaxTokenType.Comma:
+                    continue;
+                case SyntaxTokenType.LSquare:
+                    // general style ([key] = value)
+                    enumerator.MoveNext();
+
+                    var keyExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+                    enumerator.MoveNext();
+
+                    // skip '] ='
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.RSquare, out _);
+                    CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Assignment, out _);
+
+                    var valueExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+
+                    items.Add(new GeneralTableConstructorField(keyExpression, valueExpression, currentToken.Position));
+
+                    break;
+                case SyntaxTokenType.Identifier when enumerator.GetNext(true).Type is SyntaxTokenType.Assignment:
+                    // record style (key = value)
+                    var name = enumerator.Current.Text;
+
+                    // skip key and '='
+                    enumerator.MoveNext();
+                    enumerator.MoveNext();
+
+                    var expression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+
+                    items.Add(new RecordTableConstructorField(name.ToString(), expression, currentToken.Position));
+                    break;
+                default:
+                    // list style
+                    items.Add(new ListTableConstructorField(ParseExpression(ref enumerator, OperatorPrecedence.NonOperator), currentToken.Position));
+                    break;
+            }
+        }
+
+    RETURN:
+        return new TableConstructorExpressionNode(items.AsSpan().ToArray(), startToken.Position);
+    }
+
+    ExpressionNode ParseTableAccessExpression(ref SyntaxTokenEnumerator enumerator, ExpressionNode? parentTable)
+    {
+        IdentifierNode? identifier = null;
+        if (parentTable == null)
+        {
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            identifier = new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position);
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        ExpressionNode result;
+        var current = enumerator.Current;
+        if (current.Type is SyntaxTokenType.LSquare)
+        {
+            // indexer access -- table[key]
+
+            // skip '['
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse key expression
+            var keyExpression = ParseExpression(ref enumerator, OperatorPrecedence.NonOperator);
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // check ']'
+            CheckCurrent(ref enumerator, SyntaxTokenType.RSquare);
+
+            result = new TableIndexerAccessExpressionNode(identifier ?? parentTable!, keyExpression, current.Position);
+        }
+        else if (current.Type is SyntaxTokenType.Dot)
+        {
+            // member access -- table.key
+
+            // skip '.'
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            var key = enumerator.Current.Text.ToString();
+
+            result = new TableMemberAccessExpressionNode(identifier ?? parentTable!, key, current.Position);
+        }
+        else if (current.Type is SyntaxTokenType.Colon)
+        {
+            // self method call -- table:method(arg0, arg1, ...)
+
+            // skip ':'
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse identifier
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            var methodName = enumerator.Current.Text;
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // parse arguments
+            var arguments = ParseCallFunctionArguments(ref enumerator);
+            result = new CallTableMethodExpressionNode(identifier ?? parentTable!, methodName.ToString(), arguments, current.Position);
+        }
+        else
+        {
+            LuaParseException.SyntaxError(ChunkName, current.Position, current);
+            return null!; // dummy
+        }
+
+        return result;
+    }
+
+    GroupedExpressionNode ParseGroupedExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip '('
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out var lParen);
+        enumerator.SkipEoL();
+
+        var expression = ParseExpression(ref enumerator, GetPrecedence(enumerator.Current.Type));
+        MoveNextWithValidation(ref enumerator);
+
+        // check ')'
+        CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+
+        return new GroupedExpressionNode(expression, lParen.Position);
+    }
+
+    ExpressionNode ParseCallFunctionExpression(ref SyntaxTokenEnumerator enumerator, ExpressionNode? function)
+    {
+        // parse name
+        if (function == null)
+        {
+            CheckCurrent(ref enumerator, SyntaxTokenType.Identifier);
+            function = new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position);
+            enumerator.MoveNext();
+            enumerator.SkipEoL();
+        }
+
+        // parse parameters
+        var parameters = ParseCallFunctionArguments(ref enumerator);
+
+        return new CallFunctionExpressionNode(function, parameters);
+    }
+
+    FunctionDeclarationExpressionNode ParseFunctionDeclarationExpression(ref SyntaxTokenEnumerator enumerator)
+    {
+        // skip 'function' keyword
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.Function, out var functionToken);
+        enumerator.SkipEoL();
+
+        var (_, Identifiers, Statements, HasVariableArgments, LineDefined, LastLineDefined) = ParseFunctionDeclarationCore(ref enumerator, true);
+        return new FunctionDeclarationExpressionNode(Identifiers, Statements, HasVariableArgments, functionToken.Position, LineDefined, LastLineDefined);
+    }
+
+    ExpressionNode[] ParseCallFunctionArguments(ref SyntaxTokenEnumerator enumerator)
+    {
+        if (enumerator.Current.Type is SyntaxTokenType.String)
+        {
+            return [new StringLiteralNode(enumerator.Current.Text, true, enumerator.Current.Position)];
+        }
+        else if (enumerator.Current.Type is SyntaxTokenType.RawString)
+        {
+            return [new StringLiteralNode(enumerator.Current.Text, false, enumerator.Current.Position)];
+        }
+        else if (enumerator.Current.Type is SyntaxTokenType.LCurly)
+        {
+            return [ParseTableConstructorExpression(ref enumerator)];
+        }
+
+        // check and skip '('
+        CheckCurrentAndSkip(ref enumerator, SyntaxTokenType.LParen, out _);
+
+        ExpressionNode[] arguments;
+        if (enumerator.Current.Type is SyntaxTokenType.RParen)
+        {
+            // parameterless
+            arguments = [];
+        }
+        else
+        {
+            // parse arguments
+            arguments = ParseExpressionList(ref enumerator);
+            enumerator.SkipEoL();
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            // check ')'
+            CheckCurrent(ref enumerator, SyntaxTokenType.RParen);
+        }
+
+        return arguments;
+    }
+
+    ExpressionNode[] ParseExpressionList(ref SyntaxTokenEnumerator enumerator)
+    {
+        using var builder = new PooledList<ExpressionNode>(8);
+
+        while (true)
+        {
+            enumerator.SkipEoL();
+
+            if (!TryParseExpression(ref enumerator, OperatorPrecedence.NonOperator, out var expression))
+            {
+                enumerator.MovePrevious();
+                break;
+            }
+
+            builder.Add(expression);
+
+            enumerator.SkipEoL();
+            if (enumerator.GetNext().Type != SyntaxTokenType.Comma) break;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            if (!enumerator.MoveNext()) break;
+        }
+
+        return builder.AsSpan().ToArray();
+    }
+
+    IdentifierNode[] ParseIdentifierList(ref SyntaxTokenEnumerator enumerator)
+    {
+        using var buffer = new PooledList<IdentifierNode>(8);
+
+        while (true)
+        {
+            if (enumerator.Current.Type != SyntaxTokenType.Identifier) break;
+            var identifier = new IdentifierNode(enumerator.Current.Text, enumerator.Current.Position);
+            buffer.Add(identifier);
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+
+            if (enumerator.Current.Type != SyntaxTokenType.Comma) break;
+
+            MoveNextWithValidation(ref enumerator);
+            enumerator.SkipEoL();
+        }
+
+        return buffer.AsSpan().ToArray();
+    }
+
+    StatementNode[] ParseStatementList(ref SyntaxTokenEnumerator enumerator, SyntaxTokenType endTokenType, out SyntaxToken endToken)
+    {
+        using var statements = new PooledList<StatementNode>(64);
+
+        // parse statements
+        while (enumerator.MoveNext())
+        {
+            if (enumerator.Current.Type == endTokenType) break;
+            if (enumerator.Current.Type is SyntaxTokenType.EndOfLine or SyntaxTokenType.SemiColon) continue;
+
+            var node = ParseStatement(ref enumerator);
+            statements.Add(node);
+        }
+
+        endToken = enumerator.Current;
+
+        return statements.AsSpan().ToArray();
+    }
+
+    void CheckCurrentAndSkip(ref SyntaxTokenEnumerator enumerator, SyntaxTokenType expectedToken, out SyntaxToken token)
+    {
+        CheckCurrent(ref enumerator, expectedToken);
+        token = enumerator.Current;
+        MoveNextWithValidation(ref enumerator);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void CheckCurrent(ref SyntaxTokenEnumerator enumerator, SyntaxTokenType expectedToken)
+    {
+        if (enumerator.Current.Type != expectedToken)
+        {
+            LuaParseException.ExpectedToken(ChunkName, enumerator.Current.Position, expectedToken);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    void MoveNextWithValidation(ref SyntaxTokenEnumerator enumerator)
+    {
+        if (!enumerator.MoveNext()) LuaParseException.SyntaxError(ChunkName, enumerator.Current.Position, enumerator.Current);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static OperatorPrecedence GetPrecedence(SyntaxTokenType type)
+    {
+        return type switch
+        {
+            SyntaxTokenType.Addition or SyntaxTokenType.Subtraction => OperatorPrecedence.Addition,
+            SyntaxTokenType.Multiplication or SyntaxTokenType.Division or SyntaxTokenType.Modulo => OperatorPrecedence.Multiplication,
+            SyntaxTokenType.Equality or SyntaxTokenType.Inequality or SyntaxTokenType.LessThan or SyntaxTokenType.LessThanOrEqual or SyntaxTokenType.GreaterThan or SyntaxTokenType.GreaterThanOrEqual => OperatorPrecedence.Relational,
+            SyntaxTokenType.Concat => OperatorPrecedence.Concat,
+            SyntaxTokenType.Exponentiation => OperatorPrecedence.Exponentiation,
+            SyntaxTokenType.And => OperatorPrecedence.And,
+            SyntaxTokenType.Or => OperatorPrecedence.Or,
+            _ => OperatorPrecedence.NonOperator,
+        };
+    }
+
+    static double ConvertTextToNumber(ReadOnlySpan<char> text)
+    {
+        if (text.Length > 2 && text[0] is '0' && text[1] is 'x' or 'X')
+        {
+            return HexConverter.ToDouble(text);
+        }
+        else
+        {
+            return double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);
+        }
+    }
+}

+ 6 - 0
src/Lua/CodeAnalysis/Syntax/SyntaxNode.cs

@@ -0,0 +1,6 @@
+namespace Lua.CodeAnalysis.Syntax;
+
+public abstract record SyntaxNode(SourcePosition Position)
+{
+    public abstract TResult Accept<TContext, TResult>(ISyntaxNodeVisitor<TContext, TResult> visitor, TContext context);
+}

+ 364 - 0
src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs

@@ -0,0 +1,364 @@
+using Lua.Internal;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public readonly struct SyntaxToken(SyntaxTokenType type, ReadOnlyMemory<char> text, SourcePosition position) : IEquatable<SyntaxToken>
+{
+    public static SyntaxToken EndOfLine(SourcePosition position) => new(SyntaxTokenType.EndOfLine, Keywords.LF.AsMemory(), position);
+
+    public static SyntaxToken LParen(SourcePosition position) => new(SyntaxTokenType.LParen, Keywords.LParen.AsMemory(), position);
+    public static SyntaxToken RParen(SourcePosition position) => new(SyntaxTokenType.RParen, Keywords.RParen.AsMemory(), position);
+    public static SyntaxToken LCurly(SourcePosition position) => new(SyntaxTokenType.LCurly, Keywords.LCurly.AsMemory(), position);
+    public static SyntaxToken RCurly(SourcePosition position) => new(SyntaxTokenType.RCurly, Keywords.RCurly.AsMemory(), position);
+    public static SyntaxToken LSquare(SourcePosition position) => new(SyntaxTokenType.LSquare, Keywords.LSquare.AsMemory(), position);
+    public static SyntaxToken RSquare(SourcePosition position) => new(SyntaxTokenType.RSquare, Keywords.RSquare.AsMemory(), position);
+
+    public static SyntaxToken Nil(SourcePosition position) => new(SyntaxTokenType.Nil, Keywords.Nil.AsMemory(), position);
+    public static SyntaxToken True(SourcePosition position) => new(SyntaxTokenType.True, Keywords.True.AsMemory(), position);
+    public static SyntaxToken False(SourcePosition position) => new(SyntaxTokenType.False, Keywords.False.AsMemory(), position);
+
+    public static SyntaxToken Addition(SourcePosition position) => new(SyntaxTokenType.Addition, Keywords.Addition.AsMemory(), position);
+    public static SyntaxToken Subtraction(SourcePosition position) => new(SyntaxTokenType.Subtraction, Keywords.Subtraction.AsMemory(), position);
+    public static SyntaxToken Multiplication(SourcePosition position) => new(SyntaxTokenType.Multiplication, Keywords.Multiplication.AsMemory(), position);
+    public static SyntaxToken Division(SourcePosition position) => new(SyntaxTokenType.Division, Keywords.Division.AsMemory(), position);
+    public static SyntaxToken Modulo(SourcePosition position) => new(SyntaxTokenType.Modulo, Keywords.Modulo.AsMemory(), position);
+    public static SyntaxToken Exponentiation(SourcePosition position) => new(SyntaxTokenType.Exponentiation, Keywords.Exponentiation.AsMemory(), position);
+
+    public static SyntaxToken Equality(SourcePosition position) => new(SyntaxTokenType.Equality, Keywords.Equality.AsMemory(), position);
+    public static SyntaxToken Inequality(SourcePosition position) => new(SyntaxTokenType.Inequality, Keywords.Inequality.AsMemory(), position);
+    public static SyntaxToken GreaterThan(SourcePosition position) => new(SyntaxTokenType.GreaterThan, Keywords.GreaterThan.AsMemory(), position);
+    public static SyntaxToken GreaterThanOrEqual(SourcePosition position) => new(SyntaxTokenType.GreaterThanOrEqual, Keywords.GreaterThanOrEqual.AsMemory(), position);
+    public static SyntaxToken LessThan(SourcePosition position) => new(SyntaxTokenType.LessThan, Keywords.LessThan.AsMemory(), position);
+    public static SyntaxToken LessThanOrEqual(SourcePosition position) => new(SyntaxTokenType.LessThanOrEqual, Keywords.LessThanOrEqual.AsMemory(), position);
+
+    public static SyntaxToken Length(SourcePosition position) => new(SyntaxTokenType.Length, Keywords.Length.AsMemory(), position);
+    public static SyntaxToken Concat(SourcePosition position) => new(SyntaxTokenType.Concat, Keywords.Concat.AsMemory(), position);
+    public static SyntaxToken VarArg(SourcePosition position) => new(SyntaxTokenType.VarArg, "...".AsMemory(), position);
+
+    public static SyntaxToken Assignment(SourcePosition position) => new(SyntaxTokenType.Assignment, Keywords.Assignment.AsMemory(), position);
+
+    public static SyntaxToken And(SourcePosition position) => new(SyntaxTokenType.And, Keywords.And.AsMemory(), position);
+    public static SyntaxToken Or(SourcePosition position) => new(SyntaxTokenType.Or, Keywords.Or.AsMemory(), position);
+    public static SyntaxToken Not(SourcePosition position) => new(SyntaxTokenType.Not, Keywords.Not.AsMemory(), position);
+
+    public static SyntaxToken End(SourcePosition position) => new(SyntaxTokenType.End, Keywords.End.AsMemory(), position);
+    public static SyntaxToken Then(SourcePosition position) => new(SyntaxTokenType.Then, Keywords.Then.AsMemory(), position);
+
+    public static SyntaxToken If(SourcePosition position) => new(SyntaxTokenType.If, Keywords.If.AsMemory(), position);
+    public static SyntaxToken ElseIf(SourcePosition position) => new(SyntaxTokenType.ElseIf, Keywords.ElseIf.AsMemory(), position);
+    public static SyntaxToken Else(SourcePosition position) => new(SyntaxTokenType.Else, Keywords.Else.AsMemory(), position);
+
+    public static SyntaxToken Local(SourcePosition position) => new(SyntaxTokenType.Local, Keywords.Local.AsMemory(), position);
+
+    public static SyntaxToken Return(SourcePosition position) => new(SyntaxTokenType.Return, Keywords.Return.AsMemory(), position);
+    public static SyntaxToken Goto(SourcePosition position) => new(SyntaxTokenType.Goto, Keywords.Goto.AsMemory(), position);
+
+    public static SyntaxToken Comma(SourcePosition position) => new(SyntaxTokenType.Comma, ",".AsMemory(), position);
+    public static SyntaxToken Dot(SourcePosition position) => new(SyntaxTokenType.Dot, ".".AsMemory(), position);
+    public static SyntaxToken SemiColon(SourcePosition position) => new(SyntaxTokenType.SemiColon, ";".AsMemory(), position);
+    public static SyntaxToken Colon(SourcePosition position) => new(SyntaxTokenType.Colon, ":".AsMemory(), position);
+
+    public static SyntaxToken Do(SourcePosition position) => new(SyntaxTokenType.Do, Keywords.Do.AsMemory(), position);
+    public static SyntaxToken While(SourcePosition position) => new(SyntaxTokenType.While, Keywords.While.AsMemory(), position);
+    public static SyntaxToken Repeat(SourcePosition position) => new(SyntaxTokenType.Repeat, Keywords.Repeat.AsMemory(), position);
+    public static SyntaxToken Until(SourcePosition position) => new(SyntaxTokenType.Until, Keywords.Until.AsMemory(), position);
+    public static SyntaxToken Break(SourcePosition position) => new(SyntaxTokenType.Break, Keywords.Break.AsMemory(), position);
+    public static SyntaxToken Function(SourcePosition position) => new(SyntaxTokenType.Function, Keywords.Function.AsMemory(), position);
+    public static SyntaxToken For(SourcePosition position) => new(SyntaxTokenType.For, Keywords.For.AsMemory(), position);
+    public static SyntaxToken In(SourcePosition position) => new(SyntaxTokenType.In, Keywords.In.AsMemory(), position);
+
+    public SyntaxTokenType Type { get; } = type;
+    public ReadOnlyMemory<char> Text { get; } = text;
+    public SourcePosition Position { get; } = position;
+
+    public static SyntaxToken Number(string text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Number, text.AsMemory(), position);
+    }
+
+    public static SyntaxToken Number(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Number, text, position);
+    }
+
+    public static SyntaxToken Identifier(string text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Identifier, text.AsMemory(), position);
+    }
+
+    public static SyntaxToken Identifier(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Identifier, text, position);
+    }
+
+    public static SyntaxToken String(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.String, text, position);
+    }
+
+    public static SyntaxToken RawString(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.RawString, text, position);
+    }
+
+    public static SyntaxToken Label(ReadOnlyMemory<char> text, SourcePosition position)
+    {
+        return new(SyntaxTokenType.Label, text, position);
+    }
+
+    public override string ToString()
+    {
+        return $"{Position} {Type}:{Text}";
+    }
+
+    public string ToDisplayString()
+    {
+        return Type switch
+        {
+            SyntaxTokenType.EndOfLine => Keywords.LF,
+            SyntaxTokenType.LParen => Keywords.LParen,
+            SyntaxTokenType.RParen => Keywords.RParen,
+            SyntaxTokenType.LCurly => Keywords.LCurly,
+            SyntaxTokenType.RCurly => Keywords.RCurly,
+            SyntaxTokenType.LSquare => Keywords.LSquare,
+            SyntaxTokenType.RSquare => Keywords.RSquare,
+            SyntaxTokenType.SemiColon => ";",
+            SyntaxTokenType.Comma => ",",
+            SyntaxTokenType.Number => Text.ToString(),
+            SyntaxTokenType.String => $"\"{Text}\"",
+            SyntaxTokenType.RawString => $"[[{Text}]]",
+            SyntaxTokenType.Nil => Keywords.Nil,
+            SyntaxTokenType.True => Keywords.True,
+            SyntaxTokenType.False => Keywords.False,
+            SyntaxTokenType.Identifier => Text.ToString(),
+            SyntaxTokenType.Addition => Keywords.Addition,
+            SyntaxTokenType.Subtraction => Keywords.Subtraction,
+            SyntaxTokenType.Multiplication => Keywords.Multiplication,
+            SyntaxTokenType.Division => Keywords.Division,
+            SyntaxTokenType.Modulo => Keywords.Modulo,
+            SyntaxTokenType.Exponentiation => Keywords.Exponentiation,
+            SyntaxTokenType.Equality => Keywords.Equality,
+            SyntaxTokenType.Inequality => Keywords.Inequality,
+            SyntaxTokenType.GreaterThan => Keywords.GreaterThan,
+            SyntaxTokenType.LessThan => Keywords.LessThan,
+            SyntaxTokenType.GreaterThanOrEqual => Keywords.GreaterThanOrEqual,
+            SyntaxTokenType.LessThanOrEqual => Keywords.LessThanOrEqual,
+            SyntaxTokenType.And => Keywords.And,
+            SyntaxTokenType.Not => Keywords.Not,
+            SyntaxTokenType.Or => Keywords.Or,
+            SyntaxTokenType.Assignment => Keywords.Assignment,
+            SyntaxTokenType.Concat => Keywords.Concat,
+            SyntaxTokenType.Length => Keywords.Length,
+            SyntaxTokenType.Break => Keywords.Break,
+            SyntaxTokenType.Do => Keywords.Do,
+            SyntaxTokenType.For => Keywords.For,
+            SyntaxTokenType.Goto => Keywords.Goto,
+            SyntaxTokenType.If => Keywords.If,
+            SyntaxTokenType.ElseIf => Keywords.ElseIf,
+            SyntaxTokenType.Else => Keywords.Else,
+            SyntaxTokenType.Function => Keywords.Function,
+            SyntaxTokenType.End => Keywords.End,
+            SyntaxTokenType.Then => Keywords.Then,
+            SyntaxTokenType.In => Keywords.In,
+            SyntaxTokenType.Local => Keywords.Local,
+            SyntaxTokenType.Repeat => Keywords.Repeat,
+            SyntaxTokenType.Return => Keywords.Return,
+            SyntaxTokenType.Until => Keywords.Until,
+            SyntaxTokenType.While => Keywords.While,
+            _ => "",
+        };
+    }
+
+    public bool Equals(SyntaxToken other)
+    {
+        return other.Type == Type &&
+            other.Text.Span.SequenceEqual(Text.Span) &&
+            other.Position == Position;
+    }
+
+    public override bool Equals(object? obj)
+    {
+        if (obj is SyntaxToken token) return Equals(token);
+        return false;
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(Type, Utf16StringMemoryComparer.Default.GetHashCode(Text), Position);
+    }
+
+    public static bool operator ==(SyntaxToken left, SyntaxToken right)
+    {
+        return left.Equals(right);
+    }
+
+    public static bool operator !=(SyntaxToken left, SyntaxToken right)
+    {
+        return !(left == right);
+    }
+}
+
+public enum SyntaxTokenType
+{
+    /// <summary>
+    /// Invalid token
+    /// </summary>
+    Invalid,
+
+    /// <summary>
+    /// End of line
+    /// </summary>
+    EndOfLine,
+
+    /// <summary>
+    /// Left parenthesis '('
+    /// </summary>
+    LParen,
+    /// <summary>
+    /// Right parenthesis ')'
+    /// </summary>
+    RParen,
+
+    /// <summary>
+    /// Left curly bracket '{'
+    /// </summary>
+    LCurly,
+    /// <summary>
+    /// Right curly bracket '}'
+    /// </summary>
+    RCurly,
+
+    /// <summary>
+    /// Left square bracket '['
+    /// </summary>
+    LSquare,
+    /// <summary>
+    /// Right square bracket ']'
+    /// </summary>
+    RSquare,
+
+    /// <summary>
+    /// Semi colon (;)
+    /// </summary>
+    SemiColon,
+
+    /// <summary>
+    /// Colon (:)
+    /// </summary>
+    Colon,
+
+    /// <summary>
+    /// Comma (,)
+    /// </summary>
+    Comma,
+
+    /// <summary>
+    /// Dot (.)
+    /// </summary>
+    Dot,
+
+    /// <summary>
+    /// Numeric literal (e.g. 1, 2, 1.0, 2.0, ...)
+    /// </summary>
+    Number,
+
+    /// <summary>
+    /// String literal (e.g. "foo", "bar", ...)
+    /// </summary>
+    String,
+
+    /// <summary>
+    /// Raw string literal (e.g. [[Hello, World!]])
+    /// </summary>
+    RawString,
+
+    /// <summary>
+    /// Nil literal (nil)
+    /// </summary>
+    Nil,
+
+    /// <summary>
+    /// Boolean literal (true)
+    /// </summary>
+    True,
+    /// <summary>
+    /// Boolean literal (false)
+    /// </summary>
+    False,
+
+    /// <summary>
+    /// Identifier
+    /// </summary>
+    Identifier,
+
+    /// <summary>
+    /// Label
+    /// </summary>
+    Label,
+
+    /// <summary>
+    /// Addition operator (+)
+    /// </summary>
+    Addition,
+    /// <summary>
+    /// Subtraction operator (-)
+    /// </summary>
+    Subtraction,
+    /// <summary>
+    /// Multiplication operator (*)
+    /// </summary>
+    Multiplication,
+    /// <summary>
+    /// Division operator (/)
+    /// </summary>
+    Division,
+    /// <summary>
+    /// Modulo operator (%)
+    /// </summary>
+    Modulo,
+    /// <summary>
+    /// Exponentiation operator (^)
+    /// </summary>
+    Exponentiation,
+
+    Equality,          // ==
+    Inequality,       // ~=
+    GreaterThan,        // >
+    LessThan,           // <
+    GreaterThanOrEqual, // >=
+    LessThanOrEqual,    // <=
+
+    And,            // and
+    Not,            // not
+    Or,             // or
+
+    /// <summary>
+    /// Assignment operator (=)
+    /// </summary>
+    Assignment,
+
+    Concat,         // ..
+    Length,         // #
+
+    VarArg,         // ...
+
+    Break,          // break
+    Do,             // do
+    For,            // for
+    Goto,           // goto
+
+    If,             // if
+    ElseIf,         // elseif
+    Else,           // else
+    Function,       // function
+
+    End,            // end
+    Then,           // then
+
+    In,             // in
+    Local,          // local
+    Repeat,         // repeat
+    Return,         // return
+    Until,          // until
+    While,          // while
+}

+ 60 - 0
src/Lua/CodeAnalysis/Syntax/SyntaxTokenEnumerator.cs

@@ -0,0 +1,60 @@
+using System.Runtime.CompilerServices;
+
+namespace Lua.CodeAnalysis.Syntax;
+
+public ref struct SyntaxTokenEnumerator(ReadOnlySpan<SyntaxToken> source)
+{
+    ReadOnlySpan<SyntaxToken> source = source;
+    SyntaxToken current;
+    int offset;
+
+    public SyntaxToken Current => current;
+    public int Position => offset;
+    public bool IsCompleted => source.Length == offset;
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool MoveNext()
+    {
+        if (IsCompleted) return false;
+        current = source[offset];
+        offset++;
+        return true;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public bool MovePrevious()
+    {
+        if (offset == 0) return false;
+        offset--;
+        current = source[offset - 1];
+        return true;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void SkipEoL()
+    {
+        while (true)
+        {
+            if (current.Type != SyntaxTokenType.EndOfLine) return;
+            if (!MoveNext()) return;
+        }
+    }
+
+    public SyntaxToken GetNext(bool skipEoL = false)
+    {
+        if (!skipEoL)
+        {
+            return IsCompleted ? default : source[offset];
+        }
+
+        var i = offset;
+        while (i < source.Length)
+        {
+            var c = source[i];
+            if (source[i].Type is not SyntaxTokenType.EndOfLine) return c;
+            i++;
+        }
+
+        return default;
+    }
+}

+ 43 - 1
src/Lua/Exceptions.cs

@@ -1,3 +1,5 @@
+using Lua.CodeAnalysis;
+using Lua.CodeAnalysis.Syntax;
 using Lua.Internal;
 using Lua.Runtime;
 
@@ -18,7 +20,47 @@ public class LuaException : Exception
     }
 }
 
-public class LuaParseException(string message) : LuaException(message);
+public class LuaParseException(string? chunkName, SourcePosition position, string message) : LuaException(message)
+{
+    public string? ChunkName { get; } = chunkName;
+    public SourcePosition? Position { get; } = position;
+
+    public static void UnexpectedToken(string? chunkName, SourcePosition position, SyntaxToken token)
+    {
+        throw new LuaParseException(chunkName, position, $"unexpected symbol <{token.Type}> near '{token.Text}'");
+    }
+
+    public static void ExpectedToken(string? chunkName, SourcePosition position, SyntaxTokenType token)
+    {
+        throw new LuaParseException(chunkName, position, $"'{token}' expected");
+    }
+
+    public static void UnfinishedLongComment(string? chunkName, SourcePosition position)
+    {
+        throw new LuaParseException(chunkName, position, $"unfinished long comment (starting at line {position.Line})");
+    }
+
+    public static void SyntaxError(string? chunkName, SourcePosition position, SyntaxToken? token)
+    {
+        throw new LuaParseException(chunkName, position, $"syntax error {(token == null ? "" : $"near '{token.Value.Text}'")}");
+    }
+
+    public static void NoVisibleLabel(string label, string? chunkName, SourcePosition position)
+    {
+        throw new LuaParseException(chunkName, position, $"no visible label '{label}' for <goto>");
+    }
+
+    public static void BreakNotInsideALoop(string? chunkName, SourcePosition position)
+    {
+        throw new LuaParseException(chunkName, position, "<break> not inside a loop");
+    }
+
+    public override string Message => $"{ChunkName}:{(Position == null ? "" : $"{Position.Value}:")} {base.Message}";
+}
+
+public class LuaScanException(string message) : LuaException(message);
+
+public class LuaUnDumpException(string message) : LuaException(message);
 
 public class LuaRuntimeException : LuaException
 {

+ 241 - 0
tests/Lua.Tests/LexerTests.cs

@@ -0,0 +1,241 @@
+using Lua.CodeAnalysis.Syntax;
+
+namespace Lua.Tests;
+
+public class LexerTests
+{
+    [Test]
+    [TestCase("0")]
+    [TestCase("123")]
+    [TestCase("1234567890")]
+    public void Test_Numeric_Integer(string x)
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Number(x, new(1, 0))
+        };
+        var actual = GetTokens(x);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("1.2")]
+    [TestCase("123.45")]
+    [TestCase("12345.6789")]
+    public void Test_Numeric_Decimal(string x)
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Number(x, new(1, 0))
+        };
+        var actual = GetTokens(x);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("0x123")]
+    [TestCase("0x456")]
+    [TestCase("0x789")]
+    public void Test_Numeric_Hex(string x)
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Number(x, new(1, 0))
+        };
+        var actual = GetTokens(x);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("12E3")]
+    [TestCase("45E+6")]
+    [TestCase("78E-9")]
+    [TestCase("1e+2")]
+    [TestCase("3e-4")]
+    public void Test_Numeric_Exponential(string x)
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Number(x, new(1, 0))
+        };
+        var actual = GetTokens(x);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("\"\"")]
+    [TestCase("\"hello\"")]
+    [TestCase("\"1.23\"")]
+    [TestCase("\"1-2-3-4-5\"")]
+    [TestCase("\'hello\'")]
+    public void Test_String(string x)
+    {
+        var expected = new[]
+        {
+            SyntaxToken.String(x.AsMemory(1, x.Length - 2), new(1, 0))
+        };
+        var actual = GetTokens(x);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("foo")]
+    [TestCase("bar")]
+    [TestCase("baz")]
+    public void Test_Identifier(string x)
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Identifier(x, new(1, 0))
+        };
+        var actual = GetTokens(x);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("-- hello!")]
+    [TestCase("-- how are you?")]
+    [TestCase("-- goodbye!")]
+    public void Test_Comment_Line(string code)
+    {
+        var expected = Array.Empty<SyntaxToken>();
+        var actual = GetTokens(code);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase(@"--[[
+        hello!
+        how are you?
+        goodbye!
+    ]]--")]
+    [TestCase(@"--[[
+        hello!
+        how are you?
+        goodbye!
+    ]]")]
+    public void Test_Comment_Block(string code)
+    {
+        var expected = Array.Empty<SyntaxToken>();
+        var actual = GetTokens(code);
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    [TestCase("--[[ \n hello")]
+    [TestCase("--[[ \r hello")]
+    [TestCase("--[[ \r\n hello")]
+    public void Test_Comment_Block_Error(string code)
+    {
+        Assert.Throws<LuaParseException>(() => GetTokens(code), "main.lua:(1,5): unfinished long comment (starting at line 0)");
+    }
+
+    [Test]
+    public void Test_Comment_Line_WithCode()
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Number("10.0", new(1, 0))
+        };
+        var actual = GetTokens("10.0 -- this is numeric literal");
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    public void Test_Nil()
+    {
+        var expected = new[]
+        {
+            SyntaxToken.Nil(new(1, 0))
+        };
+        var actual = GetTokens("nil");
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    public void Test_True()
+    {
+        var expected = new[]
+        {
+            SyntaxToken.True(new(1, 0))
+        };
+        var actual = GetTokens("true");
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    public void Test_False()
+    {
+        var expected = new[]
+        {
+            SyntaxToken.False(new(1, 0))
+        };
+        var actual = GetTokens("false");
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    public void Test_If()
+    {
+        var expected = new[]
+        {
+            SyntaxToken.If(new(1, 0)), SyntaxToken.Identifier("x", new(1, 3)), SyntaxToken.Equality(new(1, 5)), SyntaxToken.Number("1.0", new(1, 8)), SyntaxToken.Then(new(1, 12)), SyntaxToken.EndOfLine(new(1, 16)),
+            SyntaxToken.Return(new(2, 4)), SyntaxToken.Nil(new(2, 11)), SyntaxToken.EndOfLine(new(2, 14)),
+            SyntaxToken.End(new(3, 0)),
+        };
+        var actual = GetTokens(
+@"if x == 1.0 then
+    return nil
+end");
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    [Test]
+    public void Test_If_Else()
+    {
+        var expected = new[]
+        {
+            SyntaxToken.If(new(1, 0)), SyntaxToken.Identifier("x", new(1, 3)), SyntaxToken.Equality(new(1, 5)), SyntaxToken.Number("1.0", new(1, 8)), SyntaxToken.Then(new(1, 12)), SyntaxToken.EndOfLine(new(1, 16)),
+            SyntaxToken.Return(new(2, 4)), SyntaxToken.Number("1.0", new(2, 11)), SyntaxToken.EndOfLine(new(2, 14)),
+            SyntaxToken.Else(new(3, 0)), SyntaxToken.EndOfLine(new(3, 4)),
+            SyntaxToken.Return(new(4, 4)), SyntaxToken.Number("0.0", new(4, 11)), SyntaxToken.EndOfLine(new(4, 14)),
+            SyntaxToken.End(new(5, 0)),
+        };
+        var actual = GetTokens(
+@"if x == 1.0 then
+    return 1.0
+else
+    return 0.0
+end");
+
+        CollectionAssert.AreEqual(expected, actual);
+    }
+
+    static SyntaxToken[] GetTokens(string source)
+    {
+        var list = new List<SyntaxToken>();
+        var lexer = new Lexer
+        {
+            Source = source.AsMemory(),
+            ChunkName = "main.lua"
+        };
+        while (lexer.MoveNext())
+        {
+            list.Add(lexer.Current);
+        }
+        return list.ToArray();
+    }
+}

+ 29 - 0
tests/Lua.Tests/ParserTests.cs

@@ -0,0 +1,29 @@
+using Lua.CodeAnalysis.Syntax;
+using Lua.CodeAnalysis.Syntax.Nodes;
+
+namespace Lua.Tests
+{
+    // TODO: add more tests
+
+    public class ParserTests
+    {
+        [Test]
+        public void Test_If_ElseIf_Else_Empty()
+        {
+            var source =
+@"if true then
+elseif true then
+else
+end";
+            var actual = LuaSyntaxTree.Parse(source).Nodes[0];
+            var expected = new IfStatementNode(
+                new() { Position = new(1,8),ConditionNode = new BooleanLiteralNode(true, new(1, 3)), ThenNodes = [] },
+                [new() {Position = new(2,13), ConditionNode = new BooleanLiteralNode(true, new(2, 7)), ThenNodes = [] }],
+                [],
+                new(1, 0));
+
+            Assert.That(actual, Is.TypeOf<IfStatementNode>());
+            Assert.That(actual.ToString(), Is.EqualTo(expected.ToString()));
+        }
+    }
+}