Browse Source

Merge pull request #14 from AnnulusGames/io-library

Add: IO Library
Annulus Games 1 year ago
parent
commit
e610b87852

+ 0 - 4
src/Lua/LuaFunction.cs

@@ -26,10 +26,6 @@ public abstract partial class LuaFunction
         {
         {
             return await InvokeAsyncCore(context, buffer, cancellationToken);
             return await InvokeAsyncCore(context, buffer, cancellationToken);
         }
         }
-        catch (Exception ex) when (ex is not (LuaException or OperationCanceledException))
-        {
-            throw new LuaRuntimeException(state.GetTraceback(), ex.Message);
-        }
         finally
         finally
         {
         {
             thread.PopCallStackFrame();
             thread.PopCallStackFrame();

+ 1 - 7
src/Lua/LuaValue.cs

@@ -132,18 +132,12 @@ public readonly struct LuaValue : IEquatable<LuaValue>
                     break;
                     break;
                 }
                 }
             case LuaValueType.UserData:
             case LuaValueType.UserData:
-                if (t == typeof(LuaUserData))
+                if (t == typeof(LuaUserData) || t.IsSubclassOf(typeof(LuaUserData)))
                 {
                 {
                     var v = (LuaUserData)referenceValue!;
                     var v = (LuaUserData)referenceValue!;
                     result = Unsafe.As<LuaUserData, T>(ref v);
                     result = Unsafe.As<LuaUserData, T>(ref v);
                     return true;
                     return true;
                 }
                 }
-                else if (t == typeof(LuaUserData<T>))
-                {
-                    var v = (LuaUserData<T>)referenceValue!;
-                    result = Unsafe.As<LuaUserData<T>, T>(ref v);
-                    return true;
-                }
                 else if (t == typeof(object))
                 else if (t == typeof(object))
                 {
                 {
                     result = (T)referenceValue!;
                     result = (T)referenceValue!;

+ 4 - 2
src/Lua/Runtime/LuaVirtualMachine.cs

@@ -105,10 +105,12 @@ public static partial class LuaVirtualMachine
                 case OpCode.Self:
                 case OpCode.Self:
                     {
                     {
                         stack.EnsureCapacity(RA + 2);
                         stack.EnsureCapacity(RA + 2);
-                        var table = stack.UnsafeGet(RB).Read<LuaTable>();
+                        var table = stack.UnsafeGet(RB);
                         var vc = RK(stack, chunk, instruction.C, frame.Base);
                         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 + 1) = table;
-                        stack.UnsafeGet(RA) = table[vc];
+                        stack.UnsafeGet(RA) = value;
                         stack.NotifyTop(RA + 2);
                         stack.NotifyTop(RA + 2);
                     }
                     }
                     break;
                     break;

+ 2 - 2
src/Lua/Runtime/Tracebacks.cs

@@ -6,8 +6,8 @@ public class Traceback
 {
 {
     public required CallStackFrame[] StackFrames { get; init; }
     public required CallStackFrame[] StackFrames { get; init; }
 
 
-    internal string RootChunkName => StackFrames[^1].RootChunkName;
-    internal SourcePosition LastPosition => StackFrames[^1].CallPosition!.Value;
+    internal string RootChunkName => StackFrames.Length == 0 ? "" : StackFrames[^1].RootChunkName;
+    internal SourcePosition LastPosition => StackFrames.Length == 0 ? default : StackFrames[^1].CallPosition!.Value;
 
 
     public override string ToString()
     public override string ToString()
     {
     {

+ 28 - 0
src/Lua/Standard/IO/CloseFunction.cs

@@ -0,0 +1,28 @@
+namespace Lua.Standard.IO;
+
+public sealed class CloseFunction : LuaFunction
+{
+    public override string Name => "close";
+    public static readonly CloseFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.ArgumentCount >= 1
+            ? context.ReadArgument<FileHandle>(0)
+            : context.State.Environment["io"].Read<LuaTable>()["stdout"].Read<FileHandle>();
+
+        try
+        {
+            file.Close();
+            buffer.Span[0] = true;
+            return new(1);
+        }
+        catch (IOException ex)
+        {
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return new(3);
+        }
+    }
+}

+ 26 - 0
src/Lua/Standard/IO/FileFlushFunction.cs

@@ -0,0 +1,26 @@
+namespace Lua.Standard.IO;
+
+public sealed class FileFlushFunction : LuaFunction
+{
+    public override string Name => "flush";
+    public static readonly FileFlushFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.ReadArgument<FileHandle>(0);
+
+        try
+        {
+            file.Flush();
+            buffer.Span[0] = true;
+            return new(1);
+        }
+        catch (IOException ex)
+        {
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return new(3);
+        }
+    }
+}

+ 133 - 0
src/Lua/Standard/IO/FileHandle.cs

@@ -0,0 +1,133 @@
+using Lua.Runtime;
+
+namespace Lua.Standard.IO;
+
+// TODO: optimize (remove StreamReader/Writer)
+
+public class FileHandle : LuaUserData
+{
+    class IndexMetamethod : LuaFunction
+    {
+        protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+        {
+            context.ReadArgument<FileHandle>(0);
+            var key = context.ReadArgument(1);
+
+            if (key.TryRead<string>(out var name))
+            {
+                buffer.Span[0] = name switch
+                {
+                    "write" => FileWriteFunction.Instance,
+                    "read" => FileReadFunction.Instance,
+                    "lines" => FileLinesFunction.Instance,
+                    "flush" => FileFlushFunction.Instance,
+                    "setvbuf" => FileSetVBufFunction.Instance,
+                    "close" => CloseFunction.Instance,
+                    _ => LuaValue.Nil,
+                };
+            }
+            else
+            {
+                buffer.Span[0] = LuaValue.Nil;
+            }
+
+            return new(1);
+        }
+    }
+
+    Stream stream;
+    StreamWriter? writer;
+    StreamReader? reader;
+    bool isClosed;
+
+    public bool IsClosed => Volatile.Read(ref isClosed);
+
+    static readonly LuaTable fileHandleMetatable;
+
+    static FileHandle()
+    {
+        fileHandleMetatable = new LuaTable();
+        fileHandleMetatable[Metamethods.Index] = new IndexMetamethod();
+    }
+
+    public FileHandle(Stream stream)
+    {
+        this.stream = stream;
+        if (stream.CanRead) reader = new StreamReader(stream);
+        if (stream.CanWrite) writer = new StreamWriter(stream);
+        Metatable = fileHandleMetatable;
+    }
+
+    public string? ReadLine()
+    {
+        return reader!.ReadLine();
+    }
+
+    public string ReadToEnd()
+    {
+        return reader!.ReadToEnd();
+    }
+
+    public int ReadByte()
+    {
+        return stream.ReadByte();
+    }
+
+    public void Write(ReadOnlySpan<char> buffer)
+    {
+        writer!.Write(buffer);
+    }
+
+    public long Seek(string whence, long offset)
+    {
+        if (whence != null)
+        {
+            switch (whence)
+            {
+                case "set":
+                    stream.Seek(offset, SeekOrigin.Begin);
+                    break;
+                case "cur":
+                    stream.Seek(offset, SeekOrigin.Current);
+                    break;
+                case "end":
+                    stream.Seek(offset, SeekOrigin.End);
+                    break;
+                default:
+                    throw new ArgumentException($"Invalid option '{whence}'");
+            }
+        }
+
+        return stream.Position;
+    }
+
+    public void Flush()
+    {
+        writer!.Flush();
+    }
+
+    public void SetVBuf(string mode, int size)
+    {
+        // Ignore size parameter
+
+        if (writer != null)
+        {
+            writer.AutoFlush = mode is "no" or "line";
+        }
+    }
+
+    public void Close()
+    {
+        if (isClosed) throw new ObjectDisposedException(nameof(FileHandle));
+        Volatile.Write(ref isClosed, true);
+
+        if (reader != null)
+        {
+            reader.Dispose();
+        }
+        else
+        {
+            stream.Close();
+        }
+    }
+}

+ 29 - 0
src/Lua/Standard/IO/FileLinesFunction.cs

@@ -0,0 +1,29 @@
+namespace Lua.Standard.IO;
+
+public sealed class FileLinesFunction : LuaFunction
+{
+    public override string Name => "lines";
+    public static readonly FileLinesFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var arg0 = context.ReadArgument<FileHandle>(0);
+        var arg1 = context.ArgumentCount >= 2
+            ? context.Arguments[1]
+            : "*l";
+        
+        buffer.Span[0] = new Iterator(arg0, arg1);
+        return new(1);
+    }
+
+    class Iterator(FileHandle file, LuaValue format) : LuaFunction
+    {
+        readonly LuaValue[] formats = [format];
+
+        protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+        {
+            var resultCount = IOHelper.Read(context.State, file, Name, 0, formats, buffer, true);
+            return new(resultCount);
+        }
+    }
+}

+ 18 - 0
src/Lua/Standard/IO/FileReadFunction.cs

@@ -0,0 +1,18 @@
+using System.Buffers.Text;
+using System.Text;
+using Lua.Internal;
+
+namespace Lua.Standard.IO;
+
+public sealed class FileReadFunction : LuaFunction
+{
+    public override string Name => "read";
+    public static readonly FileReadFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.ReadArgument<FileHandle>(0);
+        var resultCount = IOHelper.Read(context.State, file, Name, 1, context.Arguments[1..], buffer, false);
+        return new(resultCount);
+    }
+}

+ 41 - 0
src/Lua/Standard/IO/FileSeekFunction.cs

@@ -0,0 +1,41 @@
+namespace Lua.Standard.IO;
+
+public sealed class FileSeekFunction : LuaFunction
+{
+    public override string Name => "seek";
+    public static readonly FileSeekFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.ReadArgument<FileHandle>(0);
+        var whence = context.ArgumentCount >= 2
+            ? context.ReadArgument<string>(1)
+            : "cur";
+        var offset = context.ArgumentCount >= 3
+            ? context.ReadArgument<double>(2)
+            : 0;
+        
+        if (whence is not ("set" or "cur" or "end"))
+        {
+            throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #2 to 'seek' (invalid option '{whence}')");
+        }
+
+        if (!MathEx.IsInteger(offset))
+        {
+            throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #3 to 'seek' (number has no integer representation)");
+        }
+        
+        try
+        {
+            buffer.Span[0] = file.Seek(whence, (long)offset);
+            return new(1);
+        }
+        catch (IOException ex)
+        {
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return new(3);
+        }
+    }
+}

+ 26 - 0
src/Lua/Standard/IO/FileSetVBufFunction.cs

@@ -0,0 +1,26 @@
+namespace Lua.Standard.IO;
+
+public sealed class FileSetVBufFunction : LuaFunction
+{
+    public override string Name => "setvbuf";
+    public static readonly FileSetVBufFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.ReadArgument<FileHandle>(0);
+        var mode = context.ReadArgument<string>(1);
+        var size = context.ArgumentCount >= 3
+            ? context.ReadArgument<double>(2)
+            : -1;
+
+        if (!MathEx.IsInteger(size))
+        {
+            throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #3 to 'setvbuf' (number has no integer representation)");
+        }
+
+        file.SetVBuf(mode, (int)size);
+
+        buffer.Span[0] = true;
+        return new(1);
+    }
+}

+ 14 - 0
src/Lua/Standard/IO/FileWriteFunction.cs

@@ -0,0 +1,14 @@
+namespace Lua.Standard.IO;
+
+public sealed class FileWriteFunction : LuaFunction
+{
+    public override string Name => "write";
+    public static readonly FileWriteFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.ReadArgument<FileHandle>(0);
+        var resultCount = IOHelper.Write(file, Name, context, buffer);
+        return new(resultCount);
+    }
+}

+ 26 - 0
src/Lua/Standard/IO/FlushFunction.cs

@@ -0,0 +1,26 @@
+namespace Lua.Standard.IO;
+
+public sealed class FlushFunction : LuaFunction
+{
+    public override string Name => "flush";
+    public static readonly FlushFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.State.Environment["io"].Read<LuaTable>()["stdout"].Read<FileHandle>();
+
+        try
+        {
+            file.Flush();
+            buffer.Span[0] = true;
+            return new(1);
+        }
+        catch (IOException ex)
+        {
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return new(3);
+        }
+    }
+}

+ 166 - 0
src/Lua/Standard/IO/IOHelper.cs

@@ -0,0 +1,166 @@
+using System.Buffers.Text;
+using System.Text;
+using Lua.Internal;
+
+namespace Lua.Standard.IO;
+
+internal static class IOHelper
+{
+    public static int Open(LuaState state, string fileName, string mode, Memory<LuaValue> buffer, bool throwError)
+    {
+        var fileMode = mode switch
+        {
+            "r" or "rb" or "r+" or "r+b" => FileMode.Open,
+            "w" or "wb" or "w+" or "w+b" => FileMode.Create,
+            "a" or "ab" or "a+" or "a+b" => FileMode.Append,
+            _ => throw new LuaRuntimeException(state.GetTraceback(), "bad argument #2 to 'open' (invalid mode)"),
+        };
+
+        var fileAccess = mode switch
+        {
+            "r" or "rb" => FileAccess.Read,
+            "w" or "wb" or "a" or "ab" => FileAccess.Write,
+            _ => FileAccess.ReadWrite,
+        };
+
+        try
+        {
+            var stream = File.Open(fileName, fileMode, fileAccess);
+            buffer.Span[0] = new FileHandle(stream);
+            return 1;
+        }
+        catch (IOException ex)
+        {
+            if (throwError)
+            {
+                throw;
+            }
+
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return 3;
+        }
+    }
+
+    // TODO: optimize (use IBuffertWrite<byte>, async)
+
+    public static int Write(FileHandle file, string name, LuaFunctionExecutionContext context, Memory<LuaValue> buffer)
+    {
+        try
+        {
+            for (int i = 1; i < context.ArgumentCount; i++)
+            {
+                var arg = context.Arguments[i];
+                if (arg.TryRead<string>(out var str))
+                {
+                    file.Write(str);
+                }
+                else if (arg.TryRead<double>(out var d))
+                {
+                    using var fileBuffer = new PooledArray<char>(64);
+                    var span = fileBuffer.AsSpan();
+                    d.TryFormat(span, out var charsWritten);
+                    file.Write(span[..charsWritten]);
+                }
+                else
+                {
+                    LuaRuntimeException.BadArgument(context.State.GetTraceback(), i + 1, name);
+                }
+            }
+        }
+        catch (IOException ex)
+        {
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return 3;
+        }
+
+        buffer.Span[0] = file;
+        return 1;
+    }
+
+    static readonly LuaValue[] defaultReadFormat = ["*l"];
+
+    public static int Read(LuaState state, FileHandle file, string name, int startArgumentIndex, ReadOnlySpan<LuaValue> formats, Memory<LuaValue> buffer, bool throwError)
+    {
+        if (formats.Length == 0)
+        {
+            formats = defaultReadFormat;
+        }
+
+        try
+        {
+            for (int i = 0; i < formats.Length; i++)
+            {
+                var format = formats[i];
+                if (format.TryRead<string>(out var str))
+                {
+                    switch (str)
+                    {
+                        case "*n":
+                        case "*number":
+                            // TODO: support number format
+                            throw new NotImplementedException();
+                        case "*a":
+                        case "*all":
+                            buffer.Span[i] = file.ReadToEnd();
+                            break;
+                        case "*l":
+                        case "*line":
+                            buffer.Span[i] = file.ReadLine() ?? LuaValue.Nil;
+                            break;
+                        case "L":
+                        case "*L":
+                            var text = file.ReadLine();
+                            buffer.Span[i] = text == null ? LuaValue.Nil : text + Environment.NewLine;
+                            break;
+                    }
+                }
+                else if (format.TryRead<double>(out var d))
+                {
+                    if (!MathEx.IsInteger(d))
+                    {
+                        throw new LuaRuntimeException(state.GetTraceback(), $"bad argument #{i + startArgumentIndex} to 'read' (number has no integer representation)");
+                    }
+
+                    var count = (int)d;
+                    using var byteBuffer = new PooledArray<byte>(count);
+
+                    for (int j = 0; j < count; j++)
+                    {
+                        var b = file.ReadByte();
+                        if (b == -1)
+                        {
+                            buffer.Span[0] = LuaValue.Nil;
+                            return 1;
+                        }
+
+                        byteBuffer[j] = (byte)b;
+                    }
+
+                    buffer.Span[i] = Encoding.UTF8.GetString(byteBuffer.AsSpan());
+                }
+                else
+                {
+                    LuaRuntimeException.BadArgument(state.GetTraceback(), i + 1, name);
+                }
+            }
+
+            return formats.Length;
+        }
+        catch (IOException ex)
+        {
+            if (throwError)
+            {
+                throw;
+            }
+
+            buffer.Span[0] = LuaValue.Nil;
+            buffer.Span[1] = ex.Message;
+            buffer.Span[2] = ex.HResult;
+            return 3;
+        }
+    }
+}

+ 34 - 0
src/Lua/Standard/IO/InputFunction.cs

@@ -0,0 +1,34 @@
+namespace Lua.Standard.IO;
+
+public sealed class InputFunction : LuaFunction
+{
+    public override string Name => "input";
+    public static readonly InputFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var io = context.State.Environment["io"].Read<LuaTable>();
+
+        if (context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil)
+        {
+            buffer.Span[0] = io["stdio"];
+            return new(1);
+        }
+
+        var arg = context.Arguments[0];
+        if (arg.TryRead<FileHandle>(out var file))
+        {
+            io["stdio"] = file;
+            buffer.Span[0] = file;
+            return new(1);
+        }
+        else
+        {
+            var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
+            var handle = new FileHandle(stream);
+            io["stdio"] = handle;
+            buffer.Span[0] = handle;
+            return new(1);
+        }
+    }
+}

+ 45 - 0
src/Lua/Standard/IO/LinesFunction.cs

@@ -0,0 +1,45 @@
+using Lua.Internal;
+
+namespace Lua.Standard.IO;
+
+public sealed class LinesFunction : LuaFunction
+{
+    public override string Name => "lines";
+    public static readonly LinesFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        if (context.ArgumentCount == 0)
+        {
+            var file = context.State.Environment["io"].Read<LuaTable>()["stdio"].Read<FileHandle>();
+            buffer.Span[0] = new Iterator(file, []);
+            return new(1);
+        }
+        else
+        {
+            var fileName = context.ReadArgument<string>(0);
+
+            using var methodBuffer = new PooledArray<LuaValue>(32);
+            IOHelper.Open(context.State, fileName, "r", methodBuffer.AsMemory(), true);
+
+            var file = methodBuffer[0].Read<FileHandle>();
+            buffer.Span[0] = new Iterator(file, context.Arguments[1..]);
+            return new(1);
+        }
+    }
+
+    class Iterator(FileHandle file, ReadOnlySpan<LuaValue> formats) : LuaFunction
+    {
+        readonly LuaValue[] formats = formats.ToArray();
+
+        protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+        {
+            var resultCount = IOHelper.Read(context.State, file, Name, 0, formats, buffer, true);
+            if (resultCount > 0 && buffer.Span[0].Type is LuaValueType.Nil)
+            {
+                file.Close();
+            }
+            return new(resultCount);
+        }
+    }
+}

+ 18 - 0
src/Lua/Standard/IO/OpenFunction.cs

@@ -0,0 +1,18 @@
+namespace Lua.Standard.IO;
+
+public sealed class OpenFunction : LuaFunction
+{
+    public override string Name => "open";
+    public static readonly OpenFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var fileName = context.ReadArgument<string>(0);
+        var mode = context.ArgumentCount >= 2
+            ? context.ReadArgument<string>(1)
+            : "r";
+
+        var resultCount = IOHelper.Open(context.State, fileName, mode, buffer, false);
+        return new(resultCount);
+    }
+}

+ 34 - 0
src/Lua/Standard/IO/OutputFunction.cs

@@ -0,0 +1,34 @@
+namespace Lua.Standard.IO;
+
+public sealed class OutputFunction : LuaFunction
+{
+    public override string Name => "output";
+    public static readonly OutputFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var io = context.State.Environment["io"].Read<LuaTable>();
+
+        if (context.ArgumentCount == 0 || context.Arguments[0].Type is LuaValueType.Nil)
+        {
+            buffer.Span[0] = io["stdout"];
+            return new(1);
+        }
+
+        var arg = context.Arguments[0];
+        if (arg.TryRead<FileHandle>(out var file))
+        {
+            io["stdout"] = file;
+            buffer.Span[0] = file;
+            return new(1);
+        }
+        else
+        {
+            var stream = File.Open(arg.ToString()!, FileMode.Open, FileAccess.ReadWrite);
+            var handle = new FileHandle(stream);
+            io["stdout"] = handle;
+            buffer.Span[0] = handle;
+            return new(1);
+        }
+    }
+}

+ 14 - 0
src/Lua/Standard/IO/ReadFunction.cs

@@ -0,0 +1,14 @@
+namespace Lua.Standard.IO;
+
+public sealed class ReadFunction : LuaFunction
+{
+    public override string Name => "read";
+    public static readonly ReadFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.State.Environment["io"].Read<LuaTable>()["stdio"].Read<FileHandle>();
+        var resultCount = IOHelper.Read(context.State, file, Name, 0, context.Arguments, buffer, false);
+        return new(resultCount);
+    }
+}

+ 23 - 0
src/Lua/Standard/IO/TypeFunction.cs

@@ -0,0 +1,23 @@
+namespace Lua.Standard.IO;
+
+public sealed class TypeFunction : LuaFunction
+{
+    public override string Name => "type";
+    public static readonly TypeFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var arg0 = context.ReadArgument(0);
+
+        if (arg0.TryRead<FileHandle>(out var file))
+        {
+            buffer.Span[0] = file.IsClosed ? "closed file" : "file";
+        }
+        else
+        {
+            buffer.Span[0] = LuaValue.Nil;
+        }
+
+        return new(1);
+    }
+}

+ 14 - 0
src/Lua/Standard/IO/WriteFunction.cs

@@ -0,0 +1,14 @@
+namespace Lua.Standard.IO;
+
+public sealed class WriteFunction : LuaFunction
+{
+    public override string Name => "write";
+    public static readonly WriteFunction Instance = new();
+
+    protected override ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var file = context.State.Environment["io"].Read<LuaTable>()["stdout"].Read<FileHandle>();
+        var resultCount = IOHelper.Write(file, Name, context, buffer);
+        return new(resultCount);
+    }
+}

+ 29 - 2
src/Lua/Standard/OpenLibExtensions.cs

@@ -1,5 +1,6 @@
 using Lua.Standard.Basic;
 using Lua.Standard.Basic;
 using Lua.Standard.Coroutines;
 using Lua.Standard.Coroutines;
+using Lua.Standard.IO;
 using Lua.Standard.Mathematics;
 using Lua.Standard.Mathematics;
 using Lua.Standard.Modules;
 using Lua.Standard.Modules;
 using Lua.Standard.Table;
 using Lua.Standard.Table;
@@ -24,7 +25,7 @@ public static class OpenLibExtensions
         NextFunction.Instance,
         NextFunction.Instance,
         IPairsFunction.Instance,
         IPairsFunction.Instance,
         PairsFunction.Instance,
         PairsFunction.Instance,
-        TypeFunction.Instance,
+        Basic.TypeFunction.Instance,
         PCallFunction.Instance,
         PCallFunction.Instance,
         XPCallFunction.Instance,
         XPCallFunction.Instance,
         DoFileFunction.Instance,
         DoFileFunction.Instance,
@@ -72,6 +73,17 @@ public static class OpenLibExtensions
         SortFunction.Instance,
         SortFunction.Instance,
     ];
     ];
 
 
+    static readonly LuaFunction[] ioFunctions = [
+        OpenFunction.Instance,
+        CloseFunction.Instance,
+        InputFunction.Instance,
+        OutputFunction.Instance,
+        WriteFunction.Instance,
+        ReadFunction.Instance,
+        LinesFunction.Instance,
+        IO.TypeFunction.Instance,
+    ];
+
     public static void OpenBasicLibrary(this LuaState state)
     public static void OpenBasicLibrary(this LuaState state)
     {
     {
         // basic
         // basic
@@ -114,7 +126,7 @@ public static class OpenLibExtensions
         var package = new LuaTable(0, 1);
         var package = new LuaTable(0, 1);
         package["loaded"] = new LuaTable();
         package["loaded"] = new LuaTable();
         state.Environment["package"] = package;
         state.Environment["package"] = package;
-        
+
         state.Environment[RequireFunction.Instance.Name] = RequireFunction.Instance;
         state.Environment[RequireFunction.Instance.Name] = RequireFunction.Instance;
     }
     }
 
 
@@ -128,4 +140,19 @@ public static class OpenLibExtensions
 
 
         state.Environment["table"] = table;
         state.Environment["table"] = table;
     }
     }
+
+    public static void OpenIOLibrary(this LuaState state)
+    {
+        var io = new LuaTable(0, ioFunctions.Length);
+        foreach (var func in ioFunctions)
+        {
+            io[func.Name] = func;
+        }
+
+        io["stdio"] = new FileHandle(Console.OpenStandardInput());
+        io["stdout"] = new FileHandle(Console.OpenStandardOutput());
+        io["stderr"] = new FileHandle(Console.OpenStandardError());
+
+        state.Environment["io"] = io;
+    }
 }
 }