Browse Source

Merge pull request #18 from AnnulusGames/string-library

Add: string library
Annulus Games 1 year ago
parent
commit
307c39178c
30 changed files with 1307 additions and 128 deletions
  1. 14 1
      src/Lua/CodeAnalysis/Compilation/LuaCompiler.cs
  2. 13 4
      src/Lua/CodeAnalysis/Syntax/DisplayStringSyntaxVisitor.cs
  3. 22 27
      src/Lua/CodeAnalysis/Syntax/Lexer.cs
  4. 1 1
      src/Lua/CodeAnalysis/Syntax/Nodes/StringLiteralNode.cs
  5. 9 4
      src/Lua/CodeAnalysis/Syntax/Parser.cs
  6. 10 4
      src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs
  7. 364 0
      src/Lua/Internal/StringHelper.cs
  8. 48 23
      src/Lua/LuaValue.cs
  9. 0 39
      src/Lua/Runtime/LuaValueRuntimeExtensions.cs
  10. 29 14
      src/Lua/Runtime/LuaVirtualMachine.cs
  11. 6 4
      src/Lua/Standard/Basic/PrintFunction.cs
  12. 1 1
      src/Lua/Standard/Basic/ToNumberFunction.cs
  13. 51 0
      src/Lua/Standard/OpenLibExtensions.cs
  14. 1 1
      src/Lua/Standard/OperatingSystem/DateFunction.cs
  15. 1 1
      src/Lua/Standard/OperatingSystem/DateTimeHelper.cs
  16. 1 1
      src/Lua/Standard/OperatingSystem/ExitFunction.cs
  17. 3 3
      src/Lua/Standard/Table/ConcatFunction.cs
  18. 32 0
      src/Lua/Standard/Text/ByteFunction.cs
  19. 31 0
      src/Lua/Standard/Text/CharFunction.cs
  20. 15 0
      src/Lua/Standard/Text/DumpFunction.cs
  21. 81 0
      src/Lua/Standard/Text/FindFunction.cs
  22. 289 0
      src/Lua/Standard/Text/FormatFunction.cs
  23. 55 0
      src/Lua/Standard/Text/GMatchFunction.cs
  24. 104 0
      src/Lua/Standard/Text/GSubFunction.cs
  25. 15 0
      src/Lua/Standard/Text/LenFunction.cs
  26. 15 0
      src/Lua/Standard/Text/LowerFunction.cs
  27. 36 0
      src/Lua/Standard/Text/RepFunction.cs
  28. 20 0
      src/Lua/Standard/Text/ReverseFunction.cs
  29. 25 0
      src/Lua/Standard/Text/SubFunction.cs
  30. 15 0
      src/Lua/Standard/Text/UpperFunction.cs

+ 14 - 1
src/Lua/CodeAnalysis/Compilation/LuaCompiler.cs

@@ -69,7 +69,20 @@ public sealed class LuaCompiler : ISyntaxNodeVisitor<ScopeCompilationContext, bo
 
     public bool VisitStringLiteralNode(StringLiteralNode node, ScopeCompilationContext context)
     {
-        var index = context.Function.GetConstantIndex(node.Text);
+        string? str;
+        if (node.IsShortLiteral)
+        {
+            if (!StringHelper.TryFromStringLiteral(node.Text.Span, out str))
+            {
+                throw new LuaParseException(context.Function.ChunkName, node.Position, $"invalid escape sequence near '{node.Text}'");
+            }
+        }
+        else
+        {
+            str = node.Text.ToString();
+        }
+
+        var index = context.Function.GetConstantIndex(str);
         context.PushInstruction(Instruction.LoadK(context.StackPosition, index), node.Position, true);
         return true;
     }

+ 13 - 4
src/Lua/CodeAnalysis/Syntax/DisplayStringSyntaxVisitor.cs

@@ -193,7 +193,7 @@ public sealed class DisplayStringSyntaxVisitor : ISyntaxNodeVisitor<DisplayStrin
     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());
@@ -411,9 +411,18 @@ public sealed class DisplayStringSyntaxVisitor : ISyntaxNodeVisitor<DisplayStrin
 
     public bool VisitStringLiteralNode(StringLiteralNode node, Context context)
     {
-        context.Append("\"");
-        context.Append(node.Text);
-        context.Append("\"");
+        if (node.IsShortLiteral)
+        {
+            context.Append("\"");
+            context.Append(node.Text.ToString());
+            context.Append("\"");
+        }
+        else
+        {
+            context.Append("[[");
+            context.Append(node.Text.ToString());
+            context.Append("]]");
+        }
         return true;
     }
 

+ 22 - 27
src/Lua/CodeAnalysis/Syntax/Lexer.cs

@@ -1,4 +1,5 @@
 using System.Runtime.CompilerServices;
+using Lua.Internal;
 
 namespace Lua.CodeAnalysis.Syntax;
 
@@ -6,7 +7,7 @@ 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;
@@ -204,7 +205,7 @@ public ref struct Lexer
                     return true;
                 }
 
-                if (!IsNumeric(c2))
+                if (!StringHelper.IsNumber(c2))
                 {
                     current = SyntaxToken.Dot(position);
                     return true;
@@ -223,7 +224,7 @@ public ref struct Lexer
         }
 
         // numeric literal
-        if (c1 is '.' || IsNumeric(c1))
+        if (c1 is '.' || StringHelper.IsNumber(c1))
         {
             if (c1 is '0' && c2 is 'x' or 'X') // hex 0x
             {
@@ -309,19 +310,25 @@ public ref struct Lexer
         {
             var quote = c1;
             var stringStartOffset = offset;
-
             var isTerminated = false;
+
             while (span.Length > offset)
             {
                 var c = span[offset];
-                if (c == quote)
+
+                if (c is '\n' or '\r')
                 {
-                    isTerminated = true;
                     break;
                 }
 
-                if (c is '\n' or '\r')
+                if (c is '\\')
+                {
+                    Advance(1);
+                    if (span.Length <= offset) break;
+                }
+                else if (c == quote)
                 {
+                    isTerminated = true;
                     break;
                 }
 
@@ -350,7 +357,7 @@ public ref struct Lexer
                     throw new LuaParseException(ChunkName, this.position, "error: Unterminated string");
                 }
 
-                current = SyntaxToken.String(Source[start..end], position);
+                current = SyntaxToken.RawString(Source[start..end], position);
                 return true;
             }
             else
@@ -435,7 +442,7 @@ public ref struct Lexer
     }
 
     (int Start, int End, bool IsTerminated) ReadUntilLongBracketEnd(ref ReadOnlySpan<char> span)
-    {   
+    {
         var c = span[offset];
         var level = 0;
         while (c is '=')
@@ -450,6 +457,7 @@ public ref struct Lexer
         var startOffset = offset;
         var endOffset = 0;
         var isTerminated = false;
+        var prevC = char.MinValue;
 
         while (span.Length > offset + level + 1)
         {
@@ -472,7 +480,7 @@ public ref struct Lexer
                 }
             }
 
-            if (current is ']')
+            if (current is ']' && prevC is not '\\')
             {
                 endOffset = offset;
 
@@ -489,6 +497,7 @@ public ref struct Lexer
             }
 
         CONTINUE:
+            prevC = current;
             Advance(1);
         }
 
@@ -499,7 +508,7 @@ public ref struct Lexer
     void ReadDigit(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
     {
         readCount = 0;
-        while (span.Length > offset && IsDigit(span[offset]))
+        while (span.Length > offset && StringHelper.IsDigit(span[offset]))
         {
             Advance(1);
             readCount++;
@@ -510,33 +519,19 @@ public ref struct Lexer
     void ReadNumber(ref ReadOnlySpan<char> span, ref int offset, out int readCount)
     {
         readCount = 0;
-        while (span.Length > offset && IsNumeric(span[offset]))
+        while (span.Length > offset && StringHelper.IsNumber(span[offset]))
         {
             Advance(1);
             readCount++;
         }
     }
 
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    static bool IsDigit(char c)
-    {
-        return IsNumeric(c) ||
-            ('a' <= c && c <= 'f') ||
-            ('A' <= c && c <= 'F');
-    }
-
-    [MethodImpl(MethodImplOptions.AggressiveInlining)]
-    static bool IsNumeric(char c)
-    {
-        return '0' <= c && c <= '9';
-    }
-
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     static bool IsIdentifier(char c)
     {
         return c == '_' ||
             ('A' <= c && c <= 'Z') ||
             ('a' <= c && c <= 'z') ||
-            IsNumeric(c);
+            StringHelper.IsNumber(c);
     }
 }

+ 1 - 1
src/Lua/CodeAnalysis/Syntax/Nodes/StringLiteralNode.cs

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

+ 9 - 4
src/Lua/CodeAnalysis/Syntax/Parser.cs

@@ -556,12 +556,13 @@ public ref struct Parser
         {
             SyntaxTokenType.Identifier => enumerator.GetNext(true).Type switch
             {
-                SyntaxTokenType.LParen or SyntaxTokenType.String => ParseCallFunctionExpression(ref enumerator, null),
+                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.ToString(), 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),
@@ -587,7 +588,7 @@ public ref struct Parser
             result = ParseTableAccessExpression(ref enumerator, result);
             goto RECURSIVE;
         }
-        else if (nextType is SyntaxTokenType.LParen or SyntaxTokenType.String or SyntaxTokenType.LCurly)
+        else if (nextType is SyntaxTokenType.LParen or SyntaxTokenType.String or SyntaxTokenType.RawString or SyntaxTokenType.LCurly)
         {
             MoveNextWithValidation(ref enumerator);
             result = ParseCallFunctionExpression(ref enumerator, result);
@@ -859,7 +860,11 @@ public ref struct Parser
     {
         if (enumerator.Current.Type is SyntaxTokenType.String)
         {
-            return [new StringLiteralNode(enumerator.Current.Text.ToString(), enumerator.Current.Position)];
+            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)
         {

+ 10 - 4
src/Lua/CodeAnalysis/Syntax/SyntaxToken.cs

@@ -91,14 +91,14 @@ public readonly struct SyntaxToken(SyntaxTokenType type, ReadOnlyMemory<char> te
         return new(SyntaxTokenType.Identifier, text, position);
     }
 
-    public static SyntaxToken String(string text, SourcePosition position)
+    public static SyntaxToken String(ReadOnlyMemory<char> text, SourcePosition position)
     {
-        return new(SyntaxTokenType.String, text.AsMemory(), position);
+        return new(SyntaxTokenType.String, text, position);
     }
 
-    public static SyntaxToken String(ReadOnlyMemory<char> text, SourcePosition position)
+    public static SyntaxToken RawString(ReadOnlyMemory<char> text, SourcePosition position)
     {
-        return new(SyntaxTokenType.String, text, position);
+        return new(SyntaxTokenType.RawString, text, position);
     }
 
     public static SyntaxToken Label(ReadOnlyMemory<char> text, SourcePosition position)
@@ -126,6 +126,7 @@ public readonly struct SyntaxToken(SyntaxTokenType type, ReadOnlyMemory<char> te
             SyntaxTokenType.Comma => ",",
             SyntaxTokenType.Number => Text.ToString(),
             SyntaxTokenType.String => $"\"{Text}\"",
+            SyntaxTokenType.RawString => $"[[{Text}]]",
             SyntaxTokenType.Nil => Keywords.Nil,
             SyntaxTokenType.True => Keywords.True,
             SyntaxTokenType.False => Keywords.False,
@@ -266,6 +267,11 @@ public enum SyntaxTokenType
     /// </summary>
     String,
 
+    /// <summary>
+    /// Raw string literal (e.g. [[Hello, World!]])
+    /// </summary>
+    RawString,
+
     /// <summary>
     /// Nil literal (nil)
     /// </summary>

+ 364 - 0
src/Lua/Internal/StringHelper.cs

@@ -0,0 +1,364 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Lua.Internal;
+
+internal static class StringHelper
+{
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static ReadOnlySpan<char> Slice(string s, int i, int j)
+    {
+        if (i < 0) i = s.Length + i + 1;
+        if (j < 0) j = s.Length + j + 1;
+
+        if (i < 1) i = 1;
+        if (j > s.Length) j = s.Length;
+
+        return i > j ? "" : s.AsSpan()[(i - 1)..j];
+    }
+
+    public static bool TryFromStringLiteral(ReadOnlySpan<char> literal, [NotNullWhen(true)] out string? result)
+    {
+        var builder = new ValueStringBuilder(literal.Length);
+        for (int i = 0; i < literal.Length; i++)
+        {
+            var c = literal[i];
+            if (c is '\\' && i < literal.Length - 1)
+            {
+                i++;
+                c = literal[i];
+
+                switch (c)
+                {
+                    case '\n':
+                        builder.Append('\n');
+                        break;
+                    case '\r':
+                        builder.Append('\r');
+                        // check CRLF
+                        if (i + 1 < literal.Length && literal[i + 1] is '\n')
+                        {
+                            i++;
+                        }
+                        break;
+                    case 'a':
+                        builder.Append('\a');
+                        break;
+                    case 'b':
+                        builder.Append('\b');
+                        break;
+                    case 'f':
+                        builder.Append('\f');
+                        break;
+                    case 'n':
+                        builder.Append('\n');
+                        break;
+                    case 'r':
+                        builder.Append('\r');
+                        break;
+                    case 't':
+                        builder.Append('\t');
+                        break;
+                    case 'v':
+                        builder.Append('\v');
+                        break;
+                    case '\\':
+                        builder.Append('\\');
+                        break;
+                    case '\"':
+                        builder.Append('\"');
+                        break;
+                    case '\'':
+                        builder.Append('\'');
+                        break;
+                    case '[':
+                        builder.Append('[');
+                        break;
+                    case ']':
+                        builder.Append(']');
+                        break;
+                    case 'x':
+                        i++;
+                        if (i >= literal.Length)
+                        {
+                            result = null;
+                            return false;
+                        }
+
+                        c = literal[i];
+                        if (IsDigit(c))
+                        {
+                            var start = i;
+                            for (int j = 0; j < 2; j++)
+                            {
+                                i++;
+                                if (i >= literal.Length) break;
+                                c = literal[i];
+                                if (!IsDigit(c)) break;
+                            }
+
+                            builder.Append((char)int.Parse(literal[start..i], NumberStyles.HexNumber));
+                            i--;
+                        }
+                        else
+                        {
+                            result = null;
+                            return false;
+                        }
+                        break;
+                    default:
+                        if (IsNumber(c))
+                        {
+                            var start = i;
+                            for (int j = 0; j < 3; j++)
+                            {
+                                i++;
+                                if (i >= literal.Length) break;
+                                c = literal[i];
+                                if (!IsNumber(c)) break;
+                            }
+
+                            builder.Append((char)int.Parse(literal[start..i]));
+                            i--;
+                        }
+                        else
+                        {
+                            result = null;
+                            return false;
+                        }
+                        break;
+                }
+            }
+            else
+            {
+                builder.Append(c);
+            }
+        }
+
+        result = builder.ToString();
+        return true;
+    }
+
+    public static string Escape(ReadOnlySpan<char> str)
+    {
+        var builder = new ValueStringBuilder(str.Length);
+
+        for (int i = 0; i < str.Length; i++)
+        {
+            var c = str[i];
+
+            switch (c)
+            {
+                case '\a':
+                    builder.Append("\\\a");
+                    break;
+                case '\b':
+                    builder.Append("\\\b");
+                    break;
+                case '\f':
+                    builder.Append("\\\f");
+                    break;
+                case '\n':
+                    builder.Append("\\\n");
+                    break;
+                case '\r':
+                    builder.Append("\\\r");
+                    break;
+                case '\t':
+                    builder.Append("\\\t");
+                    break;
+                case '\v':
+                    builder.Append("\\\v");
+                    break;
+                case '\\':
+                    builder.Append("\\\\");
+                    break;
+                case '\"':
+                    builder.Append("\\\"");
+                    break;
+                case '\'':
+                    builder.Append("\\\'");
+                    break;
+                default:
+                    builder.Append(c);
+                    break;
+            }
+        }
+
+        return builder.ToString();
+    }
+
+    public static Regex ToRegex(ReadOnlySpan<char> pattern)
+    {
+        var builder = new ValueStringBuilder();
+        var isEscapeSequence = false;
+        var isInSet = false;
+
+        for (var i = 0; i < pattern.Length; i++)
+        {
+            var c = pattern[i];
+
+            if (isEscapeSequence)
+            {
+                if (c == '%' || c == '_')
+                {
+                    builder.Append(c);
+                    isEscapeSequence = false;
+                }
+                else
+                {
+                    switch (c)
+                    {
+                        case 'a': // all letters
+                            builder.Append("\\p{L}");
+                            break;
+                        case 'A': // all Non letters
+                            builder.Append("\\P{L}");
+                            break;
+                        case 's': // all space characters
+                            builder.Append("\\s");
+                            break;
+                        case 'S': // all NON space characters
+                            builder.Append("\\S");
+                            break;
+                        case 'd': // all digits
+                            builder.Append("\\d");
+                            break;
+                        case 'D': // all NON digits
+                            builder.Append("\\D");
+                            break;
+                        case 'w': // all alphanumeric characters
+                            builder.Append("\\w");
+                            break;
+                        case 'W': // all NON alphanumeric characters
+                            builder.Append("\\W");
+                            break;
+                        case 'c': // all control characters
+                            builder.Append("\\p{C}");
+                            break;
+                        case 'C': // all NON control characters
+                            builder.Append("[\\P{C}]");
+                            break;
+                        case 'g': // all printable characters except space
+                            builder.Append("[^\\p{C}\\s]");
+                            break;
+                        case 'G': // all NON printable characters including space
+                            builder.Append("[\\p{C}\\s]");
+                            break;
+                        case 'p': // all punctuation characters
+                            builder.Append("\\p{P}");
+                            break;
+                        case 'P': // all NON punctuation characters
+                            builder.Append("\\P{P}");
+                            break;
+                        case 'l': // all lowercase letters
+                            builder.Append("\\p{Ll}");
+                            break;
+                        case 'L': // all NON lowercase letters
+                            builder.Append("\\P{Ll}");
+                            break;
+                        case 'u': // all uppercase letters
+                            builder.Append("\\p{Lu}");
+                            break;
+                        case 'U': // all NON uppercase letters
+                            builder.Append("\\P{Lu}");
+                            break;
+                        case 'x': // all hexadecimal digits
+                            builder.Append("[0-9A-Fa-f]");
+                            break;
+                        case 'X': // all NON hexadecimal digits
+                            builder.Append("[^0-9A-Fa-f]");
+                            break;
+                        case 'b':
+                            if (i < pattern.Length - 2)
+                            {
+                                var c1 = pattern[i + 1];
+                                var c2 = pattern[i + 2];
+
+                                var c1Escape = Regex.Escape(c1.ToString());
+                                var c2Escape = Regex.Escape(c2.ToString());
+
+                                builder.Append("(");
+                                builder.Append(c1Escape);
+                                builder.Append("(?>(?<n>");
+                                builder.Append(c1Escape);
+                                builder.Append(")|(?<-n>");
+                                builder.Append(c2Escape);
+                                builder.Append(")|(?:[^");
+                                builder.Append(c1Escape);
+                                builder.Append(c2Escape);
+                                builder.Append("]*))*");
+                                builder.Append(c2Escape);
+                                builder.Append("(?(n)(?!)))");
+                                i += 2;
+                            }
+                            else
+                            {
+                                throw new Exception(); // TODO: add message
+                            }
+
+                            break;
+                        default:
+                            builder.Append('\\');
+                            builder.Append(c);
+                            break;
+                    }
+                    isEscapeSequence = false;
+                }
+            }
+            else if (c == '%')
+            {
+                isEscapeSequence = true;
+            }
+            else if (c == '\\')
+            {
+                builder.Append("\\\\");
+            }
+            else if (isInSet)
+            {
+                if (c == ']') isInSet = false;
+                builder.Append(c);
+            }
+            else if (c == '-')
+            {
+                builder.Append("*?");
+            }
+            else if (c == '[')
+            {
+                builder.Append('[');
+                isInSet = true;
+            }
+            else if (c == '^' && !isInSet)
+            {
+                builder.Append("\\G");
+            }
+            else if (c == '(')
+            {
+                builder.Append('(');
+            }
+            else
+            {
+                builder.Append(c);
+            }
+        }
+
+        return new Regex(builder.ToString());
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static bool IsNumber(char c)
+    {
+        return '0' <= c && c <= '9';
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public static bool IsDigit(char c)
+    {
+        return IsNumber(c) ||
+            ('a' <= c && c <= 'f') ||
+            ('A' <= c && c <= 'F');
+    }
+}

+ 48 - 23
src/Lua/LuaValue.cs

@@ -1,5 +1,6 @@
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
+using Lua.Internal;
 using Lua.Runtime;
 
 namespace Lua;
@@ -34,19 +35,7 @@ public readonly struct LuaValue : IEquatable<LuaValue>
         switch (type)
         {
             case LuaValueType.Number:
-                if (t == typeof(int))
-                {
-                    var v = (int)value;
-                    result = Unsafe.As<int, T>(ref v);
-                    return true;
-                }
-                else if (t == typeof(long))
-                {
-                    var v = (long)value;
-                    result = Unsafe.As<long, T>(ref v);
-                    return true;
-                }
-                else if (t == typeof(float))
+                if (t == typeof(float))
                 {
                     var v = (float)value;
                     result = Unsafe.As<float, T>(ref v);
@@ -90,6 +79,40 @@ public readonly struct LuaValue : IEquatable<LuaValue>
                     result = Unsafe.As<string, T>(ref v);
                     return true;
                 }
+                else if (t == typeof(double))
+                {
+                    var str = (string)referenceValue!;
+                    var span = str.AsSpan().Trim();
+
+                    var sign = 1;
+                    if (span.Length > 0 && span[0] == '-')
+                    {
+                        sign = -1;
+                        span = span[1..];
+                    }
+
+                    if (span.Length > 2 && span[0] is '0' && span[1] is 'x' or 'X')
+                    {
+                        // TODO: optimize
+                        try
+                        {
+                            var d = HexConverter.ToDouble(span) * sign;
+                            result = Unsafe.As<double, T>(ref d);
+                            return true;
+                        }
+                        catch (FormatException)
+                        {
+                            result = default!;
+                            return false;
+                        }
+                    }
+                    else
+                    {
+                        var tryResult = double.TryParse(str, out var d);
+                        result = tryResult ? Unsafe.As<double, T>(ref d) : default!;
+                        return tryResult;
+                    }
+                }
                 else if (t == typeof(object))
                 {
                     result = (T)referenceValue!;
@@ -312,14 +335,14 @@ public readonly struct LuaValue : IEquatable<LuaValue>
     {
         return type switch
         {
-            LuaValueType.Nil => "Nil",
-            LuaValueType.Boolean => Read<bool>().ToString(),
-            LuaValueType.String => Read<string>().ToString(),
+            LuaValueType.Nil => "nil",
+            LuaValueType.Boolean => Read<bool>() ? "true" : "false",
+            LuaValueType.String => Read<string>(),
             LuaValueType.Number => Read<double>().ToString(),
-            LuaValueType.Function => Read<LuaFunction>().ToString(),
-            LuaValueType.Thread => Read<LuaThread>().ToString(),
-            LuaValueType.Table => Read<LuaTable>().ToString(),
-            LuaValueType.UserData => referenceValue?.ToString(),
+            LuaValueType.Function => $"function: {referenceValue!.GetHashCode()}",
+            LuaValueType.Thread => $"thread: {referenceValue!.GetHashCode()}",
+            LuaValueType.Table => $"table: {referenceValue!.GetHashCode()}",
+            LuaValueType.UserData => $"userdata: {referenceValue!.GetHashCode()}",
             _ => "",
         };
     }
@@ -361,7 +384,7 @@ public readonly struct LuaValue : IEquatable<LuaValue>
         return false;
     }
 
-    internal async ValueTask<int> CallToStringAsync(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    internal ValueTask<int> CallToStringAsync(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     {
         if (this.TryGetMetamethod(context.State, Metamethods.ToString, out var metamethod))
         {
@@ -371,15 +394,17 @@ public readonly struct LuaValue : IEquatable<LuaValue>
             }
 
             context.State.Push(this);
-            return await func.InvokeAsync(context with
+
+            return func.InvokeAsync(context with
             {
                 ArgumentCount = 1,
+                StackPosition = context.State.CurrentThread.Stack.Count,
             }, buffer, cancellationToken);
         }
         else
         {
             buffer.Span[0] = ToString()!;
-            return 1;
+            return new(1);
         }
     }
 }

+ 0 - 39
src/Lua/Runtime/LuaValueRuntimeExtensions.cs

@@ -1,49 +1,10 @@
 using System.Buffers;
 using System.Runtime.CompilerServices;
-using Lua.Internal;
 
 namespace Lua.Runtime;
 
 internal static class LuaRuntimeExtensions
 {
-    public static bool TryGetNumber(this LuaValue value, out double result)
-    {
-        if (value.TryRead(out result)) return true;
-
-        if (value.TryRead<string>(out var str))
-        {
-            var span = str.AsSpan().Trim();
-
-            var sign = 1;
-            if (span.Length > 0 && span[0] == '-')
-            {
-                sign = -1;
-                span = span[1..];
-            }
-
-            if (span.Length > 2 && span[0] is '0' && span[1] is 'x' or 'X')
-            {
-                // TODO: optimize
-                try
-                {
-                    result = HexConverter.ToDouble(span) * sign;
-                    return true;
-                }
-                catch (FormatException)
-                {
-                    return false;
-                }
-            }
-            else
-            {
-                return double.TryParse(str, out result);
-            }
-        }
-
-        result = default;
-        return false;
-    }
-
     public static bool TryGetMetamethod(this LuaValue value, LuaState state, string methodName, out LuaValue result)
     {
         result = default;

+ 29 - 14
src/Lua/Runtime/LuaVirtualMachine.cs

@@ -108,7 +108,7 @@ public static partial class LuaVirtualMachine
                         var table = stack.UnsafeGet(RB);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
                         var value = await GetTableValue(state, chunk, pc, table, vc, cancellationToken);
-                        
+
                         stack.UnsafeGet(RA + 1) = table;
                         stack.UnsafeGet(RA) = value;
                         stack.NotifyTop(RA + 2);
@@ -121,7 +121,7 @@ public static partial class LuaVirtualMachine
                         var vb = RK(stack, chunk, instruction.B, frame.Base);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
 
-                        if (vb.TryGetNumber(out var valueB) && vc.TryGetNumber(out var valueC))
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             stack.UnsafeGet(RA) = valueB + valueC;
                         }
@@ -160,7 +160,7 @@ public static partial class LuaVirtualMachine
                         var vb = RK(stack, chunk, instruction.B, frame.Base);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
 
-                        if (vb.TryGetNumber(out var valueB) && vc.TryGetNumber(out var valueC))
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             stack.UnsafeGet(RA) = valueB - valueC;
                         }
@@ -199,7 +199,7 @@ public static partial class LuaVirtualMachine
                         var vb = RK(stack, chunk, instruction.B, frame.Base);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
 
-                        if (vb.TryGetNumber(out var valueB) && vc.TryGetNumber(out var valueC))
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             stack.UnsafeGet(RA) = valueB * valueC;
                         }
@@ -238,7 +238,7 @@ public static partial class LuaVirtualMachine
                         var vb = RK(stack, chunk, instruction.B, frame.Base);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
 
-                        if (vb.TryGetNumber(out var valueB) && vc.TryGetNumber(out var valueC))
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             stack.UnsafeGet(RA) = valueB / valueC;
                         }
@@ -277,7 +277,7 @@ public static partial class LuaVirtualMachine
                         var vb = RK(stack, chunk, instruction.B, frame.Base);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
 
-                        if (vb.TryGetNumber(out var valueB) && vc.TryGetNumber(out var valueC))
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             stack.UnsafeGet(RA) = valueB % valueC;
                         }
@@ -316,7 +316,7 @@ public static partial class LuaVirtualMachine
                         var vb = RK(stack, chunk, instruction.B, frame.Base);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
 
-                        if (vb.TryGetNumber(out var valueB) && vc.TryGetNumber(out var valueC))
+                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             stack.UnsafeGet(RA) = Math.Pow(valueB, valueC);
                         }
@@ -354,7 +354,7 @@ public static partial class LuaVirtualMachine
 
                         var vb = stack.UnsafeGet(RB);
 
-                        if (vb.TryGetNumber(out var valueB))
+                        if (vb.TryRead<double>(out var valueB))
                         {
                             stack.UnsafeGet(RA) = -valueB;
                         }
@@ -540,7 +540,11 @@ public static partial class LuaVirtualMachine
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
                         var compareResult = false;
 
-                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        if (vb.TryRead<string>(out var strB) && vc.TryRead<string>(out var strC))
+                        {
+                            compareResult = StringComparer.Ordinal.Compare(strB, strC) < 0;
+                        }
+                        else if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             compareResult = valueB < valueC;
                         }
@@ -589,7 +593,11 @@ public static partial class LuaVirtualMachine
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
                         var compareResult = false;
 
-                        if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
+                        if (vb.TryRead<string>(out var strB) && vc.TryRead<string>(out var strC))
+                        {
+                            compareResult = StringComparer.Ordinal.Compare(strB, strC) <= 0;
+                        }
+                        else if (vb.TryRead<double>(out var valueB) && vc.TryRead<double>(out var valueC))
                         {
                             compareResult = valueB <= valueC;
                         }
@@ -688,12 +696,19 @@ public static partial class LuaVirtualMachine
                                 resultCount = instruction.C - 1;
                             }
 
-                            stack.EnsureCapacity(RA + resultCount);
-                            for (int i = 0; i < resultCount; i++)
+                            if (resultCount == 0)
                             {
-                                stack.UnsafeGet(RA + i) = resultBuffer[i];
+                                stack.Pop();
+                            }
+                            else
+                            {
+                                stack.EnsureCapacity(RA + resultCount);
+                                for (int i = 0; i < resultCount; i++)
+                                {
+                                    stack.UnsafeGet(RA + i) = resultBuffer[i];
+                                }
+                                stack.NotifyTop(RA + resultCount);
                             }
-                            stack.NotifyTop(RA + resultCount);
                         }
                         finally
                         {

+ 6 - 4
src/Lua/Standard/Basic/PrintFunction.cs

@@ -1,3 +1,5 @@
+using Lua.Internal;
+
 namespace Lua.Standard.Basic;
 
 public sealed class PrintFunction : LuaFunction
@@ -5,14 +7,14 @@ public sealed class PrintFunction : LuaFunction
     public override string Name => "print";
     public static readonly PrintFunction Instance = new();
 
-    LuaValue[] buffer = new LuaValue[1];
-
     protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
     {
+        using var methodBuffer = new PooledArray<LuaValue>(1);
+
         for (int i = 0; i < context.ArgumentCount; i++)
         {
-            await context.Arguments[i].CallToStringAsync(context, this.buffer, cancellationToken);
-            Console.Write(this.buffer[0]);
+            await context.Arguments[i].CallToStringAsync(context, methodBuffer.AsMemory(), cancellationToken);
+            Console.Write(methodBuffer[0]);
             Console.Write('\t');
         }
 

+ 1 - 1
src/Lua/Standard/Basic/ToNumberFunction.cs

@@ -28,7 +28,7 @@ public sealed class ToNumberFunction : LuaFunction
         {
             if (arg1 == 10 || arg1 == 16)
             {
-                if (arg0.TryGetNumber(out var result))
+                if (arg0.TryRead<double>(out var result))
                 {
                     buffer.Span[0] = result;
                 }

+ 51 - 0
src/Lua/Standard/OpenLibExtensions.cs

@@ -1,3 +1,4 @@
+using Lua.Runtime;
 using Lua.Standard.Basic;
 using Lua.Standard.Bitwise;
 using Lua.Standard.Coroutines;
@@ -6,11 +7,24 @@ using Lua.Standard.Mathematics;
 using Lua.Standard.Modules;
 using Lua.Standard.OperatingSystem;
 using Lua.Standard.Table;
+using Lua.Standard.Text;
 
 namespace Lua.Standard;
 
 public static class OpenLibExtensions
 {
+    sealed class StringIndexMetamethod(LuaTable table) : LuaFunction
+    {
+        protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+        {
+            context.GetArgument<string>(0);
+            var key = context.GetArgument(1);
+
+            buffer.Span[0] = table[key];
+            return new(1);
+        }
+    }
+
     static readonly LuaFunction[] baseFunctions = [
         AssertFunction.Instance,
         ErrorFunction.Instance,
@@ -75,6 +89,22 @@ public static class OpenLibExtensions
         SortFunction.Instance,
     ];
 
+    static readonly LuaFunction[] stringFunctions = [
+        ByteFunction.Instance,
+        CharFunction.Instance,
+        DumpFunction.Instance,
+        FindFunction.Instance,
+        FormatFunction.Instance,
+        GMatchFunction.Instance,
+        GSubFunction.Instance,
+        LenFunction.Instance,
+        LowerFunction.Instance,
+        RepFunction.Instance,
+        ReverseFunction.Instance,
+        SubFunction.Instance,
+        UpperFunction.Instance,
+    ];
+
     static readonly LuaFunction[] ioFunctions = [
         OpenFunction.Instance,
         CloseFunction.Instance,
@@ -173,6 +203,27 @@ public static class OpenLibExtensions
         state.Environment["table"] = table;
     }
 
+    public static void OpenStringLibrary(this LuaState state)
+    {
+        var @string = new LuaTable(0, stringFunctions.Length);
+        foreach (var func in stringFunctions)
+        {
+            @string[func.Name] = func;
+        }
+
+        state.Environment["string"] = @string;
+
+        // set __index
+        var key = new LuaValue("");
+        if (!state.TryGetMetatable(key, out var metatable))
+        {
+            metatable = new();
+            state.SetMetatable(key, metatable);
+        }
+
+        metatable[Metamethods.Index] = new StringIndexMetamethod(@string);
+    }
+
     public static void OpenIOLibrary(this LuaState state)
     {
         var io = new LuaTable(0, ioFunctions.Length);

+ 1 - 1
src/Lua/Standard/OperatingSystem/DateFunction.cs

@@ -186,7 +186,7 @@ public sealed class DateFunction : LuaFunction
             }
             else
             {
-                throw new LuaRuntimeException(state.GetTraceback(), $"bad argument #1 to 'date' (invalid conversion specifier '{format}')");
+                throw new LuaRuntimeException(state.GetTraceback(), $"bad argument #1 to 'date' (invalid conversion specifier '{format.ToString()}')");
             }
         }
 

+ 1 - 1
src/Lua/Standard/OperatingSystem/DateTimeHelper.cs

@@ -42,7 +42,7 @@ internal static class DateTimeHelper
                 }
             }
 
-            if (value.TryGetNumber(out var d) && MathEx.IsInteger(d))
+            if (value.TryRead<double>(out var d) && MathEx.IsInteger(d))
             {
                 return (int)d;
             }

+ 1 - 1
src/Lua/Standard/OperatingSystem/ExitFunction.cs

@@ -20,7 +20,7 @@ public sealed class ExitFunction : LuaFunction
             {
                 Environment.Exit(b ? 0 : 1);
             }
-            else if (code.TryGetNumber(out var d))
+            else if (code.TryRead<double>(out var d))
             {
                 if (!MathEx.IsInteger(d))
                 {

+ 3 - 3
src/Lua/Standard/Table/ConcatFunction.cs

@@ -14,15 +14,15 @@ public sealed class ConcatFunction : LuaFunction
             ? context.GetArgument<string>(1)
             : "";
         var arg2 = context.HasArgument(2)
-            ? (int)context.GetArgument<double>(2)
+            ? (long)context.GetArgument<double>(2)
             : 1;
         var arg3 = context.HasArgument(3)
-            ? (int)context.GetArgument<double>(3)
+            ? (long)context.GetArgument<double>(3)
             : arg0.ArrayLength;
 
         var builder = new ValueStringBuilder(512);
 
-        for (int i = arg2; i <= arg3; i++)
+        for (long i = arg2; i <= arg3; i++)
         {
             var value = arg0[i];
 

+ 32 - 0
src/Lua/Standard/Text/ByteFunction.cs

@@ -0,0 +1,32 @@
+
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class ByteFunction : LuaFunction
+{
+    public override string Name => "byte";
+    public static readonly ByteFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        var i = context.HasArgument(1)
+            ? context.GetArgument<double>(1)
+            : 1;
+        var j = context.HasArgument(2)
+            ? context.GetArgument<double>(2)
+            : i;
+
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 2, i);
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 3, j);
+
+        var span = StringHelper.Slice(s, (int)i, (int)j);
+        for (int k = 0; k < span.Length; k++)
+        {
+            buffer.Span[k] = span[k];
+        }
+
+        return new(span.Length);
+    }
+}

+ 31 - 0
src/Lua/Standard/Text/CharFunction.cs

@@ -0,0 +1,31 @@
+
+using System.Text;
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class CharFunction : LuaFunction
+{
+    public override string Name => "char";
+    public static readonly CharFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        if (context.ArgumentCount == 0)
+        {
+            buffer.Span[0] = "";
+            return new(1);
+        }
+
+        var builder = new ValueStringBuilder(context.ArgumentCount);
+        for (int i = 0; i < context.ArgumentCount; i++)
+        {
+            var arg = context.GetArgument<double>(i);
+            LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, i + 1, arg);
+            builder.Append((char)arg);
+        }
+
+        buffer.Span[0] = builder.ToString();
+        return new(1);
+    }
+}

+ 15 - 0
src/Lua/Standard/Text/DumpFunction.cs

@@ -0,0 +1,15 @@
+
+namespace Lua.Standard.Text;
+
+// stirng.dump is not supported (throw exception)
+
+public sealed class DumpFunction : LuaFunction
+{
+    public override string Name => "dump";
+    public static readonly DumpFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        throw new NotSupportedException("stirng.dump is not supported");
+    }
+}

+ 81 - 0
src/Lua/Standard/Text/FindFunction.cs

@@ -0,0 +1,81 @@
+
+using System.Text.RegularExpressions;
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class FindFunction : LuaFunction
+{
+    public override string Name => "find";
+    public static readonly FindFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        var pattern = context.GetArgument<string>(1);
+        var init = context.HasArgument(2)
+            ? context.GetArgument<double>(2)
+            : 1;
+        var plain = context.HasArgument(3)
+            ? context.GetArgument(3).ToBoolean()
+            : false;
+
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 3, init);
+
+        // init can be negative value
+        if (init < 0)
+        {
+            init = s.Length + init + 1;
+        }
+
+        // out of range
+        if (init != 1 && (init < 1 || init > s.Length))
+        {
+            buffer.Span[0] = LuaValue.Nil;
+            return new(1);
+        }
+
+        // empty pattern
+        if (pattern.Length == 0)
+        {
+            buffer.Span[0] = 1;
+            buffer.Span[1] = 1;
+            return new(2);
+        }
+
+        var source = s.AsSpan()[(int)(init - 1)..];
+
+        if (plain)
+        {
+            var start = source.IndexOf(pattern);
+            if (start == -1)
+            {
+                buffer.Span[0] = LuaValue.Nil;
+                return new(1);
+            }
+
+            // 1-based
+            buffer.Span[0] = start + 1;
+            buffer.Span[1] = start + pattern.Length;
+            return new(2);
+        }
+        else
+        {
+            var regex = StringHelper.ToRegex(pattern);
+            var match = regex.Match(source.ToString());
+
+            if (match.Success)
+            {
+                // 1-based
+                buffer.Span[0] = init + match.Index;
+                buffer.Span[1] = init + match.Index + match.Length - 1;
+                return new(2);
+            }
+            else
+            {
+                buffer.Span[0] = LuaValue.Nil;
+                return new(1);
+            }
+        }
+    }
+}

+ 289 - 0
src/Lua/Standard/Text/FormatFunction.cs

@@ -0,0 +1,289 @@
+
+using System.Text;
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+// Ignore 'p' format
+ 
+public sealed class FormatFunction : LuaFunction
+{
+    public override string Name => "format";
+    public static readonly FormatFunction Instance = new();
+
+    protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var format = context.GetArgument<string>(0);
+
+        // TODO: pooling StringBuilder
+        var builder = new StringBuilder(format.Length * 2);
+        var parameterIndex = 1;
+
+        for (int i = 0; i < format.Length; i++)
+        {
+            if (format[i] == '%')
+            {
+                i++;
+
+                // escape
+                if (format[i] == '%')
+                {
+                    builder.Append('%');
+                    continue;
+                }
+                
+                var leftJustify = false;
+                var plusSign = false;
+                var zeroPadding = false;
+                var alternateForm = false;
+                var blank = false;
+                var width = 0;
+                var precision = -1;
+
+                // Process flags
+                while (true)
+                {
+                    var c = format[i];
+                    switch (c)
+                    {
+                        case '-':
+                            if (leftJustify) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
+                            leftJustify = true;
+                            break;
+                        case '+':
+                            if (plusSign) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
+                            plusSign = true;
+                            break;
+                        case '0':
+                            if (zeroPadding) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
+                            zeroPadding = true;
+                            break;
+                        case '#':
+                            if (alternateForm) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
+                            alternateForm = true;
+                            break;
+                        case ' ':
+                            if (blank) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
+                            blank = true;
+                            break;
+                        default:
+                            goto PROCESS_WIDTH;
+                    }
+
+                    i++;
+                }
+
+            PROCESS_WIDTH:
+
+                // Process width
+                var start = i;
+                if (char.IsDigit(format[i]))
+                {
+                    i++;
+                    if (char.IsDigit(format[i])) i++;
+                    if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
+                    width = int.Parse(format.AsSpan()[start..i]);
+                }
+
+                // Process precision
+                if (format[i] == '.')
+                {
+                    i++;
+                    start = i;
+                    if (char.IsDigit(format[i])) i++;
+                    if (char.IsDigit(format[i])) i++;
+                    if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
+                    precision = int.Parse(format.AsSpan()[start..i]);
+                }
+
+                // Process conversion specifier
+                var specifier = format[i];
+
+                if (context.ArgumentCount <= parameterIndex)
+                {
+                    throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #{parameterIndex + 1} to 'format' (no value)");
+                }
+                var parameter = context.GetArgument(parameterIndex++);
+
+                // TODO: reduce allocation
+                string formattedValue = default!;
+                switch (specifier)
+                {
+                    case 'f':
+                    case 'e':
+                    case 'g':
+                    case 'G':
+                        if (!parameter.TryRead<double>(out var f))
+                        {
+                            LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
+                        }
+
+                        switch (specifier)
+                        {
+                            case 'f':
+                                formattedValue = precision < 0
+                                    ? f.ToString()
+                                    : f.ToString($"F{precision}");
+                                break;
+                            case 'e':
+                                formattedValue = precision < 0
+                                    ? f.ToString()
+                                    : f.ToString($"E{precision}");
+                                break;
+                            case 'g':
+                                formattedValue = precision < 0
+                                    ? f.ToString()
+                                    : f.ToString($"G{precision}");
+                                break;
+                            case 'G':
+                                formattedValue = precision < 0
+                                    ? f.ToString().ToUpper()
+                                    : f.ToString($"G{precision}").ToUpper();
+                                break;
+                        }
+
+                        if (plusSign && f >= 0)
+                        {
+                            formattedValue = $"+{formattedValue}";
+                        }
+                        break;
+                    case 's':
+                        using (var strBuffer = new PooledArray<LuaValue>(1))
+                        {
+                            await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
+                            formattedValue = strBuffer[0].Read<string>();
+                        }
+
+                        if (specifier is 's' && precision > 0 && precision <= formattedValue.Length)
+                        {
+                            formattedValue = formattedValue[..precision];
+                        }
+                        break;
+                    case 'q':
+                        switch (parameter.Type)
+                        {
+                            case LuaValueType.Nil:
+                                formattedValue = "nil";
+                                break;
+                            case LuaValueType.Boolean:
+                                formattedValue = parameter.Read<bool>() ? "true" : "false";
+                                break;
+                            case LuaValueType.String:
+                                formattedValue = $"\"{StringHelper.Escape(parameter.Read<string>())}\"";
+                                break;
+                            case LuaValueType.Number:
+                                // TODO: floating point numbers must be in hexadecimal notation
+                                formattedValue = parameter.Read<double>().ToString();
+                                break;
+                            default:
+                                using (var strBuffer = new PooledArray<LuaValue>(1))
+                                {
+                                    await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
+                                    formattedValue = strBuffer[0].Read<string>();
+                                }
+                                break;
+                        }
+                        break;
+                    case 'i':
+                    case 'd':
+                    case 'u':
+                    case 'c':
+                    case 'x':
+                    case 'X':
+                        if (!parameter.TryRead<double>(out var x))
+                        {
+                            LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
+                        }
+
+                        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, parameterIndex + 1, x);
+
+                        switch (specifier)
+                        {
+                            case 'i':
+                            case 'd':
+                                {
+                                    var integer = checked((long)x);
+                                    formattedValue = precision < 0
+                                        ? integer.ToString()
+                                        : integer.ToString($"D{precision}");
+                                }
+                                break;
+                            case 'u':
+                                {
+                                    var integer = checked((ulong)x);
+                                    formattedValue = precision < 0
+                                        ? integer.ToString()
+                                        : integer.ToString($"D{precision}");
+                                }
+                                break;
+                            case 'c':
+                                formattedValue = ((char)(int)x).ToString();
+                                break;
+                            case 'x':
+                                {
+                                    var integer = checked((ulong)x);
+                                    formattedValue = alternateForm
+                                        ? $"0x{integer:x}"
+                                        : $"{integer:x}";
+                                }
+                                break;
+                            case 'X':
+                                {
+                                    var integer = checked((ulong)x);
+                                    formattedValue = alternateForm
+                                        ? $"0X{integer:X}"
+                                        : $"{integer:X}";
+                                }
+                                break;
+                            case 'o':
+                                {
+                                    var integer = checked((long)x);
+                                    formattedValue = Convert.ToString(integer, 8);
+                                }
+                                break;
+                        }
+
+                        if (plusSign && x >= 0)
+                        {
+                            formattedValue = $"+{formattedValue}";
+                        }
+                        break;
+                    default:
+                        throw new LuaRuntimeException(context.State.GetTraceback(), $"invalid option '%{specifier}' to 'format'");
+                }
+
+                // Apply blank (' ') flag for positive numbers
+                if (specifier is 'd' or 'i' or 'f' or 'g' or 'G')
+                {
+                    if (blank && !leftJustify && !zeroPadding && parameter.Read<double>() >= 0)
+                    {
+                        formattedValue = $" {formattedValue}";
+                    }
+                }
+
+                // Apply width and padding
+                if (width > formattedValue.Length)
+                {
+                    if (leftJustify)
+                    {
+                        formattedValue = formattedValue.PadRight(width);
+                    }
+                    else
+                    {
+                        formattedValue = zeroPadding ? formattedValue.PadLeft(width, '0') : formattedValue.PadLeft(width);
+                    }
+                }
+
+                builder.Append(formattedValue);
+            }
+            else
+            {
+                builder.Append(format[i]);
+            }
+        }
+
+
+        buffer.Span[0] = builder.ToString();
+        return 1;
+    }
+}

+ 55 - 0
src/Lua/Standard/Text/GMatchFunction.cs

@@ -0,0 +1,55 @@
+using System.Text.RegularExpressions;
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class GMatchFunction : LuaFunction
+{
+    public override string Name => "gmatch";
+    public static readonly GMatchFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        var pattern = context.GetArgument<string>(1);
+
+        var regex = StringHelper.ToRegex(pattern);
+        buffer.Span[0] = new Iterator(regex.Matches(s));
+        return new(1);
+    }
+
+    class Iterator(MatchCollection matches) : LuaFunction
+    {
+        int i;
+
+        protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+        {
+            if (matches.Count > i)
+            {
+                var match = matches[i];
+                var groups = match.Groups;
+
+                i++;
+
+                if (groups.Count == 1)
+                {
+                    buffer.Span[0] = match.Value;
+                }
+                else
+                {
+                    for (int j = 0; j < groups.Count; j++)
+                    {
+                        buffer.Span[j] = groups[j + 1].Value;
+                    }
+                }
+
+                return new(groups.Count);
+            }
+            else
+            {
+                buffer.Span[0] = LuaValue.Nil;
+                return new(1);
+            }
+        }
+    }
+}

+ 104 - 0
src/Lua/Standard/Text/GSubFunction.cs

@@ -0,0 +1,104 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class GSubFunction : LuaFunction
+{
+    public override string Name => "gsub";
+    public static readonly GSubFunction Instance = new();
+
+    protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        var pattern = context.GetArgument<string>(1);
+        var repl = context.GetArgument(2);
+        var n_arg = context.HasArgument(3)
+            ? context.GetArgument<double>(3)
+            : int.MaxValue;
+
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 4, n_arg);
+
+        var n = (int)n_arg;
+        var regex = StringHelper.ToRegex(pattern);
+        var matches = regex.Matches(s);
+
+        // TODO: reduce allocation
+        var builder = new StringBuilder();
+        var lastIndex = 0;
+        var replaceCount = 0;
+
+        for (int i = 0; i < matches.Count; i++)
+        {
+            if (replaceCount > n) break;
+
+            var match = matches[i];
+            builder.Append(s.AsSpan()[lastIndex..match.Index]);
+            replaceCount++;
+
+            LuaValue result;
+            if (repl.TryRead<string>(out var str))
+            {
+                result = str.Replace("%%", "%")
+                    .Replace("%0", match.Value);
+
+                for (int k = 1; k <= match.Groups.Count; k++)
+                {
+                    if (replaceCount > n) break;
+                    result = result.Read<string>().Replace($"%{k}", match.Groups[k].Value);
+                    replaceCount++;
+                }
+            }
+            else if (repl.TryRead<LuaTable>(out var table))
+            {
+                result = table[match.Groups[1].Value];
+            }
+            else if (repl.TryRead<LuaFunction>(out var func))
+            {
+                for (int k = 1; k <= match.Groups.Count; k++)
+                {
+                    context.State.Push(match.Groups[k].Value);
+                }
+
+                using var methodBuffer = new PooledArray<LuaValue>(1024);
+                await func.InvokeAsync(context with
+                {
+                    ArgumentCount = match.Groups.Count,
+                    StackPosition = null
+                }, methodBuffer.AsMemory(), cancellationToken);
+
+                result = methodBuffer[0];
+            }
+            else
+            {
+                throw new LuaRuntimeException(context.State.GetTraceback(), "bad argument #3 to 'gsub' (string/function/table expected)");
+            }
+
+            if (result.TryRead<string>(out var rs))
+            {
+                builder.Append(rs);
+            }
+            else if (result.TryRead<double>(out var rd))
+            {
+                builder.Append(rd);
+            }
+            else if (!result.ToBoolean())
+            {
+                builder.Append(match.Value);
+                replaceCount--;
+            }
+            else
+            {
+                throw new LuaRuntimeException(context.State.GetTraceback(), $"invalid replacement value (a {result.Type})");
+            }
+
+            lastIndex = match.Index + match.Length;
+        }
+
+        builder.Append(s.AsSpan()[lastIndex..s.Length]);
+
+        buffer.Span[0] = builder.ToString();
+        return 1;
+    }
+}

+ 15 - 0
src/Lua/Standard/Text/LenFunction.cs

@@ -0,0 +1,15 @@
+
+namespace Lua.Standard.Text;
+
+public sealed class LenFunction : LuaFunction
+{
+    public override string Name => "len";
+    public static readonly LenFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        buffer.Span[0] = s.Length;
+        return new(1);
+    }
+}

+ 15 - 0
src/Lua/Standard/Text/LowerFunction.cs

@@ -0,0 +1,15 @@
+
+namespace Lua.Standard.Text;
+
+public sealed class LowerFunction : LuaFunction
+{
+    public override string Name => "lower";
+    public static readonly LowerFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        buffer.Span[0] = s.ToLower();
+        return new(1);
+    }
+}

+ 36 - 0
src/Lua/Standard/Text/RepFunction.cs

@@ -0,0 +1,36 @@
+
+using System.Text;
+
+namespace Lua.Standard.Text;
+
+public sealed class RepFunction : LuaFunction
+{
+    public override string Name => "rep";
+    public static readonly RepFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        var n_arg = context.GetArgument<double>(1);
+        var sep = context.HasArgument(2)
+            ? context.GetArgument<string>(2)
+            : null;
+
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 2, n_arg);
+
+        var n = (int)n_arg;
+
+        var builder = new ValueStringBuilder(s.Length * n);
+        for (int i = 0; i < n; i++)
+        {
+            builder.Append(s);
+            if (i != n - 1 && sep != null)
+            {
+                builder.Append(sep);
+            }
+        }
+
+        buffer.Span[0] = builder.ToString();
+        return new(1);
+    }
+}

+ 20 - 0
src/Lua/Standard/Text/ReverseFunction.cs

@@ -0,0 +1,20 @@
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class ReverseFunction : LuaFunction
+{
+    public override string Name => "reverse";
+    public static readonly ReverseFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        using var strBuffer = new PooledArray<char>(s.Length);
+        var span = strBuffer.AsSpan()[..s.Length];
+        s.AsSpan().CopyTo(span);
+        span.Reverse();
+        buffer.Span[0] = span.ToString();
+        return new(1);
+    }
+}

+ 25 - 0
src/Lua/Standard/Text/SubFunction.cs

@@ -0,0 +1,25 @@
+
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+public sealed class SubFunction : LuaFunction
+{
+    public override string Name => "sub";
+    public static readonly SubFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        var i = context.GetArgument<double>(1);
+        var j = context.HasArgument(2)
+            ? context.GetArgument<double>(2)
+            : -1;
+
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 2, i);
+        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, 3, j);
+
+        buffer.Span[0] = StringHelper.Slice(s, (int)i, (int)j).ToString();
+        return new(1);
+    }
+}

+ 15 - 0
src/Lua/Standard/Text/UpperFunction.cs

@@ -0,0 +1,15 @@
+
+namespace Lua.Standard.Text;
+
+public sealed class UpperFunction : LuaFunction
+{
+    public override string Name => "upper";
+    public static readonly UpperFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var s = context.GetArgument<string>(0);
+        buffer.Span[0] = s.ToUpper();
+        return new(1);
+    }
+}