Browse Source

add: Comprehensive Lua API for basic operations

Akeit0 7 months ago
parent
commit
8249ad46b3
3 changed files with 709 additions and 5 deletions
  1. 149 0
      src/Lua/LuaThreadExtensions.cs
  2. 355 5
      src/Lua/Runtime/LuaVirtualMachine.cs
  3. 205 0
      tests/Lua.Tests/LuaApiTests.cs

+ 149 - 0
src/Lua/LuaThreadExtensions.cs

@@ -126,4 +126,153 @@ public static class LuaThreadExtensions
         var coreData = thread.CoreData!;
         coreData!.CallStack.Pop();
     }
+
+    public static async ValueTask<LuaValue> OpArithmetic(this LuaThread thread, LuaValue left, LuaValue right, OpCode opCode, CancellationToken ct = default)
+    {
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        static double Mod(double a, double b)
+        {
+            var mod = a % b;
+            if ((b > 0 && mod < 0) || (b < 0 && mod > 0))
+            {
+                mod += b;
+            }
+
+            return mod;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        static double ArithmeticOperation(OpCode code, double a, double b)
+        {
+            return code switch
+            {
+                OpCode.Add => a + b,
+                OpCode.Sub => a - b,
+                OpCode.Mul => a * b,
+                OpCode.Div => a / b,
+                OpCode.Mod => Mod(a, b),
+                OpCode.Pow => Math.Pow(a, b),
+                _ => throw new InvalidOperationException($"Unsupported arithmetic operation: {code}"),
+            };
+        }
+
+
+        if (left.TryReadDouble(out var numB) && right.TryReadDouble(out var numC))
+        {
+            return ArithmeticOperation(opCode, numB, numC);
+        }
+
+        return await LuaVirtualMachine.ExecuteBinaryOperationMetaMethod(thread, left, right, opCode, ct);
+    }
+
+    public static async ValueTask<LuaValue> OpUnary(this LuaThread thread, LuaValue left, OpCode opCode, CancellationToken ct = default)
+    {
+        if (opCode == OpCode.Unm)
+        {
+            if (left.TryReadDouble(out var numB))
+            {
+                return -numB;
+            }
+        }
+        else if (opCode == OpCode.Len)
+        {
+            if (left.TryReadString(out var str))
+            {
+                return str.Length;
+            }
+
+            if (left.TryReadTable(out var table))
+            {
+                return table.ArrayLength;
+            }
+        }
+        else
+        {
+            throw new InvalidOperationException($"Unsupported unary operation: {opCode}");
+        }
+
+
+        return await LuaVirtualMachine.ExecuteUnaryOperationMetaMethod(thread, left, opCode, ct);
+    }
+
+
+    public static async ValueTask<bool> OpCompare(this LuaThread thread, LuaValue vb, LuaValue vc, OpCode opCode, CancellationToken ct = default)
+    {
+        if (opCode is not (OpCode.Eq or OpCode.Lt or OpCode.Le))
+        {
+            throw new InvalidOperationException($"Unsupported compare operation: {opCode}");
+        }
+
+        if (opCode == OpCode.Eq)
+        {
+            if (vb == vc)
+            {
+                return true;
+            }
+        }
+        else
+        {
+            if (vb.TryReadNumber(out var numB) && vc.TryReadNumber(out var numC))
+            {
+                return opCode == OpCode.Lt ? numB < numC : numB <= numC;
+            }
+
+            if (vb.TryReadString(out var strB) && vc.TryReadString(out var strC))
+            {
+                var c = StringComparer.Ordinal.Compare(strB, strC);
+                return opCode == OpCode.Lt ? c < 0 : c <= 0;
+            }
+        }
+
+
+        return await LuaVirtualMachine.ExecuteCompareOperationMetaMethod(thread, vb, vc, opCode, ct);
+    }
+
+    public static ValueTask<LuaValue> OpGetTable(this LuaThread thread, LuaValue table, LuaValue key, CancellationToken ct = default)
+    {
+        if (table.TryReadTable(out var luaTable))
+        {
+            if (luaTable.TryGetValue(key, out var value))
+            {
+                return new(value);
+            }
+        }
+
+        return LuaVirtualMachine.ExecuteGetTableSlowPath(thread, table, key, ct);
+    }
+
+    public static ValueTask OpSetTable(this LuaThread thread, LuaValue table, LuaValue key, LuaValue value, CancellationToken ct = default)
+    {
+        if (key.TryReadNumber(out var numB))
+        {
+            if (double.IsNaN(numB))
+            {
+                throw new LuaRuntimeException(thread, "table index is NaN");
+            }
+        }
+
+
+        if (table.TryReadTable(out var luaTable))
+        {
+            ref var valueRef = ref luaTable.FindValue(key);
+            if (!Unsafe.IsNullRef(ref valueRef) && valueRef.Type != LuaValueType.Nil)
+            {
+                valueRef = value;
+                return default;
+            }
+        }
+
+        return LuaVirtualMachine.ExecuteSetTableSlowPath(thread, table, key, value, ct);
+    }
+    
+    public static ValueTask<LuaValue> OpConcat(this LuaThread thread, ReadOnlySpan<LuaValue> values,CancellationToken ct = default)
+    {
+        thread.Stack.PushRange(values);
+        return OpConcat(thread, values.Length, ct);
+    }
+    public static ValueTask<LuaValue> OpConcat(this LuaThread thread, int concatCount,CancellationToken ct = default)
+    {
+        
+        return LuaVirtualMachine.Concat(thread,  concatCount, ct);
+    }
 }

+ 355 - 5
src/Lua/Runtime/LuaVirtualMachine.cs

@@ -933,6 +933,77 @@ public static partial class LuaVirtualMachine
         return 1;
     }
 
+    internal static async ValueTask<LuaValue> Concat(LuaThread thread, int total, CancellationToken ct)
+    {
+        static bool ToString(ref LuaValue v)
+        {
+            if (v.Type == LuaValueType.String) return true;
+            if (v.Type == LuaValueType.Number)
+            {
+                v = v.ToString();
+                return true;
+            }
+
+            return false;
+        }
+
+        var stack = thread.Stack;
+        do
+        {
+            var top = thread.Stack.Count;
+            var n = 2;
+            ref var lhs = ref stack.Get(top - 2);
+            ref var rhs = ref stack.Get(top - 1);
+            if (!(lhs.Type is LuaValueType.String or LuaValueType.Number) || !ToString(ref rhs))
+            {
+                var value = await ExecuteBinaryOperationMetaMethod(thread, lhs, rhs, OpCode.Concat, ct);
+                stack.Get(top - 2) = value;
+            }
+            else if (rhs.UnsafeReadString().Length == 0)
+            {
+                ToString(ref lhs);
+            }
+            else if (lhs.TryReadString(out var str) && str.Length == 0)
+            {
+                lhs = rhs;
+            }
+            else
+            {
+                var tl = rhs.UnsafeReadString().Length;
+
+                int i = 1;
+                for (; i < total; i++)
+                {
+                    ref var v = ref stack.Get(top - i - 1);
+                    if (!ToString(ref v))
+                    {
+                        break;
+                    }
+
+                    tl += v.UnsafeReadString().Length;
+                }
+
+                n = i;
+                stack.Get(top - n) = string.Create(tl, (stack, top - n), static (span, pair) =>
+                {
+                    var (stack, index) = pair;
+                    foreach (var v in stack.AsSpan().Slice(index))
+                    {
+                        var s = v.UnsafeReadString();
+                        if (s.Length == 0) continue;
+                        s.AsSpan().CopyTo(span);
+                        span = span[s.Length..];
+                    }
+                });
+            }
+
+            total -= n - 1;
+            stack.PopUntil(top - (n - 1));
+        } while (total > 1);
+
+        return stack.AsSpan()[^1];
+    }
+
     static async ValueTask ExecuteBinaryOperationMetaMethod(int target, LuaValue vb, LuaValue vc,
         VirtualMachineExecutionContext context, OpCode opCode)
     {
@@ -1363,6 +1434,74 @@ public static partial class LuaVirtualMachine
         return true;
     }
 
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    internal static ValueTask<LuaValue> ExecuteGetTableSlowPath(LuaThread thread, LuaValue table, LuaValue key, CancellationToken ct)
+    {
+        var targetTable = table;
+        const int MAX_LOOP = 100;
+        var skip = targetTable.Type == LuaValueType.Table;
+        for (int i = 0; i < MAX_LOOP; i++)
+        {
+            if (table.TryReadTable(out var luaTable))
+            {
+                if (!skip && luaTable.TryGetValue(key, out var value))
+                {
+                    return new(value);
+                }
+
+                skip = false;
+
+                var metatable = luaTable.Metatable;
+                if (metatable != null && metatable.TryGetValue(Metamethods.Index, out table))
+                {
+                    goto Function;
+                }
+
+                return default(ValueTask<LuaValue>);
+            }
+
+            if (!table.TryGetMetamethod(thread.State, Metamethods.Index, out var metatableValue))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(thread, "index", table);
+            }
+
+            table = metatableValue;
+        Function:
+            if (table.TryReadFunction(out var function))
+            {
+                return CallGetTableFunc(thread, function, targetTable, key, ct);
+            }
+        }
+
+        throw new LuaRuntimeException(thread, "loop in gettable");
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    static async ValueTask<LuaValue> CallGetTableFunc(LuaThread thread, LuaFunction indexTable, LuaValue table, LuaValue key, CancellationToken ct)
+    {
+        var stack = thread.Stack;
+        var top = stack.Count;
+        stack.Push(table);
+        stack.Push(key);
+        var varArgCount = indexTable.GetVariableArgumentCount(3);
+
+        var newFrame = new CallStackFrame() { Base = thread.Stack.Count - 2 + varArgCount, VariableArgumentCount = varArgCount, Function = indexTable, ReturnBase = top };
+
+        thread.PushCallStackFrame(newFrame);
+        var functionContext = new LuaFunctionExecutionContext() { Thread = thread, ArgumentCount = 3, ReturnFrameBase = top };
+        if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook)
+        {
+            await ExecuteCallHook(functionContext, ct);
+        }
+
+        await indexTable.Func(functionContext, ct);
+        var results = stack.GetBuffer()[newFrame.ReturnBase..];
+        var result = results.Length == 0 ? default : results[0];
+        results.Clear();
+        thread.PopCallStackFrameWithStackPop();
+        return result;
+    }
+
     [MethodImpl(MethodImplOptions.NoInlining)]
     static bool SetTableValueSlowPath(LuaValue table, LuaValue key, LuaValue value,
         VirtualMachineExecutionContext context, out bool doRestart)
@@ -1458,6 +1597,81 @@ public static partial class LuaVirtualMachine
         return true;
     }
 
+    internal static ValueTask ExecuteSetTableSlowPath(LuaThread thread, LuaValue table, LuaValue key, LuaValue value, CancellationToken ct)
+    {
+        var targetTable = table;
+        const int MAX_LOOP = 100;
+        var skip = targetTable.Type == LuaValueType.Table;
+        for (int i = 0; i < MAX_LOOP; i++)
+        {
+            if (table.TryReadTable(out var luaTable))
+            {
+                targetTable = luaTable;
+                ref var valueRef = ref (skip ? ref Unsafe.NullRef<LuaValue>() : ref luaTable.FindValue(key));
+                skip = false;
+                if (!Unsafe.IsNullRef(ref valueRef) && valueRef.Type != LuaValueType.Nil)
+                {
+                    valueRef = value;
+                    return default(ValueTask);
+                }
+
+                var metatable = luaTable.Metatable;
+                if (metatable == null || !metatable.TryGetValue(Metamethods.NewIndex, out table))
+                {
+                    if (Unsafe.IsNullRef(ref valueRef))
+                    {
+                        luaTable[key] = value;
+                        return default(ValueTask);
+                    }
+
+                    valueRef = value;
+                    return default;
+                }
+
+                goto Function;
+            }
+
+            if (!table.TryGetMetamethod(thread.State, Metamethods.NewIndex, out var metatableValue))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(thread, "index", table);
+            }
+
+            table = metatableValue;
+
+        Function:
+            if (table.TryReadFunction(out var function))
+            {
+                return CallSetTableFunc(thread, function, targetTable, key, value, ct);
+            }
+        }
+
+        throw new LuaRuntimeException(thread, "loop in settable");
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    static async ValueTask CallSetTableFunc(LuaThread thread, LuaFunction newIndexFunction, LuaValue table, LuaValue key, LuaValue value, CancellationToken ct)
+    {
+        var stack = thread.Stack;
+        var top = stack.Count;
+        stack.Push(table);
+        stack.Push(key);
+        stack.Push(value);
+        var varArgCount = newIndexFunction.GetVariableArgumentCount(3);
+
+        var newFrame = new CallStackFrame() { Base = thread.Stack.Count - 3 + varArgCount, VariableArgumentCount = varArgCount, Function = newIndexFunction, ReturnBase = top };
+
+        thread.PushCallStackFrame(newFrame);
+        var functionContext = new LuaFunctionExecutionContext() { Thread = thread, ArgumentCount = 3, ReturnFrameBase = top };
+        if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook)
+        {
+            await ExecuteCallHook(functionContext, ct);
+        }
+
+        await newIndexFunction.Func(functionContext, ct);
+        var results = stack.GetBuffer()[newFrame.ReturnBase..];
+        results.Clear();
+        thread.PopCallStackFrameWithStackPop();
+    }
 
     [MethodImpl(MethodImplOptions.NoInlining)]
     static bool ExecuteBinaryOperationMetaMethod(LuaValue vb, LuaValue vc,
@@ -1510,7 +1724,7 @@ public static partial class LuaVirtualMachine
 
             var RA = context.Instruction.A + context.FrameBase;
 
-            var results = stack.GetBuffer()[newFrame.Base..];
+            var results = stack.GetBuffer()[newFrame.ReturnBase..];
             stack.Get(RA) = results.Length == 0 ? default : results[0];
             results.Clear();
             context.Thread.PopCallStackFrameWithStackPop();
@@ -1521,6 +1735,47 @@ public static partial class LuaVirtualMachine
         return false;
     }
 
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    internal static async ValueTask<LuaValue> ExecuteBinaryOperationMetaMethod(LuaThread thread, LuaValue vb, LuaValue vc, OpCode opCode, CancellationToken ct)
+    {
+        var (name, description) = opCode.GetNameAndDescription();
+
+        if (vb.TryGetMetamethod(thread.State, name, out var metamethod) ||
+            vc.TryGetMetamethod(thread.State, name, out metamethod))
+        {
+            if (!metamethod.TryReadFunction(out var func))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(thread, "call", metamethod);
+            }
+
+            var stack = thread.Stack;
+            var top = stack.Count;
+            stack.Push(vb);
+            stack.Push(vc);
+            var varArgCount = func.GetVariableArgumentCount(2);
+
+            var newFrame = new CallStackFrame() { Base = thread.Stack.Count - 2 + varArgCount, VariableArgumentCount = varArgCount, Function = func, ReturnBase = top };
+
+            thread.PushCallStackFrame(newFrame);
+            var functionContext = new LuaFunctionExecutionContext() { Thread = thread, ArgumentCount = 2, ReturnFrameBase = top };
+            if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook)
+            {
+                await ExecuteCallHook(functionContext, ct);
+            }
+
+
+            await func.Func(functionContext, ct);
+            var results = stack.GetBuffer()[newFrame.ReturnBase..];
+            var result = results.Length == 0 ? default : results[0];
+            results.Clear();
+            thread.PopCallStackFrameWithStackPop();
+            return result;
+        }
+
+        LuaRuntimeException.AttemptInvalidOperation(thread, description, vb, vc);
+        return default;
+    }
+
     [MethodImpl(MethodImplOptions.NoInlining)]
     static bool ExecuteUnaryOperationMetaMethod(LuaValue vb, VirtualMachineExecutionContext context,
         OpCode opCode, out bool doRestart)
@@ -1585,6 +1840,45 @@ public static partial class LuaVirtualMachine
         return true;
     }
 
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    internal static async ValueTask<LuaValue> ExecuteUnaryOperationMetaMethod(LuaThread thread, LuaValue vb, OpCode opCode, CancellationToken ct)
+    {
+        var (name, description) = opCode.GetNameAndDescription();
+
+        if (vb.TryGetMetamethod(thread.State, name, out var metamethod))
+        {
+            if (!metamethod.TryReadFunction(out var func))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(thread, "call", metamethod);
+            }
+
+            var stack = thread.Stack;
+            var top = stack.Count;
+            stack.Push(vb);
+            var varArgCount = func.GetVariableArgumentCount(1);
+
+            var newFrame = new CallStackFrame() { Base = thread.Stack.Count - 1 + varArgCount, VariableArgumentCount = varArgCount, Function = func, ReturnBase = top };
+
+            thread.PushCallStackFrame(newFrame);
+            var functionContext = new LuaFunctionExecutionContext() { Thread = thread, ArgumentCount = 2, ReturnFrameBase = top };
+            if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook)
+            {
+                await ExecuteCallHook(functionContext, ct);
+            }
+
+
+            await func.Func(functionContext, ct);
+            var results = stack.GetBuffer()[newFrame.ReturnBase..];
+            var result = results.Length == 0 ? default : results[0];
+            results.Clear();
+            thread.PopCallStackFrameWithStackPop();
+            return result;
+        }
+
+        LuaRuntimeException.AttemptInvalidOperation(thread, description, vb);
+        return default;
+    }
+
     [MethodImpl(MethodImplOptions.NoInlining)]
     static bool ExecuteCompareOperationMetaMethod(LuaValue vb, LuaValue vc,
         VirtualMachineExecutionContext context, OpCode opCode, out bool doRestart)
@@ -1673,6 +1967,65 @@ public static partial class LuaVirtualMachine
         return true;
     }
 
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    internal static async ValueTask<bool> ExecuteCompareOperationMetaMethod(LuaThread thread, LuaValue vb, LuaValue vc, OpCode opCode, CancellationToken ct)
+    {
+        var (name, description) = opCode.GetNameAndDescription();
+        bool reverseLe = false;
+    ReCheck:
+        if (vb.TryGetMetamethod(thread.State, name, out var metamethod) ||
+            vc.TryGetMetamethod(thread.State, name, out metamethod))
+        {
+            if (!metamethod.TryReadFunction(out var func))
+            {
+                LuaRuntimeException.AttemptInvalidOperation(thread, "call", metamethod);
+            }
+
+            var stack = thread.Stack;
+            var top = stack.Count;
+            stack.Push(vb);
+            stack.Push(vc);
+            var varArgCount = func.GetVariableArgumentCount(2);
+
+            var newFrame = new CallStackFrame() { Base = thread.Stack.Count - 2 + varArgCount, VariableArgumentCount = varArgCount, Function = func, ReturnBase = top };
+
+            thread.PushCallStackFrame(newFrame);
+            var functionContext = new LuaFunctionExecutionContext() { Thread = thread, ArgumentCount = 2, ReturnFrameBase = top };
+            if (thread.CallOrReturnHookMask.Value != 0 && !thread.IsInHook)
+            {
+                await ExecuteCallHook(functionContext, ct);
+            }
+
+
+            await func.Func(functionContext, ct);
+            var results = stack.GetBuffer()[newFrame.ReturnBase..];
+            var result = results.Length == 0 ? default : results[0];
+            results.Clear();
+            thread.PopCallStackFrameWithStackPop();
+            return result.ToBoolean();
+        }
+
+        if (opCode == OpCode.Le)
+        {
+            reverseLe = true;
+            name = Metamethods.Lt;
+            (vb, vc) = (vc, vb);
+            goto ReCheck;
+        }
+
+        if (opCode != OpCode.Eq)
+        {
+            if (reverseLe)
+            {
+                (vb, vc) = (vc, vb);
+            }
+
+            LuaRuntimeException.AttemptInvalidOperation(thread, description, vb, vc);
+        }
+
+        return default;
+    }
+
     // If there are variable arguments, the base of the stack is moved by that number and the values of the variable arguments are placed in front of it.
     // see: https://wubingzheng.github.io/build-lua-in-rust/en/ch08-02.arguments.html
     [MethodImpl(MethodImplOptions.NoInlining)]
@@ -1845,9 +2198,6 @@ public static partial class LuaVirtualMachine
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     static ValueTask<int> Invoke(this LuaFunction function, VirtualMachineExecutionContext context, in CallStackFrame frame, int arguments)
     {
-        return function.Func(new()
-        {
-            Thread = context.Thread, ArgumentCount = arguments, ReturnFrameBase = frame.ReturnBase,
-        }, context.CancellationToken);
+        return function.Func(new() { Thread = context.Thread, ArgumentCount = arguments, ReturnFrameBase = frame.ReturnBase, }, context.CancellationToken);
     }
 }

+ 205 - 0
tests/Lua.Tests/LuaApiTests.cs

@@ -0,0 +1,205 @@
+using Lua.Runtime;
+using Lua.Standard;
+
+namespace Lua.Tests;
+
+public class LuaApiTests
+{
+    LuaState state = default!;
+
+    [OneTimeSetUp]
+    public void SetUp()
+    {
+        state = LuaState.Create();
+        state.OpenStandardLibraries();
+    }
+
+    [Test]
+    public async Task TestArithmetic()
+    {
+        var source = """
+                     metatable = {
+                         __add = function(a, b)
+                             local t = { }
+                             for i = 1, #a do
+                                 t[i] = a[i] + b[i]
+                             end
+                             return t
+                         end
+                     }
+
+                     local a = { 1, 2, 3 }
+                     local b = { 4, 5, 6 }
+
+                     setmetatable(a, metatable)
+                     return a, b
+                     """;
+        var result = await state.DoStringAsync(source);
+        var a = result[0].Read<LuaTable>();
+        var b = result[1].Read<LuaTable>();
+
+        var c = await state.MainThread.OpArithmetic(a, b, OpCode.Add);
+        var table = c.Read<LuaTable>();
+        Assert.Multiple(() =>
+        {
+            Assert.That(table[1].Read<double>(), Is.EqualTo(5));
+            Assert.That(table[2].Read<double>(), Is.EqualTo(7));
+            Assert.That(table[3].Read<double>(), Is.EqualTo(9));
+        });
+    }
+
+    [Test]
+    public async Task TestUnary()
+    {
+        var source = """
+                     metatable = {
+                         __unm = function(a)
+                             local t = { }
+                             for i = 1, #a do
+                                 t[i] = -a[i]
+                             end
+                             return t
+                         end
+                     }
+
+                     local a = { 1, 2, 3 }
+
+                     setmetatable(a, metatable)
+                     return a
+                     """;
+        var result = await state.DoStringAsync(source);
+        var a = result[0].Read<LuaTable>();
+
+        var c = await state.MainThread.OpUnary(a, OpCode.Unm);
+        var table = c.Read<LuaTable>();
+        Assert.Multiple(() =>
+        {
+            Assert.That(table[1].Read<double>(), Is.EqualTo(-1));
+            Assert.That(table[2].Read<double>(), Is.EqualTo(-2));
+            Assert.That(table[3].Read<double>(), Is.EqualTo(-3));
+        });
+    }
+
+    [Test]
+    public async Task TestCompare()
+    {
+        var source = """
+                     metatable = {
+                         __eq = function(a, b)
+                             if(#a ~= #b) then
+                                 return false
+                             end
+                             for i = 1, #a do
+                                if(a[i] ~= b[i]) then
+                                    return  false
+                                end
+                             end
+                             return true
+                         end
+                     }
+
+                     local a = { 1, 2, 3 }
+                     local b = { 4, 5, 6 }
+                     local c = { 1, 2, 3 }
+                     setmetatable(a, metatable)
+                     return a, b, c
+                     """;
+        var result = await state.DoStringAsync(source);
+        var a = result[0].Read<LuaTable>();
+        var b = result[1].Read<LuaTable>();
+        var c = result[2].Read<LuaTable>();
+        var ab = await state.MainThread.OpCompare(a, b, OpCode.Eq);
+        Assert.False(ab);
+        var ac = await state.MainThread.OpCompare(a, c, OpCode.Eq);
+        Assert.True(ac);
+    }
+
+    [Test]
+    public async Task TestGetTable()
+    {
+        var source = """
+                     metatable = {
+                         __index = {x=1}
+                     }
+
+                     local a = {}
+                     setmetatable(a, metatable)
+                     return a
+                     """;
+        var result = await state.DoStringAsync(source);
+        var a = result[0].Read<LuaTable>();
+        Assert.That(await state.MainThread.OpGetTable(a, "x"), Is.EqualTo(new LuaValue(1)));
+        a.Metatable!["__index"] = state.DoStringAsync("return function(a,b) return b end").Result[0];
+        Assert.That(await state.MainThread.OpGetTable(a, "x"), Is.EqualTo(new LuaValue("x")));
+    }
+
+    [Test]
+    public async Task TestSetTable()
+    {
+        var source = """
+                     metatable = {
+                         __newindex = {}
+                     }
+
+                     local a = {}
+                     a.x = 1
+                     setmetatable(a, metatable)
+                     return a
+                     """;
+        var result = await state.DoStringAsync(source);
+        var a = result[0].Read<LuaTable>();
+        await state.MainThread.OpSetTable(a, "a", "b");
+        var b = a.Metatable!["__newindex"].Read<LuaTable>()["a"];
+        Assert.True(b.Read<string>() == "b");
+    }
+
+    [Test]
+    public async Task Test_Metamethod_Concat()
+    {
+        var source = @"
+metatable = {
+    __concat = function(a, b)
+        local t = { }
+        for i = 1, #a do
+            table.insert(t, a[i])
+        end
+        for i = 1, #b do
+            table.insert(t, b[i])
+        end
+        return t
+    end
+}
+
+local a = { 1, 2, 3 }
+local b = { 4, 5, 6 }
+local c = { 7, 8, 9 }
+setmetatable(a, metatable)
+setmetatable(c, metatable)
+
+return a,b,c
+";
+
+        var result = await state.DoStringAsync(source);
+        Assert.That(result, Has.Length.EqualTo(3));
+
+        var a = result[0];
+        var b = result[1];
+        var c = result[2];
+        var d = await state.MainThread.OpConcat([a, b, c]);
+
+        var table = d.Read<LuaTable>();
+        Assert.That(table.ArrayLength, Is.EqualTo(9));
+        Assert.Multiple(() =>
+        {
+            Assert.That(table[1].Read<double>(), Is.EqualTo(1));
+            Assert.That(table[2].Read<double>(), Is.EqualTo(2));
+            Assert.That(table[3].Read<double>(), Is.EqualTo(3));
+            Assert.That(table[4].Read<double>(), Is.EqualTo(4));
+            Assert.That(table[5].Read<double>(), Is.EqualTo(5));
+            Assert.That(table[6].Read<double>(), Is.EqualTo(6));
+            Assert.That(table[7].Read<double>(), Is.EqualTo(7));
+            Assert.That(table[8].Read<double>(), Is.EqualTo(8));
+            Assert.That(table[9].Read<double>(), Is.EqualTo(9));
+        });
+    }
+}